Skip to content

Commit 7d5dc19

Browse files
committed
Add BinaryHeap Implementation and Comprehensive Tests
- Implemented BinaryHeap class supporting both min-heap and max-heap functionality. - Methods: add, insert, poll, remove, peek, size, isEmpty, heapifyUp, heapifyDown, swap, compare. - Ensured compare supports elements implementing Comparable interface. - Added comprehensive tests for BinaryHeap: - Tests for add, poll, insert, remove, peek, size, isEmpty. - Covered edge cases, including empty heaps and insertion at specific indices. - Fully tested heapifyDown method, covering all conditional branches. - Included mock objects for Comparable elements in compare method.
1 parent 1d2a409 commit 7d5dc19

File tree

2 files changed

+387
-0
lines changed

2 files changed

+387
-0
lines changed

src/Heap/BinaryHeap.php

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace KaririCode\DataStructure\Heap;
6+
7+
use KaririCode\Contract\DataStructure\Behavioral\Comparable;
8+
use KaririCode\Contract\DataStructure\Behavioral\Countable;
9+
use KaririCode\Contract\DataStructure\Heap;
10+
11+
/**
12+
* BinaryHeap implementation.
13+
*
14+
* This class implements a binary heap (min-heap or max-heap) using a dynamic array.
15+
* It provides O(log n) time complexity for add, poll, and remove operations, and O(1) for peek and isEmpty operations.
16+
*
17+
* @category Heaps
18+
*
19+
* @author Walmir Silva <[email protected]>
20+
* @license MIT
21+
*
22+
* @see https://kariricode.org/
23+
*/
24+
class BinaryHeap implements Heap, Countable
25+
{
26+
private array $heap;
27+
private string $type;
28+
29+
public function __construct(string $type = 'min')
30+
{
31+
$this->heap = [];
32+
$this->type = $type;
33+
}
34+
35+
public function add(mixed $element): void
36+
{
37+
$this->heap[] = $element;
38+
$this->heapifyUp();
39+
}
40+
41+
public function insert(int $index, mixed $element): void
42+
{
43+
// Inserting at a specific index is not typical for a binary heap
44+
// but we'll implement it to satisfy the interface
45+
if ($index < 0 || $index > $this->size()) {
46+
throw new \OutOfRangeException('Index out of range');
47+
}
48+
$this->heap[$index] = $element;
49+
$this->heapifyUp($index);
50+
$this->heapifyDown($index);
51+
}
52+
53+
public function poll(): mixed
54+
{
55+
if ($this->isEmpty()) {
56+
return null;
57+
}
58+
59+
$root = $this->heap[0];
60+
$lastElement = array_pop($this->heap);
61+
62+
if (! $this->isEmpty()) {
63+
$this->heap[0] = $lastElement;
64+
$this->heapifyDown();
65+
}
66+
67+
return $root;
68+
}
69+
70+
public function remove(mixed $element): bool
71+
{
72+
$index = array_search($element, $this->heap, true);
73+
if (false === $index) {
74+
return false;
75+
}
76+
77+
$lastElement = array_pop($this->heap);
78+
if ($index < $this->size()) {
79+
$this->heap[$index] = $lastElement;
80+
$this->heapifyUp($index);
81+
$this->heapifyDown($index);
82+
}
83+
84+
return true;
85+
}
86+
87+
public function peek(): mixed
88+
{
89+
return $this->heap[0] ?? null;
90+
}
91+
92+
public function size(): int
93+
{
94+
return count($this->heap);
95+
}
96+
97+
public function isEmpty(): bool
98+
{
99+
return empty($this->heap);
100+
}
101+
102+
private function heapifyUp(?int $index = null): void
103+
{
104+
$index = $index ?? $this->size() - 1;
105+
while ($index > 0) {
106+
$parentIndex = ($index - 1) >> 1;
107+
if ($this->compare($this->heap[$index], $this->heap[$parentIndex])) {
108+
$this->swap($index, $parentIndex);
109+
$index = $parentIndex;
110+
} else {
111+
break;
112+
}
113+
}
114+
}
115+
116+
private function heapifyDown(int $index = 0): void
117+
{
118+
$size = $this->size();
119+
120+
while (true) {
121+
$leftChild = ($index << 1) + 1;
122+
$rightChild = ($index << 1) + 2;
123+
$largest = $index;
124+
125+
if ($leftChild < $size && $this->compare($this->heap[$leftChild], $this->heap[$largest])) {
126+
$largest = $leftChild;
127+
}
128+
129+
if ($rightChild < $size && $this->compare($this->heap[$rightChild], $this->heap[$largest])) {
130+
$largest = $rightChild;
131+
}
132+
133+
if ($largest !== $index) {
134+
$this->swap($index, $largest);
135+
$index = $largest;
136+
} else {
137+
break;
138+
}
139+
}
140+
}
141+
142+
private function swap(int $i, int $j): void
143+
{
144+
[$this->heap[$i], $this->heap[$j]] = [$this->heap[$j], $this->heap[$i]];
145+
}
146+
147+
private function compare(mixed $a, mixed $b): bool
148+
{
149+
if ($a instanceof Comparable && $b instanceof Comparable) {
150+
return 'min' === $this->type
151+
? $a->compareTo($b) < 0
152+
: $a->compareTo($b) > 0;
153+
}
154+
155+
return 'min' === $this->type ? $a < $b : $a > $b;
156+
}
157+
}

