實驗環境
這篇文章描述了優先佇列的一些效能測試以及測試結果。下文中,g++、msvc++ 和 local(下文實驗中使用這個生成器)代表三種不同的生成器:
g++
- CPU 頻率——cpu 主頻:2660.644 MHz
- 記憶體——記憶體總量:484412 kB
- 平臺——Linux-2.6.12-9-386-i686-with-debian-testing-unstable
- 編譯器——g++(GCC)4.0.2 20050808(prerelease)(Ubuntu 4.0.1-4ubuntu9) Copyright (C) 2005 Free Software Foundation, Inc. 這是一個免費軟體,可以檢視原始碼進行復制,未經授權不得用於商業或者其他特殊目的。
msvc++
- CPU 頻率——cpu 主頻:2660.554 MHz
- 記憶體——記憶體總量:484412 kB
- 平臺—— Windows XP Pro
- 編譯器—— Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 13.10.3077 for 80×86 Copyright (C) Microsoft Corporation 1984-2002. All rights reserved
實驗專案
- 優先順序佇列文字的push操作時間測試
- 優先順序佇列文字的push和pop操作時間測試
- 優先順序佇列隨機整數的push操作時間測試
- 優先順序佇列隨機整數的push和pop時間測試
- 優先順序佇列文字的pop操作記憶體使用測試
- 優先順序佇列文字的join操作時間測試
- 優先順序佇列文字的修改操作時間測試一
- 優先順序佇列文字的修改操作時間測試二
觀察實驗
底層資料結構的複雜度
下表按照遞增的順序,展示了不同底層資料複雜度。有一點非常有趣:這個表也反映了關於操作的常量的事情(詳見攤銷push和pop操作)。
push | pop | modify | erase | join | |
std::priority_queue | Θ(n) worstΘ(log(n)) amortized | Θ(log(n)) Worst | Theta;(n log(n)) Worst[std note 1] | Θ(n log(n))[std note 2] | Θ(n log(n))[std note 1] |
priority_queuewith Tag =pairing_heap_tag | O(1) | Θ(n) worstΘ(log(n)) amortized | Θ(n) worstΘ(log(n)) amortized | Θ(n) worstΘ(log(n)) amortized | O(1) |
priority_queuewith Tag =binary_heap_tag | Θ(n) worstΘ(log(n)) amortized | Θ(n) worstΘ(log(n)) amortized | Θ(n) | Θ(n) | Θ(n) |
priority_queuewith Tag =binomial_heap_tag | Θ(log(n)) worstO(1) amortized | Θ(log(n)) | Θ(log(n)) | Θ(log(n)) | Θ(log(n)) |
priority_queuewith Tag =rc_binomial_heap_tag | O(1) | Θ(log(n)) | Θ(log(n)) | Θ(log(n)) | Θ(log(n)) |
priority_queuewith Tag =thin_heap_tag | O(1) | Θ(n) worstΘ(log(n)) amortized | Θ(log(n)) worstO(1) amortized,orΘ(log(n)) amortized[thin_heap_note] | Θ(n) worstΘ(log(n)) amortized | Θ(n) |
[std note 1]這不是演算法的屬性,而是歸咎於STL的優先佇列不支援迭代器(也就不支援訪問佇列中指定值的能力)。如果優先順序佇列底層採用std::vector,那麼利用STL介面卡以及top函式將會返回佇列中第一個元素的引用這一事實,仍然可以將複雜度降低到Θ(n)。然而,如果採用std::deque實現,則無法降低複雜度。
[std note 2]與[std note 1]一樣,也不是演算法的屬性,而是與STL實現相關。同樣,如果優先順序佇列採用std::vector實現,也可以將複雜度降低到Θ(n),但是,是一個非常大的常數(必須呼叫std::make_heap,這個操作是一個開銷很大的線性操作);如果優先順序佇列用std::deque實現,則不可能降低複雜度。
[thin_heap_note] 一個稀疏堆,最壞情況的修改時間總是為&Theta(log(n)),但是攤銷時間依賴於操作的特性:I)如果是插入較大的key值(從優先佇列的比較函式角度來看),攤銷時間是O(1)。但是如果II)插入一個較小的key值,那麼攤銷時間和最壞的情況下是一樣的。注意:在大多數演算法中,I)很重要,II)並不重要。
攤銷push和pop操作
很多情況下,優先順序佇列主要是為了進行頻繁的push和pop操作。所有底層資料結構都有相同的攤銷對數複雜度,但是它們的常數不同。
上表顯示,不同資料結構在某些方面是受限制的。總而言之,如果某個資料結構在最壞情況下的複雜度比另一個資料結構更低,那麼從攤銷複雜度的角度來講,它會更慢。因此,舉個例子,一個冗餘計數二項式堆(優先順序佇列帶有這樣的tag, Tag = rc_binomial_heap_tag)在最壞情況下的push操作比二項堆(優先順序佇列帶有這樣的tag,Tag = binomial_heap_tag)更低,因此冗餘二項堆的攤銷push操作從常數角度看比二項堆更慢。
如上表所示,受限制最小的底層資料結構是二叉堆和配對堆。因此,也就不奇怪他們在攤銷常數上表現最好。
- 配對堆對於非原始型別(例如:std::strings)表現最好,正如Priority Queue Text push Timing Test 和Priority Queue Text push and pop Timing Test所展示的一樣。
- 正如Priority Queue Random Integer push Timing Test和Priority Queue Random Integer push and pop Timing Test兩個實驗所示,二叉堆對於非原始型別(例如int)表現得最好。
圖形演算法
在一些圖形演算法中,需要進行key遞減操作[clrs2001];如果一個值是增長的(從優先順序佇列比較函式的角度講),這個操作與修改操作的複雜度是相同的。上表和Priority Queue Text modify Timing Test – I顯示:改良堆(優先順序佇列帶有tag: Tag = thin_heap_tag)比配對堆(優先順序佇列帶有tag:Tag = pairing_heap_tag)的效能要好,然而餘下的測試卻得到了相反的結果。
這使得在這種情況下應該使用哪種實現變得很難決定。例如,Dijkstra的最短路徑演算法,需要進行Θ(n)次push和pop操作 (n是結點的數量)、O(n2) 次修改操作,實際情況中也可以是Θ(n)次修改操作 。在難以找到先驗特徵的圖中,實際modify操作的數量會讓push和pop操作的數量變得微不足道。