優先佇列的效能測試

zaishaoyi發表於2015-09-12

實驗環境

這篇文章描述了優先佇列的一些效能測試以及測試結果。下文中,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

local

  • CPU 頻率——cpu 主頻:2660.554 MHz
  • 記憶體——記憶體總量:484412 kB
  • 平臺—— Windows XP Pro
  • 編譯器—— g++ (GCC) 4.1.1 20060525 (Red Hat 4.1.1-1) Copyright (C) 2006 Free Software Foundation, Inc. 這是一個免費軟體,可以檢視原始碼進行復制,未經授權不得用於商業或者其他特殊目的。

實驗專案

  1. 優先順序佇列文字的push操作時間測試
  2. 優先順序佇列文字的push和pop操作時間測試
  3. 優先順序佇列隨機整數的push操作時間測試
  4. 優先順序佇列隨機整數的push和pop時間測試
  5. 優先順序佇列文字的pop操作記憶體使用測試
  6. 優先順序佇列文字的join操作時間測試
  7. 優先順序佇列文字的修改操作時間測試一
  8. 優先順序佇列文字的修改操作時間測試二

觀察實驗

底層資料結構的複雜度

下表按照遞增的順序,展示了不同底層資料複雜度。有一點非常有趣:這個表也反映了關於操作的常量的事情(詳見攤銷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操作從常數角度看比二項堆更慢。

如上表所示,受限制最小的底層資料結構是二叉堆和配對堆。因此,也就不奇怪他們在攤銷常數上表現最好。

  1. 配對堆對於非原始型別(例如:std::strings)表現最好,正如Priority Queue Text push Timing Test 和Priority Queue Text push and pop Timing Test所展示的一樣。
  2. 正如Priority Queue Random Integer push Timing TestPriority 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操作的數量變得微不足道。

相關文章