diff --git a/src/data-structures/PriorityQueue.ts b/src/data-structures/PriorityQueue.ts new file mode 100644 index 0000000..7d439a5 --- /dev/null +++ b/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 { + (a: T, b: T): number; +} + +class Timestamped { + 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 { + private _heap: Array> = []; + private _comparer: Comparer; + private _nextTimestamp: number = 0; + + constructor(comparer: Comparer) { + 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) { + 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; diff --git a/test/data-structures/PriorityQueue.test.ts b/test/data-structures/PriorityQueue.test.ts new file mode 100644 index 0000000..3b1f9b4 --- /dev/null +++ b/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(numberAscending); + expect(queue).toBeTruthy(); + }); + }); + + describe('sorting tests', () => { + test('number should be sorted ascending', () => { + const queue = new PriorityQueue(numberAscending); + queue.enqueue(1, 5, 3, 2, 5, 4, 0, 6); + + const array: Array = []; + 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(numberDescending); + queue.enqueue(1, 5, 3, 2, 5, 4, 0, 6); + + const array: Array = []; + 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(numberAscending); + _.times(1000, () => queue.enqueue(Math.random() * 100)); + + const array: Array = []; + 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(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 = []; + 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(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 = []; + 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(); + }); + }); + }); +});