【问题标题】:Efficient way to implement Priority Queue in Javascript?在Javascript中实现优先队列的有效方法?
【发布时间】:2017-03-21 06:00:04
【问题描述】:

优先级队列对于每个条目都有一个优先级值和数据。

因此,当向队列中添加一个新元素时,如果它的优先级值高于集合中已有的元素,它就会冒泡到表面。

当调用 pop 时,我们会获取优先级最高的元素的数据。

在 Javascript 中这种优先级队列的有效实现是什么?

拥有一个名为 PriorityQueue 的新对象是否有意义,创建两个方法(push 和 pop)接受两个参数(数据、优先级)?作为一名编码员,这对我来说很有意义,但我不确定在底层使用哪种数据结构来允许操纵元素的顺序。或者我们可以将它们全部存储在一个数组中,然后每次遍历数组以获取具有最大优先级的元素吗?

有什么好的方法可以做到这一点?

【问题讨论】:

    标签: javascript priority-queue


    【解决方案1】:

    下面是我认为真正有效的PriorityQueue 版本,它使用基于数组的二进制堆(其中根位于索引0,而位于索引i 的节点的子节点是分别位于索引 2i + 12i + 2)。

    此实现包括经典的优先级队列方法,如 pushpeekpopsize,以及便利方法 isEmptyreplace(后者是更有效的替代pop 紧接着是 push)。值不是以[value, priority] 对存储的,而是简单地以values 形式存储;这允许对可以使用 > 运算符进行本机比较的类型进行自动优先级排序。但是,传递给PriorityQueue 构造函数的自定义比较器函数可用于模拟成对语义的行为,如下例所示。

    基于堆的实现

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

    示例:

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

    【讨论】:

    • 酷,非常感谢!我想知道:在实现中使用 2 个单独的数组(一个用于数据,一个用于优先级,并且只有 data[i] 和 priority[i] 是相同的“对”)或使用二维 [][] 数组?因为,第一个选项只使用 2n 空间,但第二个可以使用最多 n^2
    • 我只使用一个数组。并且这两个选项都使用2n 空格,因为多维数组中的每一行只有两个元素(固定长度)。
    • @vaxquis 我知道这已经很长时间了,但我想我会让你知道我更新了我的答案以提高它的时间复杂度。虽然这些天我没有在 SO 上花费大量时间,但随着我对数据结构的了解更多,这个答案也让我感到厌烦,我终于腾出一些时间来修复它。 (我会删除它,但它已被接受。)如果您有任何进一步的建议,请告诉我......这次我会尽量及时。
    • @gyre 很高兴看到你进步;现在IMVHO,您的答案好多了。尽管如此,我还是想分享两个挑剔的东西:1. 将if (bottom &gt; top) this._swap(top, bottom); 之类的语句拆分为两行(还有 1TBS,我强烈建议在教程代码中使用它,但这完全是另一种野兽),2。使用临时变量来存储必须重复计算的值(尤其是,如果它们是循环中使用的函数调用的结果),例如你的left(node)right(node)this.size() 等等......仍然真的在 JS/ES 代码中的速度上有所不同。
    • @Noitidart 从教育的角度来看,“二进制堆”的维基百科页面应该很有帮助。对于 32 位整数,n &gt;&gt;&gt; 1 相当于除以 2 并取整(向下舍入),而 n &lt;&lt; 1 相当于乘以 2,并且仅在 2*n 上选择用于奇偶校验。
    【解决方案2】:

    您应该使用标准库,例如闭包库 (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

    【讨论】:

    【解决方案3】:

    我对现有优先级队列实现的效率不满意,所以我决定自己做:

    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
    

    【讨论】:

    【解决方案4】:

    我在这里提供我使用的实现。我做了以下决定:

    • 我经常发现我需要将一些有效负载与堆排序的值一起存储。所以我选择让堆由数组组成,其中数组的第一个元素必须是用于堆顺序的值。这些数组中的任何其他元素都只是未检查的有效负载。 诚然,一个没有有效负载空间的纯整数数组可以实现更快的实现,但实际上我发现自己创建了一个 Map 来将这些值与附加数据(有效负载)链接起来。此类 Map 的管理(也处理重复值!)破坏了您从此类仅整数数组中获得的好处。
    • 使用用户定义的比较器函数会带来性能成本,因此我决定不使用它。相反,使用比较运算符(&lt;&gt;、...)比较这些值。这适用于数字、bigint、字符串和 Date 实例。如果这些值是不能像那样排序的对象,则应覆盖它们的 valueOf 以保证所需的排序。或者,此类对象应作为有效负载提供,并且真正定义顺序的对象属性应作为值给出(在第一个数组位置)。
    • 扩展 Array 类也会在一定程度上降低性能,因此我选择提供将堆(一个 Array 实例)作为第一个参数的实用函数。这类似于 Python 中 heapq 模块的工作方式,并给它一种“轻松”的感觉:您直接使用自己的数组。没有new,没有继承,只是作用于您的数组的普通函数。
    • 通常的sift-up and sift-down operations 不应该在父子之间重复交换,而只能复制一个方向的树值直到已找到最终插入点,然后才应将给定值存储在该点中。
    • 它应该包含一个heapify 函数,以便可以将已填充的数组重新排序到堆中。它应该在linear time 中运行,这样比从空堆开始然后将每个节点推送到它更有效。

    下面是函数集合,带有 cmets,最后是一个简单的演示:

    /* 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`);

    更现实的例子见Dijkstra's shortest path algorithm的实现。

    这是相同的 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}};
    

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-11-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-26
    相关资源
    最近更新 更多