在Javascript?中实现优先级队列的有效方法

oaxa6hgo  于 2022-12-02  发布在  Java
关注(0)|答案(5)|浏览(176)

优先级队列具有每个条目的优先级值和数据。
因此,当将新元素添加到队列中时,如果它的优先级值高于集合中已有的元素,则它会冒泡到图面。
当一个调用pop时,我们获取具有最高优先级的元素的数据。
在Javascript中,这种优先级队列的有效实现是什么?
创建一个名为PriorityQueue的新对象、创建两个方法是否有意义(推入和弹出),它使用两个参数(数据,优先级)?作为一个程序员,这对我来说很有意义,但是我不确定在底层使用哪种数据结构来允许对元素的排序进行操作。或者我们可以将所有元素都存储在一个数组中,然后每次遍历数组以获取具有最大优先级的元素?
有什么好办法可以做到这一点?

7jmck4yq

7jmck4yq1#

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%}
cgh8pdjw

cgh8pdjw2#

您应该使用标准库,例如Closure库(goog.structs.PriorityQueue):
https://google.github.io/closure-library/api/goog.structs.PriorityQueue.html
通过点击源代码,你会知道它实际上是链接到goog.structs.Heap,你可以跟随:
https://github.com/google/closure-library/blob/master/closure/goog/structs/heap.js

4c8rllxm

4c8rllxm3#

我对现有优先级队列实现的效率并不满意,所以我决定自己创建一个:
https://github.com/luciopaiva/heapify

npm i heapify

由于使用了类型化数组,这将比任何其他公知的实现运行得更快。
在客户端和服务器端都能工作,代码库具有100%的测试覆盖率,小库(~100 LoC)。而且,界面非常简单。下面是一些代码:

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
holgip5t

holgip5t4#

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}};
odopli94

odopli945#

从@gyre的回答中得到了一些灵感,用TypeScript写了一个简约的版本,大约缩小了550字节。

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;
    }
  }
}

用法:

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());
}

相关问题