Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add priority queue data structure (#66)
* 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
Showing
2 changed files
with
258 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
}); | ||
}); | ||
}); | ||
}); |