Skip to content

Commit

Permalink
feat: add priority queue data structure (#66)
Browse files Browse the repository at this point in the history
* feat(priority-queue): implement priority queue

implement priority queue using a min heap-based implementation.

* test(priority-queue): add sorting and stability test for priority queue

* fix(priority-queue): fix unstable heap implementation
unstable heap implementation makes priority queue not respecting FIFO order when priority is the same.
  • Loading branch information
ngtrhieu committed Jul 22, 2020
1 parent 75f1f48 commit f40d4bc
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 0 deletions.
133 changes: 133 additions & 0 deletions src/data-structures/PriorityQueue.ts
@@ -0,0 +1,133 @@
/**
* A heap-based priority queue implementation
*/

const TOP = 0;
const PARENT = (i: number) => ((i + 1) >>> 1) - 1;
const LEFT = (i: number) => (i << 1) + 1;
const RIGHT = (i: number) => (i + 1) << 1;

interface Comparer<T> {
(a: T, b: T): number;
}

class Timestamped<T> {
private _value: T;
private _timestamp: number;

constructor(value: T, timestamp: number) {
this._value = value;
this._timestamp = timestamp;
}

public get value(): T {
return this._value;
}
public get timestamp(): number {
return this._timestamp;
}
}

class PriorityQueue<T> {
private _heap: Array<Timestamped<T>> = [];
private _comparer: Comparer<T>;
private _nextTimestamp: number = 0;

constructor(comparer: Comparer<T>) {
this._comparer = comparer;
}

/**
* Get the current size of the queue.
*/
public size(): number {
return this._heap.length;
}

/**
* Get the first item in the queue without removing it.
*/
public peek(): T {
return this._heap[TOP] && this._heap[TOP].value;
}

/**
* Add items into the queue.
* @param values the list of items to be added
*/
public enqueue(...values: Array<T>) {
values.forEach(value => {
this._heap.push(new Timestamped(value, this._nextTimestamp++));

// heapify up
let index = this.size() - 1;
while (
index > TOP &&
this._compareNodeAtIndex(index, PARENT(index)) < 0
) {
this._swapNodeAtIndex(index, PARENT(index));
index = PARENT(index);
}
});
}

/**
* Remove the first item from the queue and return it.
*/
public dequeue(): T {
const popped = this.peek();
const bottom = this.size() - 1;
if (bottom > TOP) {
this._swapNodeAtIndex(bottom, TOP);
}
this._heap.pop();

// Start heapify down
let current = TOP;
while (true) {
const parent = current;
const left = LEFT(current);
const right = RIGHT(current);

// Find the maximum node between among the 3 nodes above
if (left < this.size() && this._compareNodeAtIndex(left, current) < 0) {
current = left;
}
if (right < this.size() && this._compareNodeAtIndex(right, current) < 0) {
current = right;
}

// If the parent node is already max, stop heapifying
if (parent === current) {
break;
}

this._swapNodeAtIndex(parent, current);
}

return popped;
}

/**
* Heaper function to swap two nodes using indices
* @param i index of the first node
* @param j index of the second node
*/
private _swapNodeAtIndex(i: number, j: number) {
[this._heap[i], this._heap[j]] = [this._heap[j], this._heap[i]];
}

/**
* Heaper function to compare two nodes using indices
* @param i index of the first node
* @param j index of the second node
*/
private _compareNodeAtIndex(i: number, j: number): number {
return (
this._comparer(this._heap[i].value, this._heap[j].value) ||
this._heap[i].timestamp - this._heap[j].timestamp
);
}
}

export default PriorityQueue;
125 changes: 125 additions & 0 deletions test/data-structures/PriorityQueue.test.ts
@@ -0,0 +1,125 @@
import _ from 'lodash';
import PriorityQueue from '../../src/data-structures/PriorityQueue';

interface PriorityNode {
label: string | number;
priority: number;
}

const priorityNodeAscending = (a: PriorityNode, b: PriorityNode): number =>
a.priority - b.priority;

const numberAscending = (a: number, b: number): number => a - b;
const numberDescending = (a: number, b: number): number => b - a;

describe('PriorityQueue', () => {
describe('constructor()', () => {
test('empty queue', () => {
const queue = new PriorityQueue<number>(numberAscending);
expect(queue).toBeTruthy();
});
});

describe('sorting tests', () => {
test('number should be sorted ascending', () => {
const queue = new PriorityQueue<number>(numberAscending);
queue.enqueue(1, 5, 3, 2, 5, 4, 0, 6);

const array: Array<number> = [];
while (queue.peek() !== undefined) {
array.push(queue.dequeue());
}

expect(array).toEqual([0, 1, 2, 3, 4, 5, 5, 6]);
});

test('number should be sorted decending', () => {
const queue = new PriorityQueue<number>(numberDescending);
queue.enqueue(1, 5, 3, 2, 5, 4, 0, 6);

const array: Array<number> = [];
while (queue.peek() !== undefined) {
array.push(queue.dequeue());
}

expect(array).toEqual([6, 5, 5, 4, 3, 2, 1, 0]);
});

test('random generated test x10', () => {
_.times(10, () => {
const queue = new PriorityQueue<number>(numberAscending);
_.times(1000, () => queue.enqueue(Math.random() * 100));

const array: Array<number> = [];
while (queue.peek() !== undefined) {
array.push(queue.dequeue());
}

expect(
array.every((_, i) => !i || array[i - 1] < array[i]),
).toBeTruthy();
});
});
});

describe('stability tests', () => {
test('order of labels with same priority should be reserved', () => {
const queue = new PriorityQueue<PriorityNode>(priorityNodeAscending);
queue.enqueue(
{ label: 'a', priority: 0 },
{ label: 'd', priority: -1 },
{ label: 'b', priority: 0 },
{ label: 'e', priority: 1 },
{ label: 'c', priority: 0 },
{ label: 'f', priority: -1 },
);

const array: Array<PriorityNode> = [];
while (queue.peek() !== undefined) {
array.push(queue.dequeue());
}

expect(array).toEqual([
{ label: 'd', priority: -1 },
{ label: 'f', priority: -1 },
{ label: 'a', priority: 0 },
{ label: 'b', priority: 0 },
{ label: 'c', priority: 0 },
{ label: 'e', priority: 1 },
]);
});

test('random generated test x10', () => {
_.times(10, () => {
const queue = new PriorityQueue<PriorityNode>(priorityNodeAscending);
_.times(1000, time =>
queue.enqueue({
label: time, // used as the tiebreaker when priority is tied
priority: Math.floor(Math.random() * 3), // only 3 tiers of priority
}),
);

const array: Array<PriorityNode> = [];
while (queue.peek() !== undefined) {
array.push(queue.dequeue());
}

expect(
array.every((curr, i) => {
if (!i) return true;

const prev = array[i - 1];

// false when prev priority is larger
if (prev.priority > curr.priority) return false;
if (prev.priority < curr.priority) return true;

// false when prev timestampe is larger (unstable sort)
if (prev.label > curr.label) return false;
return true;
}),
).toBeTruthy();
});
});
});
});

0 comments on commit f40d4bc

Please sign in to comment.