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

在最近發佈的 .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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章