.NET 6 優先佇列 PriorityQueue 實現分析

SpringLeee發表於2021-12-24

在最近釋出的 .NET 6 中,包含了一個新的資料結構,優先佇列 PriorityQueue, 實際上這個資料結構在隔壁 Java中已經存在了很多年了, 那優先佇列是怎麼實現的呢? 讓我們來一探究竟吧。

時間複雜度

因為接下來會分析時間複雜度, 這裡先貼一張幾種時間複雜度的對比圖,從低階到高階有:O(1)、O(logn)、O(n)、O(nlogn)、O(n2 )。

什麼是優先佇列

首先,佇列大家都知道, 是一個非常基礎的資料結構, 它的特點是先進先出(FIFO)。

而優先佇列卻不一定是先進先出,因為每個元素都有一個權重值, 代表著元素出隊的優先順序。

佇列可以用陣列和連結串列實現, 簡單、高效, 這樣入隊和出隊的時間複雜度都是 O(1)。

優先佇列能不能使用上面的方法呢? 也可以, 但是每次新元素入隊後, 需要和佇列內的元素進行遍歷和大小對比, 然後插入到合適的位置, 讓整個序列保持從大到小或者從小到大,這樣入隊的時間複雜度變成 O(n), 而出隊複雜度不變, 還是 O(1)。O(n) 代表入隊的時間是線性增長的, 效率較低, 有沒有更高效的方法呢?

堆 Heap

堆這種資料結構的應用場景非常多,最經典的莫過於堆排序了, 堆排序是一種原地的、時間複雜度為 O(nlog n) 的排序演算法,另外,堆也很適合用來做優先佇列。

堆和樹的結構其實是相似的, 堆有二叉堆, d-ary 堆, 2-3 堆, 斐波那契堆等等, 堆有一個特點就是每個父節點都大於等於它的兒子節點, 這種是大頂堆, 或者每個父節點都小於等於它的兒子節點, 這種是小頂堆,另外堆的兒子不分左右, 其中 java 中的 PriorityQueue 就是用二叉小頂堆實現的。

上面就是二叉堆, 而 .NET 6 中的 PriorityQueue 是由 d-ary 堆實現的, 而 d 表示父節點有幾個兒子節點, .NET 6 中指定這個值為4,並且是小頂堆,也就是 “四叉小頂堆"。

四叉堆比二叉堆更快,可以參考下面連結的論文

A Back-to-Basics Empirical Study of Priority Queues

那麼如何在程式碼中實現呢?其實可以用陣列儲存堆, 我們可以通過”廣度優先遍歷“ 的方法, 把堆的節點對映到一個陣列中,如下

另外,堆和陣列之間還有下面的關係

  1. 堆的頂點就是陣列的第一個元素,也是最小的元素。

  2. 通過子節點的下標,就可以通過公式計算出父節點的下標, 公式為

    P = (C - 1) / 4

    其中 P = 父節點的下標, C = 子節點的下標

現在優先佇列的資料結構確定了, 接下來看元素的入隊和出隊。

入隊 Enqueue

使用堆來實現優先佇列,入隊操作2步完成, 非常簡單!

  1. 新增新節點到末尾

  2. 通過上面的公式 P = (C - 1) / 4, 新的子節點和父節點進行大小對比,如果子節點比較小,那麼就和父節點交換,重複這個過程,直到子節點大於或等於父節點,或者子節點變成堆頂,堆化完成, 這個交換過程是從下往上的, 入隊的時間複雜度是 O(log n)。

出隊 Dequeue

出隊,就是每次取佇列內最小的元素,基小頂堆結構,其實只需要取堆頂的元素即可,對應陣列的第1個元素 array[0]。

你會發現,當取出堆頂元素以後,小頂堆的頂已經空了, 為了保持堆的結構,我們需要重新堆化。

和上面的入隊 Enqueue 的邏輯有異曲同工之妙, 我們可以取堆的最後一個元素,把它放到堆頂, 然後父節點去和4個兒子節點比大小,如果比兒子節點大,就交換, 重複這個過程,直到父節點比4個兒子節點都大, 或者到達堆的最後一層,堆化完成,這個交換過程是從上往下的,出隊的時間複雜度同樣是 O(log n)。

另外,如果多個兒子節點都比父節點小,那父節點和最小的子節點交換。

擴容和收縮機制

優先佇列是用陣列實現的四叉小頂堆, 那麼就存在陣列的擴容和收縮的情況

擴容:最小為4,陣列滿的時候會擴大為當前容量的2倍。

收縮:陣列不會自動收縮,不過可以手動呼叫 TrimExcess() 方法, 當空餘的空間大於10% 的時候, 陣列的長度會收縮到當前佇列元素的數量。

總結

本文主要介紹了 .NET 6 新增的資料結構優先佇列,感興趣的也可以看一下 PriorityQueue 的原始碼, 其實就是基於堆這種結構實現的,也展示了入隊和出隊的堆結構的變化過程,另外需要注意的是,堆這種結構不是穩定的,因為在排序的過程,存在將堆的最後一個節點跟堆頂節點互換的操作,所以以相同優先順序入隊的元素並不能保證以相同的順序出隊。

參考

System/Collections/Generic/PriorityQueue.cs

https://github.com/dotnet/runtime/issues/14032

資料結構與演算法之美

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

A Back-to-Basics Empirical Study of Priority Queues

.NET 6 優先佇列 PriorityQueue 實現分析

相關文章