簡體   English   中英

在 Javascript 中實現優先隊列的有效方法?

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

對於每個條目,優先級隊列都有一個優先級值和數據。

因此,當向隊列中添加一個新元素時,如果它具有比集合中已有元素更高的優先級值,它就會冒泡到表面。

當調用 pop 時,我們獲得具有最高優先級的元素的數據。

Javascript 中這種優先級隊列的有效實現是什么?

有一個名為 PriorityQueue 的新 object 是否有意義,創建兩個方法(push 和 pop)接受兩個參數(數據,優先級)? 作為一名編碼員,這對我來說很有意義,但我不確定在允許操縱元素順序的軟肋中使用哪種數據結構。 或者我們可以將它全部存儲在一個數組中並每次遍歷該數組以獲取具有最大優先級的元素嗎?

這樣做的好方法是什么?

下面是我認為是PriorityQueue的真正高效版本,它使用基於數組的二元堆(其中根位於索引0處,索引i處的節點的子節點位於索引2i + 12i + 2 , 分別)。

此實現包括經典的優先級隊列方法,如pushpeekpopsize ,以及方便的方法isEmptyreplace (后者是一個更有效的替代pop后緊跟push )。 值不是存儲為[value, priority]對,而是簡單地存儲為value s; 這允許自動對可以使用>運算符進行本地比較的類型進行優先級排序。 但是,傳遞給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%}

您應該使用標准庫,例如 Closure Library ( 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

我對現有優先級隊列實現的效率不滿意,所以我決定自己做:

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

我在這里提供了我使用的實現。 我做了以下決定:

  • 我經常發現我需要將一些負載與堆排序的值一起存儲。 所以我選擇讓堆由數組組成,其中數組的第一個元素必須是用於堆順序的值。 這些數組中的任何其他元素將只是未被檢查的有效載荷。 確實,一個沒有空間容納有效載荷的純整數數組可以實現更快的實現,但在實踐中,我發現自己創建了一個 Map 以將這些值與附加數據(有效載荷)聯系起來。 這種 Map 的管理(也處理重復值!)破壞了您從這種只有整數的數組中獲得的好處。
  • 使用用戶定義的比較器函數會帶來性能成本,所以我決定不使用它。 相反,使用比較運算符( <> 、 ...)比較這些值。 這適用於數字、大整數、字符串和日期實例。 如果值是這樣排序的對象,則應覆蓋它們的valueOf以保證所需的排序。 或者,這樣的對象應該作為有效載荷提供,並且真正定義順序的對象的屬性應該作為值(在第一個數組位置)給出。
  • 擴展 Array 類也會在一定程度上降低性能,所以我選擇提供以堆(一個 Array 實例)作為第一個參數的實用函數。 這類似於 Python 中heapq模塊的工作方式,並給人一種“輕松”的感覺:您可以直接使用自己的數組。 沒有new ,沒有繼承,只有作用於數組的普通函數。
  • 通常的向上篩選和向下篩選操作不應該在父子節點之間進行重復交換,而只向一個方向復制樹值,直到找到最后的插入點,然后才應將給定的值存儲在該點.
  • 它應該包含一個heapify函數,以便可以將已經填充的數組重新排序到堆中。 它應該在線性時間內運行,這樣比從空堆開始然后將每個節點推送到它的效率更高。

下面是函數集合,帶有注釋,最后是一個簡單的演示:

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

有關更現實的示例,請參閱Dijkstra 最短路徑算法的實現

這是同一個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}};

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM