在最近發布的 .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
那么如何在代碼中實現呢?其實可以用數組存儲堆, 我們可以通過”廣度優先遍歷“ 的方法, 把堆的節點映射到一個數組中,如下
另外,堆和數組之間還有下面的關系
-
堆的頂點就是數組的第一個元素,也是最小的元素。
-
通過子節點的下標,就可以通過公式計算出父節點的下標, 公式為
P = (C - 1) / 4
其中 P = 父節點的下標, C = 子節點的下標
現在優先隊列的數據結構確定了, 接下來看元素的入隊和出隊。
入隊 Enqueue
使用堆來實現優先隊列,入隊操作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
