誰纔是最強的排序算法: 快速排序, 歸併排序, 堆排序

知乎上有一個問題是這樣的:

堆排序是漸進最優的比較排序算法,達到了O(nlgn)這一下界,而快排有一定的可能性會產生最壞劃分,時間複雜度可能爲O(n^2),那爲什麼快排在實際使用中通常優於堆排序?

昨天剛好寫了一篇關於快排優化的文章,今天再多做一個比較吧。首先先看一個排序算法圖:

 

排序方法平均情況最好情況最壞情況輔助空間穩定性
冒泡排序 O(n^2) O(n) O(n^2) O(1) 穩定
簡單選擇排序 O(n^2) O(n^2) O(n^2) O(1) 穩定
直接插入排序 O(n^2) O(n) O(n^2) O(1) 穩定
希爾排序 O(nlogn)~O(n^2) O(n^1.3) O(n^2) O(1) 不穩定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不穩定
歸併排序 O(nlogn) O(nlogn) O(nlogn) O(n) 穩定
快速排序 O(nlogn) O(nlogn) O(n^2) O(logn)~O(n) 不穩定

可以看到,到達nlogn級別的排序算法,一共有三種,分別是堆排序,歸併排序以及快速排序,其中只有歸併排序最穩定。那麼,爲什麼要說快速排序的平均情況是最快的呢?

實際上在算法分析中,大O的作用是給出一個規模的下界,而不是增長數量的下界。因此,算法複雜度一樣只是說明隨着數據量的增加,算法時間代價增長的趨勢相同,並不是執行的時間就一樣,這裏面有很多常量參數的差別,比如在公式裏各個排序算法的前面都省略了一個c,這個c對於堆排序來說是100,可能對於快速排序來說就是10,但因爲是常數級所以不影響大O。

另外, 堆排比較的幾乎都不是相鄰元素,對cache極不友好, 數據讀取的開銷變大。在計算機進行運算的時候,數據不一定會從內存讀取出來,而是從一種叫cache的存儲單位讀取。原因是cache相比內存,讀取速度非常快,所以cache會把一部分我們經常讀取的數據暫時儲存起來,以便下一次讀取的時候,可以不必跑到內存去讀,而是直接在cache裏面找。

一般認爲讀取數據遵從兩個原則:temporal locality,也就是不久前讀取過的一個數據,在之後很可能還會被讀取一遍;另一個叫spatial locality,也就是說讀取一個數據,在它周圍內存地址存儲的數據也很有可能被讀取到。因此,在讀取一個單位的數據(比如1個word)之後,不光單個word會被存入cache,與之內存地址相鄰的幾個word,都會以一個block爲單位存入cache中。另外,cache相比內存小得多,當cache滿了之後,會將舊的數據剔除,將新的數據覆蓋上去。
在進行堆排序的過程中,由於我們要比較一個數組前一半和後一半的數字的大小,而當數組比較長的時候,這前一半和後一半的數據相隔比較遠,這就導致了經常在cache裏面找不到要讀取的數據,需要從內存中讀出來,而當cache滿了之後,以前讀取的數據又要被剔除。
簡而言之快排和堆排讀取arr[i]這個元素的平均時間是不一樣的。

 

即使是同樣的算法,不同的人寫的代碼,不同的應用場景下執行時間也可能差別很大。下面是一個測試數據:

  1.  
    測試的平均排序時間:數據是隨機整數,時間單位是s
  2.  
    數據規模 快速排序 歸併排序 希爾排序 堆排序
  3.  
    1000萬 0.75 1.22 1.77 3.57
  4.  
    5000萬 3.78 6.29 9.48 26.54
  5.  
    1億 7.65 13.06 18.79 61.31

堆排序每次取一個最大值和堆底部的數據交換,重新篩選堆,把堆頂的X調整到位,有很大可能是依舊調整到堆的底部(堆的底部X顯然是比較小的數,纔會在底部),然後再次和堆頂最大值交換,再調整下來,可以說堆排序做了許多無用功。

 

總結起來就是,快排的最壞時間雖然複雜度高,但是在統計意義上,這種數據出現的概率極小,而堆排序過程裏的交換跟快排過程裏的交換雖然都是常量時間,但是常量時間差很多。

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