Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PriorityQueue #27

Merged
merged 1 commit into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions src/PriorityQueue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php declare(strict_types=1);

namespace Amp\Sync;

/**
* Uses a binary tree stored in an array to implement a heap.
*
* @template T of int|string
*/
final class PriorityQueue
{
/** @var array<int, object{key: T, priority: int}> */
private array $data = [];

/** @var array<T, int> */
private array $pointers = [];

/**
* Inserts the key into the queue with the given priority or updates the priority if the key
* already exists in the queue.
*
* Time complexity: O(log(n)).
*
* @param T $key
*/
public function insert(int|string $key, int $priority): void
{
if (isset($this->pointers[$key])) {
$node = $this->pointers[$key];
$entry = $this->data[$node];

$previous = $entry->priority;
$entry->priority = $priority;

// Nothing to be done if priorities are equal.
if ($previous < $priority) {
$this->heapifyDown($node);
} elseif ($previous > $priority) {
$this->heapifyUp($node);
}

return;
}

$entry = new class($key, $priority) {
public function __construct(
public readonly int|string $key,
public int $priority,
) {
}
};

$node = \count($this->data);
$this->data[$node] = $entry;
$this->pointers[$key] = $node;

$this->heapifyUp($node);
}

/**
* Removes the given key from the queue.
*
* Time complexity: O(log(n)).
*
* @param T $key
*/
public function remove(int|string $key): void
{
if (!isset($this->pointers[$key])) {
return;
}

$this->removeAndRebuild($this->pointers[$key]);
}

/**
* Deletes and returns the data at the top of the queue if the priority is less than the priority given.
*
* Time complexity: O(log(n)).
*
* @param int $priority Extract data with a priority less than the given priority.
*
* @return T|null
*/
public function extract(int $priority = \PHP_INT_MAX): int|string|null
{
$data = $this->data[0] ?? null;
if ($data === null || $data->priority > $priority) {
return null;
}

$this->removeAndRebuild(0);

return $data->key;
}

/**
* Returns the data at top of the heap or null if empty. Time complexity: O(1).
*
* @return T|null
*/
public function peekData(): int|string|null
{
return ($this->data[0] ?? null)?->key;
}

/**
* Returns the priority at top of the heap or null if empty. Time complexity: O(1).
*/
public function peekPriority(): ?int
{
return ($this->data[0] ?? null)?->priority;
}

public function isEmpty(): bool
{
return empty($this->data);
}

/**
* @param int $node Rebuild the data array from the given node upward.
*/
private function heapifyUp(int $node): void
{
$entry = $this->data[$node];
while ($node !== 0 && $entry->priority < $this->data[$parent = ($node - 1) >> 1]->priority) {
$this->swap($node, $parent);
$node = $parent;
}
}

/**
* @param int $node Rebuild the data array from the given node downward.
*/
private function heapifyDown(int $node): void
{
$length = \count($this->data);
while (($child = ($node << 1) + 1) < $length) {
if ($this->data[$child]->priority < $this->data[$node]->priority
&& ($child + 1 >= $length || $this->data[$child]->priority < $this->data[$child + 1]->priority)
) {
// Left child is less than parent and right child.
$swap = $child;
} elseif ($child + 1 < $length && $this->data[$child + 1]->priority < $this->data[$node]->priority) {
// Right child is less than parent and left child.
$swap = $child + 1;
} else { // Left and right child are greater than parent.
break;
}

$this->swap($node, $swap);
$node = $swap;
}
}

private function swap(int $left, int $right): void
{
$temp = $this->data[$left];

$this->data[$left] = $this->data[$right];
$this->pointers[$this->data[$right]->key] = $left;

$this->data[$right] = $temp;
$this->pointers[$temp->key] = $right;
}

/**
* @param int $node Remove the given node and then rebuild the data array.
*/
private function removeAndRebuild(int $node): void
{
$length = \count($this->data) - 1;
$id = $this->data[$node]->key;
$left = $this->data[$node] = $this->data[$length];
$this->pointers[$left->key] = $node;
unset($this->data[$length], $this->pointers[$id]);

if ($node < $length) { // don't need to do anything if we removed the last element
$parent = ($node - 1) >> 1;
if ($parent >= 0 && $this->data[$node]->priority < $this->data[$parent]->priority) {
$this->heapifyUp($node);
} else {
$this->heapifyDown($node);
}
}
}
}
66 changes: 66 additions & 0 deletions test/PriorityQueueTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php declare(strict_types=1);

namespace Amp\Sync;

use PHPUnit\Framework\TestCase;

class PriorityQueueTest extends TestCase
{
public function provideTestValues(): iterable
{
return [
[100, 0, 0],
[100, 0, 10],
[100, 10, 0],
[100, 10, 10],
[1000, 25, 25],
[1000, 100, 100],
[10, 0, 0],
[10, 3, 3],
[5, 1, 2],
];
}

/**
* @dataProvider provideTestValues
*/
public function testOrdering(int $count, int $toRemove, $toIncrement): void
{
$priorities = \range(0, $count - 1);
\shuffle($priorities);

$queue = new PriorityQueue();

foreach ($priorities as $key => $priority) {
$queue->insert($key, $priority);
}

for ($i = 0; $i < $toIncrement; ++$i) {
$index = \random_int(0, $count - 1);
$queue->insert($index, $count + $i);
$priorities[$index] = $count + $i;
}

$i = 0;
while ($i < $toRemove) {
$index = \random_int(0, $count - 1);
if (!isset($priorities[$index])) {
continue;
}

unset($priorities[$index]);
$queue->remove($index);
++$i;
}

$output = [];
while (($extracted = $queue->extract()) !== null) {
$output[] = $extracted;
}

\asort($priorities);

self::assertCount(\count($priorities), $output);
self::assertSame(\array_keys($priorities), $output);
}
}
Loading