Skip to content

Commit 375ef5b

Browse files
authored
Add PriorityQueue (#27)
1 parent 971745e commit 375ef5b

File tree

2 files changed

+253
-0
lines changed

2 files changed

+253
-0
lines changed

src/PriorityQueue.php

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Amp\Sync;
4+
5+
/**
6+
* Uses a binary tree stored in an array to implement a heap.
7+
*
8+
* @template T of int|string
9+
*/
10+
final class PriorityQueue
11+
{
12+
/** @var array<int, object{key: T, priority: int}> */
13+
private array $data = [];
14+
15+
/** @var array<T, int> */
16+
private array $pointers = [];
17+
18+
/**
19+
* Inserts the key into the queue with the given priority or updates the priority if the key
20+
* already exists in the queue.
21+
*
22+
* Time complexity: O(log(n)).
23+
*
24+
* @param T $key
25+
*/
26+
public function insert(int|string $key, int $priority): void
27+
{
28+
if (isset($this->pointers[$key])) {
29+
$node = $this->pointers[$key];
30+
$entry = $this->data[$node];
31+
32+
$previous = $entry->priority;
33+
$entry->priority = $priority;
34+
35+
// Nothing to be done if priorities are equal.
36+
if ($previous < $priority) {
37+
$this->heapifyDown($node);
38+
} elseif ($previous > $priority) {
39+
$this->heapifyUp($node);
40+
}
41+
42+
return;
43+
}
44+
45+
$entry = new class($key, $priority) {
46+
public function __construct(
47+
public readonly int|string $key,
48+
public int $priority,
49+
) {
50+
}
51+
};
52+
53+
$node = \count($this->data);
54+
$this->data[$node] = $entry;
55+
$this->pointers[$key] = $node;
56+
57+
$this->heapifyUp($node);
58+
}
59+
60+
/**
61+
* Removes the given key from the queue.
62+
*
63+
* Time complexity: O(log(n)).
64+
*
65+
* @param T $key
66+
*/
67+
public function remove(int|string $key): void
68+
{
69+
if (!isset($this->pointers[$key])) {
70+
return;
71+
}
72+
73+
$this->removeAndRebuild($this->pointers[$key]);
74+
}
75+
76+
/**
77+
* Deletes and returns the data at the top of the queue if the priority is less than the priority given.
78+
*
79+
* Time complexity: O(log(n)).
80+
*
81+
* @param int $priority Extract data with a priority less than the given priority.
82+
*
83+
* @return T|null
84+
*/
85+
public function extract(int $priority = \PHP_INT_MAX): int|string|null
86+
{
87+
$data = $this->data[0] ?? null;
88+
if ($data === null || $data->priority > $priority) {
89+
return null;
90+
}
91+
92+
$this->removeAndRebuild(0);
93+
94+
return $data->key;
95+
}
96+
97+
/**
98+
* Returns the data at top of the heap or null if empty. Time complexity: O(1).
99+
*
100+
* @return T|null
101+
*/
102+
public function peekData(): int|string|null
103+
{
104+
return ($this->data[0] ?? null)?->key;
105+
}
106+
107+
/**
108+
* Returns the priority at top of the heap or null if empty. Time complexity: O(1).
109+
*/
110+
public function peekPriority(): ?int
111+
{
112+
return ($this->data[0] ?? null)?->priority;
113+
}
114+
115+
public function isEmpty(): bool
116+
{
117+
return empty($this->data);
118+
}
119+
120+
/**
121+
* @param int $node Rebuild the data array from the given node upward.
122+
*/
123+
private function heapifyUp(int $node): void
124+
{
125+
$entry = $this->data[$node];
126+
while ($node !== 0 && $entry->priority < $this->data[$parent = ($node - 1) >> 1]->priority) {
127+
$this->swap($node, $parent);
128+
$node = $parent;
129+
}
130+
}
131+
132+
/**
133+
* @param int $node Rebuild the data array from the given node downward.
134+
*/
135+
private function heapifyDown(int $node): void
136+
{
137+
$length = \count($this->data);
138+
while (($child = ($node << 1) + 1) < $length) {
139+
if ($this->data[$child]->priority < $this->data[$node]->priority
140+
&& ($child + 1 >= $length || $this->data[$child]->priority < $this->data[$child + 1]->priority)
141+
) {
142+
// Left child is less than parent and right child.
143+
$swap = $child;
144+
} elseif ($child + 1 < $length && $this->data[$child + 1]->priority < $this->data[$node]->priority) {
145+
// Right child is less than parent and left child.
146+
$swap = $child + 1;
147+
} else { // Left and right child are greater than parent.
148+
break;
149+
}
150+
151+
$this->swap($node, $swap);
152+
$node = $swap;
153+
}
154+
}
155+
156+
private function swap(int $left, int $right): void
157+
{
158+
$temp = $this->data[$left];
159+
160+
$this->data[$left] = $this->data[$right];
161+
$this->pointers[$this->data[$right]->key] = $left;
162+
163+
$this->data[$right] = $temp;
164+
$this->pointers[$temp->key] = $right;
165+
}
166+
167+
/**
168+
* @param int $node Remove the given node and then rebuild the data array.
169+
*/
170+
private function removeAndRebuild(int $node): void
171+
{
172+
$length = \count($this->data) - 1;
173+
$id = $this->data[$node]->key;
174+
$left = $this->data[$node] = $this->data[$length];
175+
$this->pointers[$left->key] = $node;
176+
unset($this->data[$length], $this->pointers[$id]);
177+
178+
if ($node < $length) { // don't need to do anything if we removed the last element
179+
$parent = ($node - 1) >> 1;
180+
if ($parent >= 0 && $this->data[$node]->priority < $this->data[$parent]->priority) {
181+
$this->heapifyUp($node);
182+
} else {
183+
$this->heapifyDown($node);
184+
}
185+
}
186+
}
187+
}

test/PriorityQueueTest.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Amp\Sync;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
class PriorityQueueTest extends TestCase
8+
{
9+
public function provideTestValues(): iterable
10+
{
11+
return [
12+
[100, 0, 0],
13+
[100, 0, 10],
14+
[100, 10, 0],
15+
[100, 10, 10],
16+
[1000, 25, 25],
17+
[1000, 100, 100],
18+
[10, 0, 0],
19+
[10, 3, 3],
20+
[5, 1, 2],
21+
];
22+
}
23+
24+
/**
25+
* @dataProvider provideTestValues
26+
*/
27+
public function testOrdering(int $count, int $toRemove, $toIncrement): void
28+
{
29+
$priorities = \range(0, $count - 1);
30+
\shuffle($priorities);
31+
32+
$queue = new PriorityQueue();
33+
34+
foreach ($priorities as $key => $priority) {
35+
$queue->insert($key, $priority);
36+
}
37+
38+
for ($i = 0; $i < $toIncrement; ++$i) {
39+
$index = \random_int(0, $count - 1);
40+
$queue->insert($index, $count + $i);
41+
$priorities[$index] = $count + $i;
42+
}
43+
44+
$i = 0;
45+
while ($i < $toRemove) {
46+
$index = \random_int(0, $count - 1);
47+
if (!isset($priorities[$index])) {
48+
continue;
49+
}
50+
51+
unset($priorities[$index]);
52+
$queue->remove($index);
53+
++$i;
54+
}
55+
56+
$output = [];
57+
while (($extracted = $queue->extract()) !== null) {
58+
$output[] = $extracted;
59+
}
60+
61+
\asort($priorities);
62+
63+
self::assertCount(\count($priorities), $output);
64+
self::assertSame(\array_keys($priorities), $output);
65+
}
66+
}

0 commit comments

Comments
 (0)