d-ary heap 是泛化版本的binary heap(d=2),d-ary heap每一個非葉子節點最多有d個孩子結點。node
d-ary heap擁有以下屬性:git
下面給出一個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實現的優先級隊列比用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,數組以0爲起始,能夠獲得以下規律:緩存
構建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;
}
時間複雜度分析:
經過使用System.Diagnostics.Stopwatch 進行屢次測試,發現d=4 時,push和pop的性能都不錯,d=4不少狀況下Push都比d=2的狀況要好一些。push能夠肯定性能確實有所提升,pop不能肯定究竟是好了仍是壞了,實驗結果互有勝負。說到底System.Diagnostics.Stopwatch並非精確測試,裏面還有.net的噪音。
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
歡迎評論區交流,批評,指正~
原創文章,轉載請標明出處,謝謝~