Efficient way to implement Priority Queue in Javascript?
Asked Answered
W

5

70

Priority Queues have a priority value and data, for every entry.

Thus, when adding a new element to the queue, it bubbles up to the surface if it has a higher priority value than elements already in the collection.

When one calls pop, we get the data for the element with highest priority.

What is an efficient implementation of such a priority queue in Javascript?

Does it make sense to have a new object called PriorityQueue, create two methods (push and pop) that take two params (data, priority)? That much makes sense to me as a coder, but I'm uncertain of which data structure to use in the underbelly that will allow manipulation of the ordering of elements. Or can we just store it all in an array and walk through the array every time to grab the element with max priority?

What's a good way to do this?

Wiry answered 21/3, 2017 at 6:0 Comment(0)
C
80

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 values; 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%}
Cramped answered 21/3, 2017 at 6:20 Comment(13)
Cool, thanks a lot! I'm wondering: does it make more sense to use 2 separate arrays in the implementation (one for data and one for priority, and just have data[i] and priority[i] be the same "pair") or to use a 2d [][] array? Since, the first option uses only 2n space but the second can use up to n^2Wiry
I would just use a single array. And both options use 2n space because each row in the multidimensional array has only two elements (fixed length).Cramped
@vaxquis I know it's been a long time, but I thought I'd let you know that I updated my answer to improve its time complexity. While I don't spend a ton of time on SO these days, as I've learned more about data structures this answer has come to irk me as well and I finally set aside some time to fix it. (I would have deleted it, but it got accepted.) Please let me know if you've any further suggestions... I'll try to be a little more prompt this time around.Cramped
@Cramped glad to see you improve; your answer is a lot better now IMVHO. Still, two nitpicks I'd like to share: 1. split statements like if (bottom > top) this._swap(top, bottom); into two lines (there's also the 1TBS, which I'd strongly suggest to use especially in tutorial code, but that's another beast altogether), 2. use temp variables to store values that have to be calculated repeatedly (especially if they are the results of function calls used in loops), e.g. your left(node), right(node), this.size() etc... that still really makes a difference in speed in JS/ES code.Nudge
@Noitidart From an educational standpoint, the Wikipedia page for 'binary heap' should be very helpful. n >>> 1 for 32-bit integers is the equivalent of dividing by 2 and flooring (rounding down), while n << 1 is equivalent to multiplying by 2 and is only chosen over 2*n for parity.Cramped
@Cramped Thanks for this nice implementation. I tried using it and it took me a while to figure out why it wasn't working. Your comparator returns a boolean, whereas normally we expect a comparator to return negative, 0 or positive. This is also the signature that sort() expects: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…Drachma
I do not think this is a correct implementation. Try this: let minHeap = new PriorityQueue(); minHeap.push(1); minHeap.push(6); minHeap.push(0); minHeap.push(4); console.log(minHeap.pop()); console.log(minHeap.pop()); console.log(minHeap.pop())Bayonet
If you want a correct reference implementation that can easily adapt between minHeap and maxHeap, check this out: eloquentjavascript.net/1st_edition/appendix2.html. For a maxHeap, you simply call new BinaryHeap((x) => -x). For a minHeap, it's new BinaryHeap((x) => x).Bayonet
Doesn't this implementation have O(n) insert times? I think you could use a binary tree to achieve O(log(n)) insert times.Vasodilator
This should be added to the js stlBoat
This fails immediately when the entries are not already placed in an ascending order. Try: queue.push(50, 10, 20, 40, 30, 0);Trailer
I checked it out this code in Node 14.x and It works well. The insertion time is O(log(n)) since this algorithm uses an array abstraction of a binary three heap.Baucom
If you treat the top as index 1 (and leave index 0 unused) then then all the operations get a lot simpler: parent = i => i >>> 1;, left = i => i << 1, right = i => i << 1 + 1.Huge
O
16

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

