简体   繁体   English

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

[英]Efficient way to implement Priority Queue in Javascript?

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.当调用 pop 时,我们获得具有最高优先级的元素的数据。

What is an efficient implementation of such a priority queue in Javascript? Javascript 中这种优先级队列的有效实现是什么?

Does it make sense to have a new object called PriorityQueue, create two methods (push and pop) that take two params (data, priority)?有一个名为 PriorityQueue 的新 object 是否有意义,创建两个方法(push 和 pop)接受两个参数(数据,优先级)? 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?这样做的好方法是什么?

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).下面是我认为是PriorityQueue的真正高效版本,它使用基于数组的二元堆(其中根位于索引0处,索引i处的节点的子节点位于索引2i + 12i + 2 , 分别)。

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 ).此实现包括经典的优先级队列方法,如pushpeekpopsize ,以及方便的方法isEmptyreplace (后者是一个更有效的替代pop后紧跟push )。 Values are stored not as [value, priority] pairs, but simply as value s;值不是存储为[value, priority]对,而是简单地存储为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.但是,传递给PriorityQueue构造函数的自定义比较器函数可用于模拟成对语义的行为,如下例所示。

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 eg the Closure Library ( goog.structs.PriorityQueue ):您应该使用标准库,例如 Closure Library ( goog.structs.PriorityQueue ):

https://google.github.io/closure-library/api/goog.structs.PriorityQueue.html 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:通过点击源代码,你会知道它实际上是链接到goog.structs.Heap ,你可以遵循:

https://github.com/google/closure-library/blob/master/closure/goog/structs/heap.js https://github.com/google/closure-library/blob/master/closure/goog/structs/heap.js

I was not satisfied with the efficiency of existing priority queue implementations, so I decided to make my own:我对现有优先级队列实现的效率不满意,所以我决定自己做:

https://github.com/luciopaiva/heapify 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).适用于客户端和服务器端,具有 100% 测试覆盖率的代码库,小型库(~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

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).确实,一个没有空间容纳有效载荷的纯整数数组可以实现更快的实现,但在实践中,我发现自己创建了一个 Map 以将这些值与附加数据(有效载荷)联系起来。 The administration of such a Map (also dealing with duplicate values!) destroys the benefits you get from such an integer-only array.这种 Map 的管理(也处理重复值!)破坏了您从这种只有整数的数组中获得的好处。
  • 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.如果值是这样排序的对象,则应覆盖它们的valueOf以保证所需的排序。 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.扩展 Array 类也会在一定程度上降低性能,所以我选择提供以堆(一个 Array 实例)作为第一个参数的实用函数。 This resembles how in Python the heapq module works and gives a "light" feeling to it: You work directly with your own array.这类似于 Python 中heapq模块的工作方式,并给人一种“轻松”的感觉:您可以直接使用自己的数组。 No new , no inheritance, just plain functions acting on your array.没有new ,没有继承,只有作用于数组的普通函数。
  • 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.它应该包含一个heapify函数,以便可以将已经填充的数组重新排序到堆中。 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 (eg 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 .有关更现实的示例,请参阅Dijkstra 最短路径算法的实现

Here is the same MinHeap collection, but minified, together with its MaxHeap mirror:这是同一个MinHeap集合,但缩小了,连同它的MaxHeap镜像:

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

Took some inspiration from @gyre's answer and wrote a minimalistic version in TypeScript, that is about 550 bytes minified.从@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;
    }
  }
}

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM