Efficient way to implement Priority Queue in Javascript?
Below is what I believe to be a truly efficient version of a PriorityQueue
which uses an array-based binary heap (where the root is at index 0
, and the children of a node at index i
are at indices 2i + 1
and 2i + 2
, respectively).
This implementation includes the classical priority queue methods like push
, peek
, pop
, and size
, as well as convenience methods isEmpty
and replace
(the latter being a more efficient substitute for a pop
followed immediately by a push
). Values are stored not as [value, priority]
pairs, but simply as value
s; this allows for automatic prioritization of types that can be natively compared using the >
operator. A custom comparator function passed to the PriorityQueue
constructor can be used to emulate the behavior of pairwise semantics, however, as shown in the example below.
Heap-based Implementation:
const top = 0;
const parent = i => ((i + 1) >>> 1) - 1;
const left = i => (i << 1) + 1;
const right = i => (i + 1) << 1;
class PriorityQueue {
constructor(comparator = (a, b) => a > b) {
this._heap = [];
this._comparator = comparator;
}
size() {
return this._heap.length;
}
isEmpty() {
return this.size() == 0;
}
peek() {
return this._heap[top];
}
push(...values) {
values.forEach(value => {
this._heap.push(value);
this._siftUp();
});
return this.size();
}
pop() {
const poppedValue = this.peek();
const bottom = this.size() - 1;
if (bottom > top) {
this._swap(top, bottom);
}
this._heap.pop();
this._siftDown();
return poppedValue;
}
replace(value) {
const replacedValue = this.peek();
this._heap[top] = value;
this._siftDown();
return replacedValue;
}
_greater(i, j) {
return this._comparator(this._heap[i], this._heap[j]);
}
_swap(i, j) {
[this._heap[i], this._heap[j]] = [this._heap[j], this._heap[i]];
}
_siftUp() {
let node = this.size() - 1;
while (node > top && this._greater(node, parent(node))) {
this._swap(node, parent(node));
node = parent(node);
}
}
_siftDown() {
let node = top;
while (
(left(node) < this.size() && this._greater(left(node), node)) ||
(right(node) < this.size() && this._greater(right(node), node))
) {
let maxChild = (right(node) < this.size() && this._greater(right(node), left(node))) ? right(node) : left(node);
this._swap(node, maxChild);
node = maxChild;
}
}
}
Example:
{const top=0,parent=c=>(c+1>>>1)-1,left=c=>(c<<1)+1,right=c=>c+1<<1;class PriorityQueue{constructor(c=(d,e)=>d>e){this._heap=[],this._comparator=c}size(){return this._heap.length}isEmpty(){return 0==this.size()}peek(){return this._heap[top]}push(...c){return c.forEach(d=>{this._heap.push(d),this._siftUp()}),this.size()}pop(){const c=this.peek(),d=this.size()-1;return d>top&&this._swap(top,d),this._heap.pop(),this._siftDown(),c}replace(c){const d=this.peek();return this._heap[top]=c,this._siftDown(),d}_greater(c,d){return this._comparator(this._heap[c],this._heap[d])}_swap(c,d){[this._heap[c],this._heap[d]]=[this._heap[d],this._heap[c]]}_siftUp(){for(let c=this.size()-1;c>top&&this._greater(c,parent(c));)this._swap(c,parent(c)),c=parent(c)}_siftDown(){for(let d,c=top;left(c)<this.size()&&this._greater(left(c),c)||right(c)<this.size()&&this._greater(right(c),c);)d=right(c)<this.size()&&this._greater(right(c),left(c))?right(c):left(c),this._swap(c,d),c=d}}window.PriorityQueue=PriorityQueue}
// Default comparison semantics
const queue = new PriorityQueue();
queue.push(10, 20, 30, 40, 50);
console.log('Top:', queue.peek()); //=> 50
console.log('Size:', queue.size()); //=> 5
console.log('Contents:');
while (!queue.isEmpty()) {
console.log(queue.pop()); //=> 40, 30, 20, 10
}
// Pairwise comparison semantics
const pairwiseQueue = new PriorityQueue((a, b) => a[1] > b[1]);
pairwiseQueue.push(['low', 0], ['medium', 5], ['high', 10]);
console.log('\nContents:');
while (!pairwiseQueue.isEmpty()) {
console.log(pairwiseQueue.pop()[0]); //=> 'high', 'medium', 'low'
}
.as-console-wrapper{min-height:100%}
You should use standard libraries like e.g. the Closure Library (goog.structs.PriorityQueue
):
https://google.github.io/closure-library/api/goog.structs.PriorityQueue.html
By clicking at the source code, you will know it is actually linking to goog.structs.Heap
which you can follow:
https://github.com/google/closure-library/blob/master/closure/goog/structs/heap.js
I provide here the implementation I use. I made the following decisions:
- I often find that I need to store some payload together with the values by which the heap will be ordered. So I opted to have the heap consist of arrays, where the first element of the array must be the value to be used for the heap order. Any other elements in these arrays will just be payload that is not inspected. True, a pure integer array, without room for payload, would make a faster implementation possible, but in practice I then find myself creating a Map to link those values with additional data (the payload). The administration of such a Map (also dealing with duplicate values!) destroys the benefits you get from such an integer-only array.
- Using a user-defined comparator function comes with a performance cost, so I decided not to work with that. Instead the values are compared using comparison operators (
<
,>
, ...). This works fine for numbers, bigints, strings, and Date instances. In case the values are objects that would not order well like that, theirvalueOf
should be overridden to guarantee the desired ordering. Or, such objects should be provided as payload, and the object's property that really defines the order, should be given as the value (in first array position). - Extending the Array class also turned out to degrade the performance somewhat, so I opted to provide utility functions that take the heap (an Array instance) as first argument. This resembles how in Python the
heapq
module works and gives a "light" feeling to it: You work directly with your own array. Nonew
, no inheritance, just plain functions acting on your array. - The usual sift-up and sift-down operations should not perform repeated swaps between parent and child, but only copy the tree values in one direction until the final insertion spot has been found, and only then the given value should be stored in that spot.
- It should include a heapify function so an already populated array can be reordered into a heap. It should run in linear time so that it is more efficient than if you would start with an empty heap and then push each node unto it.
Here follows that collection of functions, with comments, and a simple demo at the end:
/* MinHeap:
* A collection of functions that operate on an array
* of [key,...data] elements (nodes).
*/
const MinHeap = {
/* siftDown:
* The node at the given index of the given heap is sifted down in
* its subtree until it does not have a child with a lesser value.
*/
siftDown(arr, i=0, value=arr[i]) {
if (i < arr.length) {
let key = value[0]; // Grab the value to compare with
while (true) {
// Choose the child with the least value
let j = i*2+1;
if (j+1 < arr.length && arr[j][0] > arr[j+1][0]) j++;
// If no child has lesser value, then we've found the spot!
if (j >= arr.length || key <= arr[j][0]) break;
// Copy the selected child node one level up...
arr[i] = arr[j];
// ...and consider the child slot for putting our sifted node
i = j;
}
arr[i] = value; // Place the sifted node at the found spot
}
},
/* heapify:
* The given array is reordered in-place so that it becomes a valid heap.
* Elements in the given array must have a [0] property (e.g. arrays).
* That [0] value serves as the key to establish the heap order. The rest
* of such an element is just payload. It also returns the heap.
*/
heapify(arr) {
// Establish heap with an incremental, bottom-up process
for (let i = arr.length>>1; i--; ) this.siftDown(arr, i);
return arr;
},
/* pop:
* Extracts the root of the given heap, and returns it (the subarray).
* Returns undefined if the heap is empty
*/
pop(arr) {
// Pop the last leaf from the given heap, and exchange it with its root
return this.exchange(arr, arr.pop()); // Returns the old root
},
/* exchange:
* Replaces the root node of the given heap with the given node, and
* returns the previous root. Returns the given node if the heap is empty.
* This is similar to a call of pop and push, but is more efficient.
*/
exchange(arr, value) {
if (!arr.length) return value;
// Get the root node, so to return it later
let oldValue = arr[0];
// Inject the replacing node using the sift-down process
this.siftDown(arr, 0, value);
return oldValue;
},
/* push:
* Inserts the given node into the given heap. It returns the heap.
*/
push(arr, value) {
let key = value[0],
// First assume the insertion spot is at the very end (as a leaf)
i = arr.length,
j;
// Then follow the path to the root, moving values down for as long
// as they are greater than the value to be inserted
while ((j = (i-1)>>1) >= 0 && key < arr[j][0]) {
arr[i] = arr[j];
i = j;
}
// Found the insertion spot
arr[i] = value;
return arr;
}
};
// Simple Demo:
let heap = [];
MinHeap.push(heap, [26, "Helen"]);
MinHeap.push(heap, [15, "Mike"]);
MinHeap.push(heap, [20, "Samantha"]);
MinHeap.push(heap, [21, "Timothy"]);
MinHeap.push(heap, [19, "Patricia"]);
let [age, name] = MinHeap.pop(heap);
console.log(`${name} is the youngest with ${age} years`);
([age, name] = MinHeap.pop(heap));
console.log(`Next is ${name} with ${age} years`);
For a more realistic example, see the implementation of Dijkstra's shortest path algorithm.
Here is the same MinHeap
collection, but minified, together with its MaxHeap
mirror:
const MinHeap={siftDown(h,i=0,v=h[i]){if(i<h.length){let k=v[0];while(1){let j=i*2+1;if(j+1<h.length&&h[j][0]>h[j+1][0])j++;if(j>=h.length||k<=h[j][0])break;h[i]=h[j];i=j;}h[i]=v}},heapify(h){for(let i=h.length>>1;i--;)this.siftDown(h,i);return h},pop(h){return this.exchange(h,h.pop())},exchange(h,v){if(!h.length)return v;let w=h[0];this.siftDown(h,0,v);return w},push(h,v){let k=v[0],i=h.length,j;while((j=(i-1)>>1)>=0&&k<h[j][0]){h[i]=h[j];i=j}h[i]=v;return h}};
const MaxHeap={siftDown(h,i=0,v=h[i]){if(i<h.length){let k=v[0];while(1){let j=i*2+1;if(j+1<h.length&&h[j][0]<h[j+1][0])j++;if(j>=h.length||k>=h[j][0])break;h[i]=h[j];i=j;}h[i]=v}},heapify(h){for(let i=h.length>>1;i--;)this.siftDown(h,i);return h},pop(h){return this.exchange(h,h.pop())},exchange(h,v){if(!h.length)return v;let w=h[0];this.siftDown(h,0,v);return w},push(h,v){let k=v[0],i=h.length,j;while((j=(i-1)>>1)>=0&&k>h[j][0]){h[i]=h[j];i=j}h[i]=v;return h}};
I was not satisfied with the efficiency of existing priority queue implementations, so I decided to make my own:
https://github.com/luciopaiva/heapify
npm i heapify
This will run faster than any other publicly known implementation due to the use of typed arrays.
Works on both client and server ends, code base with 100% test coverage, tiny library (~100 LoC). Also, the interface is really simple. Here's some code:
import Heapify from "heapify";
const queue = new Heapify();
queue.push(1, 10); // insert item with key=1, priority=10
queue.push(2, 5); // insert item with key=2, priority=5
queue.pop(); // 2
queue.peek(); // 1
queue.peekPriority(); // 10