tests/Heap/BinaryHeapTest.php

+230
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace KaririCode\DataStructure\Tests;
6+
7+
use KaririCode\Contract\DataStructure\Behavioral\Comparable;
8+
use KaririCode\DataStructure\Heap\BinaryHeap;
9+
use PHPUnit\Framework\TestCase;
10+
11+
final class BinaryHeapTest extends TestCase
12+
{
13+
public function testAdd(): void
14+
{
15+
$heap = new BinaryHeap();
16+
$heap->add(3);
17+
$heap->add(1);
18+
$heap->add(2);
19+
20+
$this->assertSame(1, $heap->peek());
21+
}
22+
23+
public function testPoll(): void
24+
{
25+
$heap = new BinaryHeap();
26+
$heap->add(3);
27+
$heap->add(1);
28+
$heap->add(2);
29+
30+
$this->assertSame(1, $heap->poll());
31+
$this->assertSame(2, $heap->poll());
32+
$this->assertSame(3, $heap->poll());
33+
$this->assertNull($heap->poll());
34+
}
35+
36+
public function testHeapifyDown(): void
37+
{
38+
$heap = new BinaryHeap();
39+
$heap->add(5);
40+
$heap->add(3);
41+
$heap->add(8);
42+
$heap->add(1);
43+
$heap->add(6);
44+
45+
$this->assertSame(1, $heap->poll());
46+
$this->assertSame(3, $heap->poll());
47+
$this->assertSame(5, $heap->poll());
48+
$this->assertSame(6, $heap->poll());
49+
$this->assertSame(8, $heap->poll());
50+
}
51+
52+
public function testHeapifyDownComplex(): void
53+
{
54+
$heap = new BinaryHeap();
55+
$heap->add(10);
56+
$heap->add(15);
57+
$heap->add(20);
58+
$heap->add(17);
59+
$heap->add(25);
60+
61+
$this->assertSame(10, $heap->poll());
62+
$this->assertSame(15, $heap->poll());
63+
$this->assertSame(17, $heap->poll());
64+
$this->assertSame(20, $heap->poll());
65+
$this->assertSame(25, $heap->poll());
66+
}
67+
68+
public function testCompare(): void
69+
{
70+
$minHeap = new BinaryHeap('min');
71+
$minHeap->add(10);
72+
$minHeap->add(5);
73+
$minHeap->add(20);
74+
$this->assertSame(5, $minHeap->poll());
75+
$this->assertSame(10, $minHeap->poll());
76+
$this->assertSame(20, $minHeap->poll());
77+
78+
$maxHeap = new BinaryHeap('max');
79+
$maxHeap->add(10);
80+
$maxHeap->add(5);
81+
$maxHeap->add(20);
82+
$this->assertSame(20, $maxHeap->poll());
83+
$this->assertSame(10, $maxHeap->poll());
84+
$this->assertSame(5, $maxHeap->poll());
85+
}
86+
87+
public function testRemove(): void
88+
{
89+
$heap = new BinaryHeap();
90+
$heap->add(3);
91+
$heap->add(1);
92+
$heap->add(2);
93+
94+
$this->assertTrue($heap->remove(1));
95+
$this->assertFalse($heap->remove(4));
96+
$this->assertSame(2, $heap->peek());
97+
}
98+
99+
public function testPeek(): void
100+
{
101+
$heap = new BinaryHeap();
102+
$this->assertNull($heap->peek());
103+
104+
$heap->add(3);
105+
$this->assertSame(3, $heap->peek());
106+
107+
$heap->add(1);
108+
$this->assertSame(1, $heap->peek());
109+
}
110+
111+
public function testSize(): void
112+
{
113+
$heap = new BinaryHeap();
114+
$this->assertSame(0, $heap->size());
115+
116+
$heap->add(3);
117+
$this->assertSame(1, $heap->size());
118+
119+
$heap->add(1);
120+
$heap->add(2);
121+
$this->assertSame(3, $heap->size());
122+
123+
$heap->poll();
124+
$this->assertSame(2, $heap->size());
125+
}
126+
127+
public function testIsEmpty(): void
128+
{
129+
$heap = new BinaryHeap();
130+
$this->assertTrue($heap->isEmpty());
131+
132+
$heap->add(3);
133+
$this->assertFalse($heap->isEmpty());
134+
135+
$heap->poll();
136+
$this->assertTrue($heap->isEmpty());
137+
}
138+
139+
public function testInsert(): void
140+
{
141+
$heap = new BinaryHeap();
142+
$heap->add(3);
143+
$heap->add(1);
144+
$heap->add(2);
145+
146+
$heap->insert(1, 0);
147+
$this->assertSame(0, $heap->peek());
148+
149+
$this->expectException(\OutOfRangeException::class);
150+
$heap->insert(-1, 4);
151+
}
152+
153+
public function testComparableElements(): void
154+
{
155+
$element1 = $this->createMock(Comparable::class);
156+
$element2 = $this->createMock(Comparable::class);
157+
$element3 = $this->createMock(Comparable::class);
158+
159+
$element1->method('compareTo')->willReturn(1);
160+
$element2->method('compareTo')->willReturn(0);
161+
$element3->method('compareTo')->willReturn(-1);
162+
163+
$heap = new BinaryHeap('max');
164+
$heap->add($element1);
165+
$heap->add($element2);
166+
$heap->add($element3);
167+
168+
$this->assertSame($element1, $heap->peek());
169+
}
170+
171+
public function testHeapifyDownWithRightChildComparison(): void
172+
{
173+
$heap = new BinaryHeap();
174+
$heap->add(10);
175+
$heap->add(15);
176+
$heap->add(20);
177+
$heap->add(17);
178+
$heap->add(8);
179+
$heap->add(25);
180+
181+
$this->assertSame(8, $heap->poll());
182+
$this->assertSame(10, $heap->poll());
183+
$this->assertSame(15, $heap->poll());
184+
$this->assertSame(17, $heap->poll());
185+
$this->assertSame(20, $heap->poll());
186+
$this->assertSame(25, $heap->poll());
187+
}
188+
189+
public function testCompareWithMinType(): void
190+
{
191+
$heap = new BinaryHeap('min');
192+
193+
$element1 = $this->createMock(Comparable::class);
194+
$element2 = $this->createMock(Comparable::class);
195+
196+
$element1->method('compareTo')->willReturn(1);
197+
$element2->method('compareTo')->willReturn(-1);
198+
199+
$reflection = new \ReflectionClass($heap);
200+
$method = $reflection->getMethod('compare');
201+
$method->setAccessible(true);
202+
203+
$result1 = $method->invokeArgs($heap, [$element1, $element2]);
204+
$result2 = $method->invokeArgs($heap, [$element2, $element1]);
205+
206+
$this->assertFalse($result1);
207+
$this->assertTrue($result2);
208+
}
209+
210+
public function testCompareWithMaxType(): void
211+
{
212+
$heap = new BinaryHeap('max');
213+
214+
$element1 = $this->createMock(Comparable::class);
215+
$element2 = $this->createMock(Comparable::class);
216+
217+
$element1->method('compareTo')->willReturn(1);
218+
$element2->method('compareTo')->willReturn(-1);
219+
220+
$reflection = new \ReflectionClass($heap);
221+
$method = $reflection->getMethod('compare');
222+
$method->setAccessible(true);
223+
224+
$result1 = $method->invokeArgs($heap, [$element1, $element2]);
225+
$result2 = $method->invokeArgs($heap, [$element2, $element1]);
226+
227+
$this->assertTrue($result1);
228+
$this->assertFalse($result2);
229+
}
230+
}

0 commit comments

Comments
 (0)