d-ary heap實現一個快速的優先級隊列(C#)

d-ary heap簡介:

d-ary heap 是泛化版本的binary heap(d=2),d-ary heap每一個非葉子節點最多有d個孩子結點。node

d-ary heap擁有以下屬性:git

  1. 相似complete binary tree,除了樹的最後一層,其它層所有填滿結點,且增長結點方式由左至右。
  2. 相似binary heap,它也分兩類最大堆和最小堆。

下面給出一個3-ary heap示例:github

3-ary max heap - root node is maximum of all nodes
             10
       /      |     \
      7       9      8
  /  |  \    /
 4   6   5  7


3-ary min heap -root node is minimum of all nodes
             10
         /    |    \
       12     11    13
     / | \
    14 15 18 

具備n個節點的徹底d叉樹的高度由logdn給出。算法

d-ary heap的應用:

d-ary heap經常使用於進一步實現優先級隊列,d-ary heap實現的優先級隊列比用binary heap實現的優先隊列在添加新元素的方面效率更高。binary heap:O(log2n) vs d-ary heap: O(logkn) ,當d > 2 時,logkn < log2n可是d-ary heap實現的優先級隊列缺點是提取優先級隊列首個元素比binary heap實現的優先隊列須要消耗更多性能。binary heap:O(log2n) vs d-ary heap:O((d-1)logdn),當 d > 2 時,(d-1)logdn > log2n ,經過對數換底公式可證結果看起來喜憂參半,那麼什麼狀況下特別適合使用d-ary heap呢?答案就是遊戲中常見的尋路算法。就以A*和Dijkstra algorithm舉例。二者通常都須要一個優先級隊列(有某些A*算法不適用優先級隊列,好比迭代加深A*),而這些算法在取出隊列首個元素時,每每要向隊列中添加更多的臨近結點。也就是添加結點次數遠遠大於提取次數。那麼正好,d-ary heap能夠取長補短。另外,d-ary heap比binary heap 對緩存更加友好,更多的子結點相鄰在一塊兒。故在實際運行效率每每會更好一些。數組

d-ary heap及優先級隊列的實現:

咱們用數組實現d-ary heap,數組以0爲起始,能夠獲得以下規律:緩存

  • 若該結點爲非根結點,那麼使用該結點的索引i能夠取得其的父結點索引,父結點爲(i-1)/d;
  • 若該結點的索引爲i,那麼它的孩子結點索引分別爲(d*i)+1 , (d*i)+2 …. (d*i)+d;
  • 若heap大小爲n,最後一個非葉子結點的索引爲(n-1)/d;(注:本文給出的實現並無使用該規則)

構建d-ary heap堆:本文給出的實現側重於進一步實現優先級隊列,並採用最小堆(方便適配尋路算法)。因此把一個輸入數組堆化,並非核心操做,爲了方便撰寫代碼以及增強可讀性,構建堆算法採用從根結點至下方式,而不是從最後一個非葉子結點向上的方式。優勢顯而易見,代碼清晰,不須要使用遞歸且不須要大量if else語句來尋找最小的孩子結點。只要孩子結點的值小於其父節點將其交換便可。缺點顯而易見,交換次數增長從而下降效率。數據結構

public void BuildHeap() 
{
for (int i = 1; i < numberOfItems; i++)
      {
int bubbleIndex = i; ar node = heap[i]; while (bubbleIndex != 0)
       {
int parentIndex = (bubbleIndex-1) / D; if (node.CompareTo(heap[parentIndex]) < 0)
         { heap[bubbleIndex]
= heap[parentIndex]; heap[parentIndex] = node; bubbleIndex = parentIndex; } else
          {
break; } } }
}

Push:優先級隊列中添加新的元素,若添加node爲空,拋出異常,若空間不足,則擴展空間。最後調用內部函數DecreaseKey加入新的結點到d-ary heap。app

public void Push(T node) 
{
if (node == null) throw new System.ArgumentNullException("node"); if (numberOfItems == heap.Length)
   { Expand(); } DecreaseKey(node, (
ushort)numberOfItems); numberOfItems++; }

DecreaseKey:傳入的index爲當前隊列中現有元素的數量。這個函數是私有的,由於對於優先級隊列來講並不須要提供改接口。這裏咱們使用了一個優化技巧,暫不保存待加入的結點到數組,直到咱們找到了它在數組中的合適位置,這樣能夠節省沒必要要的交換。函數

private void DecreaseKey (T node, ushort index)
{
if(index < numberOfItems) { if(node.CompareTo(heap[index]) > 0 ) { throw new System.Exception("New node key greater than orginal key"); } } int bubbleIndex = index; while (bubbleIndex != 0)
       {
// Parent node of the bubble node int parentIndex = (bubbleIndex-1) / D; if (node.CompareTo(heap[parentIndex]) < 0 ) { // Swap the bubble node and parent node // (we don't really need to store the bubble node until we know the final index though // so we do that after the loop instead) heap[bubbleIndex] = heap[parentIndex]; bubbleIndex = parentIndex; } else { break; } } heap[bubbleIndex] = node; }

 

Pop:彈出優先級隊列top元素,調用內部函數ExtractMin。oop

public T Pop () 
{
return ExtractMin(); }

ExtractMin返回當前root node,更新numberOfItems,從新堆化。把最後一個葉子結點移動到root node,結點依照規則上浮。這裏使用了一樣的優化技巧。沒必要把最後一個葉子結點保存到數組0的位置,等到肯定其最終位置再把它存入數組。這樣作的好處節省交換次數。

private T ExtractMin()
{
            T returnItem = heap[0];

            numberOfItems--;
            if (numberOfItems == 0) return returnItem;

            // Last item in the heap array
            var swapItem = heap[numberOfItems];
        
            int swapIndex = 0, parent;

            
            while (true) {
                parent = swapIndex;
                var curSwapItem = swapItem;
                int pd = parent * D + 1;

                // If this holds, then the indices used
                // below are guaranteed to not throw an index out of bounds
                // exception since we choose the size of the array in that way
                if (pd <= numberOfItems) 
         {
for(int i = 0;i<D-1;i++) { if (pd+i < numberOfItems && (heap[pd+i].CompareTo(curSwapItem) < 0)) { curSwapItem = heap[pd+i]; swapIndex = pd+i; } } if (pd+D-1 < numberOfItems && (heap[pd+D-1].CompareTo(curSwapItem) < 0))
{ swapIndex
= pd+D-1; } } // One if the parent's children are smaller or equal, swap them // (actually we are just pretenting we swapped them, we hold the swapData // in local variable and only assign it once we know the final index) if (parent != swapIndex) { heap[parent] = heap[swapIndex]; } else { break; } } // Assign element to the final position heap[swapIndex] = swapItem; // For debugging Validate (); return returnItem;
}

時間複雜度分析:

  • 對於用d ary heap實現的優先級隊列,若隊列擁有n個元素,其對應堆的高度最大爲logdn ,添加新元素時間複雜度爲O(logdn)
  • 對於用d ary heap實現的優先級隊列,若隊列擁有n個元素,其對應堆的高度最大爲logdn,要在d個孩子結點當中選取最小或最大結點,層層不斷上浮。故刪除隊首元素時間複雜度爲(d-1)logdn
  • 對於把數組轉化爲d ary heap,採用從最後一個非葉子結點向上的方式,其時間複雜度爲O(n),分析思路和binary heap同樣。舉例說明,對於擁有n個結點的4 ary heap,高度爲1子樹的有(3/4)n,高度爲2的子樹有(3/16)n... 處理高度爲1的子樹須要O(1),處理高度爲2的子樹須要O(2)... 累加公式爲  $\sum_{k=1}^{log_{4}^{n}}{\frac{3}{4^{k}}}nk$ ,根據比值收斂法可知這個無窮級數是收斂的,故複雜度仍爲O(n)。那麼對於本文給出的自頂向下的方式,其複雜度又如何呢?答案爲O($dlog_{d}^{n}n$),具體的運算過程(詳見下一條),理論上時間複雜度要高於採用從最後一個非葉子結點向上的方式。但二者實際效率相差多少需進行實際測試。
  • 本文的buildheap算法,第i層的結點至多須要比較和交換i次,且第i層結點數di,由此可得時間統計範式爲$\sum_{i=1}^{log_{d}^{n}}{d^{i}}i$,以d=4爲例 $\sum_{i=1}^{log_{4}^{n}}{4^{i}}i$。須要求前i項和Si關於i的表達式,Si= 1*4 +2*42+3*43+.....+ i*4i ,那麼4Si=1*42+2*43+......+i*4i+1,用4Si-Si進行錯位相減,得知3Si=i*4i+1 - (4+42+......+4i) 。痛快,後者是一個等比數列。這樣整個式子最後表達爲$Si=\frac{4}{9}+\frac{1}{3}(i-\frac{1}{3})4^{i+1}$,咱們知道i值爲logdn,代入可得O($dlog_{d}^{n}n$)。

總結:

經過使用System.Diagnostics.Stopwatch 進行屢次測試,發現d=4 時,push和pop的性能都不錯,d=4不少狀況下Push都比d=2的狀況要好一些。push能夠肯定性能確實有所提升,pop不能肯定究竟是好了仍是壞了,實驗結果互有勝負。說到底System.Diagnostics.Stopwatch並非精確測試,裏面還有.net的噪音。

附錄:

優先級隊列完整程序

Q&A:

Q:

個人尋路算法想要使用C++或Java標準庫自帶的PriorityQueue,二者都沒有提供DecreaseKey函數,帶來的問題是我沒法更新隊列裏元素key,沒有辦法進行邊放鬆,如何處理?

A:

筆者文章DecreaseKey也是私有的,沒有提供給PriorityQueue的使用者。爲何不提供呢?由於即使提供了尋路算法如何給出DecreaseKey所需的index呢?咱們知道須要更新的元素在優先級隊列中,可是index並不知道,要獲取index就須要進行搜索(或者使用額外數據結構輔助)。使用額外的數據結構輔助肯定index必然佔用更多內存空間,使用搜索肯定index必然消耗更多時間尤爲是當隊列中元素不少時。訣竅根本不改變它。而是將該節點的  "新建副本 " (具備新的更好的成本) 添加到優先級隊列中。因爲成本較低, 該節點的新副本將在隊列中的原始副本以前提取, 所以將在前面進行處理。後面遇到的重複結點直接忽略便可,而且不少狀況還沒等處處理重複結點時咱們已經找到路徑了。咱們所額外負擔的就是優先級隊列中存在一些多餘對象。這種負擔很是小,並且實現起來簡便。

 參考文獻:

https://www.geeksforgeeks.org/k-ary-heap/

http://en.wikipedia.org/wiki/Binary_heap

https://en.wikipedia.org/wiki/D-ary_heap

歡迎評論區交流,批評,指正~

原創文章,轉載請標明出處,謝謝~

相關文章
相關標籤/搜索