Othello answered 21/8, 2017 at 7:17 Comment(3)
npm page: npmjs.com/package/google-closure-library (20 MiB)Unprintable
The first link doesn't work, I think this is the new one: github.com/google/closure-library/blob/master/closure/goog/…Oscillogram
Note that this library is now in maintenance mode: github.com/google/closure-library/issues/1214Royston
U
14

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
Unsung answered 10/2, 2020 at 12:32 Comment(3)
Can I use require instead of import for Node.js?Yuille
Hey @Nermin. Not yet, but please see github.com/luciopaiva/heapify/issues/41 for updates on this.Unsung
Is there no way to have capacity increase over time as new elements are added to the Queue? I also can't seem to find a way to have no capacity constraint. I'm wondering what the technical reason for this is?Sanderling
M
11

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, their valueOf 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. No new, 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}};
Mawson answered 6/3, 2021 at 22:5 Comment(5)
Very cool man! Could you add comments to each of the functions [siftDown, heapify, pop, push, exchange] for newbies to be able to understand your lovely code better? I feel that this is a great piece of reference code! =)Wiry
Typescript adaptation: gist.github.com/MaiaVictor/2b482faf71fbff189d2df72bab261c4bStaceystaci
How come you didn't turn this into a class? heap = MinHeap(); heap.push(prio, value) etcCanthus
@wihlke, because that creates more overhead and degrades the performance somewhat. I have benchmarked several variants, and this came out as better than a class-version. See also third bullet in my answer.Mawson
@Mawson I suspected as much. I guess it depends on the use case (graph size) whether to optimize for readability or performance. Nice implementation nonetheless. +1Canthus
D
3

Took some inspiration from @gyre's answer and wrote a minimalistic version in TypeScript, that is about 550 bytes minified.

type Comparator<T> = (valueA: T, valueB: T) => number;

const swap = (arr: unknown[], i: number, j: number) => {
  [arr[i], arr[j]] = [arr[j], arr[i]];
};

class PriorityQueue<T> {
  #heap;
  #isGreater;

  constructor(comparator: Comparator<T>);
  constructor(comparator: Comparator<T>, init: T[] = []) {
    this.#heap = init;
    this.#isGreater = (a: number, b: number) =>
      comparator(init[a] as T, init[b] as T) > 0;
  }

  get size(): number {
    return this.#heap.length;
  }

  peek(): T | undefined {
    return this.#heap[0];
  }

  add(value: T): void {
    this.#heap.push(value);
    this.#siftUp();
  }

  poll(): T | undefined;
  poll(
    heap = this.#heap,
    value = heap[0],
    length = heap.length
  ): T | undefined {
    if (length) {
      swap(heap, 0, length - 1);
    }

    heap.pop();
    this.#siftDown();

    return value;
  }

  #siftUp(): void;
  #siftUp(node = this.size - 1, parent = ((node + 1) >>> 1) - 1): void {
    for (
      ;
      node && this.#isGreater(node, parent);
      node = parent, parent = ((node + 1) >>> 1) - 1
    ) {
      swap(this.#heap, node, parent);
    }
  }

  #siftDown(): void;
  #siftDown(size = this.size, node = 0, isGreater = this.#isGreater): void {
    while (true) {
      const leftNode = (node << 1) + 1;
      const rightNode = leftNode + 1;

      if (
        (leftNode >= size || isGreater(node, leftNode)) &&
        (rightNode >= size || isGreater(node, rightNode))
      ) {
        break;
      }

      const maxChild =
        rightNode < size && isGreater(rightNode, leftNode)
          ? rightNode
          : leftNode;

      swap(this.#heap, node, maxChild);

      node = maxChild;
    }
  }
}

Usage:

const numberComparator: Comparator<number> = (numberA, numberB) => {
  return numberA - numberB;
};

const queue = new PriorityQueue(numberComparator);

queue.add(10);
queue.add(30);
queue.add(20);

while (queue.size) {
  console.log(queue.poll());
}
Diverge answered 1/12, 2022 at 23:5 Comment(1)
Instead of parent = ((node + 1) >>> 1) - 1), simpler parent = ((node - 1) >>> 1) saving one addition. OK, maybe this is no longer interesting enough in the JS world. But I am from the generation that also learnt C++ and to code efficiently ;-)Sponger

© 2022 - 2024 — McMap. All rights reserved.