二分檢索是查詢有序陣列最簡單然而最有效的演算法之一。現在的問題是,更復雜的演算法能不能做的更好?我們先看一下其他方法。
有些情況下,雜湊整個資料集是不可行的,或者要求既查詢位置,又查詢資料本身。這個時候,用雜湊表就不能實現O(1)的執行時間了。但對有序陣列, 採用分治法通常可以實現O(log(n))的最壞執行時間。
在下結論前,有一點值得注意,那就是可以從很多方面“擊敗”一個演算法:所需的空間,所需的執行時間,對底層資料結構的訪問需求。接下來我們做一個執行時對比實驗,實驗中建立多個不同的隨機陣列,其元素個數均在10,000到81,920,000之間,元素均為4位元組整型資料。
二分檢索
二分檢索演算法的每一步,搜尋空間總會減半,因此保證了執行時間。在陣列中查詢一個特定元素,可以保證在 O(log(n))時間內完成,而且如果找的正好是中間元素就更快了。也就是說,要從81,920,000個元素的陣列中找某個元素的位置,只需要27個甚至更少的迭代。
由於二分檢索的隨機跳躍性,該演算法並非快取友好的,因此只要搜尋空間小於特定值(64或者更少),一些微調的二分檢索演算法就會切換回線性檢索繼續查詢。然而,這個最終的空間值是極其架構相關的,因此大部分框架都沒有做這個優化。
快速檢索;最後迴歸到二分檢索的快速檢索
如果由於某些原因,陣列長度未知,快速檢索可以識別初始的搜尋域。這個演算法從第一個元素開始,一直加倍搜尋域的上界,直到這個上界已經大於待查關鍵字。之後,根據實現不同,或者採用標準的二分檢索查詢,或者開始另一輪的快速檢索。前者可以保證O(log(n)) 的執行時間,後者則更接近O(n)的執行時間。
如果我們要找的元素比較接近陣列的開頭,快速檢索就非常有效。
抽樣檢索
抽樣檢索有點類似二分檢索,不過在確定主要搜尋區域之前,它會先從陣列中拿幾個樣例。最後,如果範圍足夠小,就採用標準的二分檢索確定待查元素的準確位置。這個理論很有趣,不過在實踐中執行效果並不好。
插值檢索;最後迴歸到順序查詢的插值檢索
在被測的演算法中,插值檢索可以說是“最聰明”的一個演算法。它類似於人類使用電話簿的方法,它試圖通過假設元素在陣列中均勻分佈,來猜測元素的位置。
首先,它抽樣選擇出搜尋空間的開頭和結尾,然後猜測元素的位置。演算法一直重複這個步驟,直到找到元素。如果猜測是準確的,比較的次數大概是O(log(log(n)),執行時間大概是O(log(n));但如果猜測的不對,執行時間就會是O(n)了。
插值檢索的一個改進版本是,只要可推測我們猜測的元素位置是接近最終位置的,就開始執行順序查詢。相比二分檢索,插值檢索的每次迭代計算代價都很高,因此在最後一步採用順序查詢,無需猜測元素位置的複雜計算,很容易就可以從很小的區域(大概10個元素)中找到最終的元素位置。
圍繞插值檢索的一大疑問就是,O(log(log(n))的比較次數可能產生O(log(log(n))的執行時間。這並非個案,因為儲存訪問時間和計算下一次猜測的CPU時間相比,這兩者之間要有所權衡。如果資料量很大,而且儲存訪問時間也很顯著,比如在一個實際的硬碟上,插值檢索輕鬆擊敗二分檢索。然而,實驗表明,如果訪問時間很短,比如說RAM,插值檢索可能不會產生任何好處。
試驗結果
試驗中的原始碼都是用Java寫的;每個實驗在相同的陣列上執行10次;陣列是隨機產生的整型陣列,儲存在記憶體中。
在插值檢索中,首先會採用抽樣檢索,從檢索空間拿20個樣例,以確定接下來的搜尋域。如果假定的域只有10個或更少的元素,就開始採用線性檢索。另外,如果這個搜尋域元素個數小於2000,就回退到標準的二分檢索了。
作為參考,java預設的Arrays.binarySearch演算法也被加入實驗,以同自定義的演算法對比執行時間。
儘管我們對插值檢索期望很高,它的實際執行時間並未擊敗java預設的二分檢索演算法。如果儲存訪問時間長,結合採用某些型別的雜湊樹和B+樹可能是一個更好的選擇。但值得注意的是,對均勻分佈的陣列,組合使用插值檢索和順序檢索在比較次數上總能勝過二分檢索。不過平臺的二分檢索已經很高效,所以很多情況下,可能不需要用更復雜的演算法來代替它。
原始資料 – 每個檢索的平均執行時間
Size |
Arrays. |
Interpolation |
Interpolation |
Sampling |
Binary |
Gallop |
Gallop |
10,000 | 1.50E-04 ms | 1.60E-04 ms | 2.50E-04 ms | 3.20E-04 ms | 5.00E-05 ms | 1.50E-04 ms | 1.00E-04 ms |
20,000 | 5.00E-05 ms | 5.50E-05 ms | 1.05E-04 ms | 2.35E-04 ms | 7.00E-05 ms | 1.15E-04 ms | 6.50E-05 ms |
40,000 | 4.75E-05 ms | 5.00E-05 ms | 9.00E-05 ms | 1.30E-04 ms | 5.25E-05 ms | 1.33E-04 ms | 8.75E-05 ms |
80,000 | 4.88E-05 ms | 5.88E-05 ms | 9.88E-05 ms | 1.95E-04 ms | 6.38E-05 ms | 1.53E-04 ms | 9.00E-05 ms |
160,000 | 5.25E-05 ms | 5.94E-05 ms | 1.01E-04 ms | 2.53E-04 ms | 6.56E-05 ms | 1.81E-04 ms | 9.38E-05 ms |
320,000 | 5.16E-05 ms | 6.13E-05 ms | 1.22E-04 ms | 2.19E-04 ms | 6.31E-05 ms | 2.45E-04 ms | 1.04E-04 ms |
640,000 | 5.30E-05 ms | 6.06E-05 ms | 9.61E-05 ms | 2.12E-04 ms | 7.27E-05 ms | 2.31E-04 ms | 1.16E-04 ms |
1,280,000 | 5.39E-05 ms | 6.06E-05 ms | 9.72E-05 ms | 2.59E-04 ms | 7.52E-05 ms | 2.72E-04 ms | 1.18E-04 ms |
2,560,000 | 5.53E-05 ms | 6.40E-05 ms | 1.11E-04 ms | 2.57E-04 ms | 7.37E-05 ms | 2.75E-04 ms | 1.05E-04 ms |
5,120,000 | 5.53E-05 ms | 6.30E-05 ms | 1.26E-04 ms | 2.69E-04 ms | 7.66E-05 ms | 3.32E-04 ms | 1.18E-04 ms |
10,240,000 | 5.66E-05 ms | 6.59E-05 ms | 1.22E-04 ms | 2.92E-04 ms | 8.07E-05 ms | 4.27E-04 ms | 1.42E-04 ms |
20,480,000 | 5.95E-05 ms | 6.54E-05 ms | 1.18E-04 ms | 3.50E-04 ms | 8.31E-05 ms | 4.88E-04 ms | 1.49E-04 ms |
40,960,000 | 5.87E-05 ms | 6.58E-05 ms | 1.15E-04 ms | 3.76E-04 ms | 8.59E-05 ms | 5.72E-04 ms | 1.75E-04 ms |
81,920,000 | 6.75E-05 ms | 6.83E-05 ms | 1.04E-04 ms | 3.86E-04 ms | 8.66E-05 ms | 6.89E-04 ms | 2.15E-04 ms |
原始資料 – 每個檢索的平均比較次數
Size |
Arrays. |
Interpolation |
Interpolation |
Sampling |
Binary |
Gallop |
Gallop |
10,000 | ? | 10.6 | 17.6 | 19.0 | 12.2 | 58.2 | 13.2 |
20,000 | ? | 11.3 | 20.7 | 19.0 | 13.2 | 66.3 | 14.2 |
40,000 | ? | 11.0 | 16.9 | 20.9 | 14.2 | 74.9 | 15.2 |
80,000 | ? | 12.1 | 19.9 | 38.0 | 15.2 | 84.0 | 16.2 |
160,000 | ? | 11.7 | 18.3 | 38.0 | 16.2 | 93.6 | 17.2 |
320,000 | ? | 12.4 | 25.3 | 38.2 | 17.2 | 103.8 | 18.2 |
640,000 | ? | 12.4 | 19.0 | 41.6 | 18.2 | 114.4 | 19.2 |
1,280,000 | ? | 12.5 | 20.2 | 57.0 | 19.2 | 125.5 | 20.2 |
2,560,000 | ? | 12.8 | 22.7 | 57.0 | 20.2 | 137.1 | 21.2 |
5,120,000 | ? | 12.7 | 26.5 | 57.5 | 21.2 | 149.2 | 22.2 |
10,240,000 | ? | 13.2 | 25.2 | 62.1 | 22.2 | 161.8 | 23.2 |
20,480,000 | ? | 13.4 | 23.4 | 76.0 | 23.2 | 175.0 | 24.2 |
40,960,000 | ? | 13.4 | 21.9 | 76.1 | 24.2 | 188.6 | 25.2 |
81,920,000 | ? | 14.0 | 19.7 | 77.0 | 25.2 | 202.7 | 26.2 |
原始碼
點此獲取檢索演算法的完整原始碼。注意,程式碼不是產品級別的;比如,在某些例子裡,可能有過多或過少的範圍檢查。