1. 程式人生 > >《演算法》第四版algs4:sort排序演算法C++實現

《演算法》第四版algs4:sort排序演算法C++實現

具體程式碼:
https://github.com/Nwpuer/algs4-in-cpp/blob/master/sort.h

這一章的實現,相比於書上我做了輕微的改變,主要目的是把程式碼寫的更加簡潔易懂,更加關注演算法是如何實現的,換言之,更關注演算法的本質,而不是如何去設計一個C++類。
做出的改動如下:
1.沒有再將每個排序演算法分別寫成一個類,而是將每個排序演算法都寫成一個函式,放在"sort.h"檔案中
2.為了將演算法適用於不同型別,書上是隻要該型別實現了Comparable介面就行,而我將演算法實現為了範型函式
3.書上使用的less()函式,因為C++中類一般都要求實現operator<,也就是可以用<來比較,所以我們直接用<,而不再去實現less()
4.書上使用的exch()函式,直接使用swap()。值得注意的是,每次使用swap之前我們都聲明瞭using std::swap,然後我們呼叫的時候是swap(),這樣如果排序的型別自己實現了swap,那麼會優先呼叫它,而不是std::swap。
5.書上實現的SortCompare,而我這裡改成了直接計算排序的時間,在"sort_test.cpp"中。這樣雖然功能沒有SortCompare強,但是實現更簡單了。

下面是關於各個演算法的一些筆記:

選擇排序:在陣列中找到最小的那個元素,將它和第一個元素交換;然後在剩下的元素中找到最小的,將它和第二個元素交換。如此往復。所以選擇排序左邊的排序完成的部分是已經確定好位置的,不用再去動它,只需要在右邊沒有排序的部分中找到最小的然後和該部分的第一個交換就行了。可以在腦子中想象出那個排序的畫面。
選擇排序的特點:執行時間與輸入無關,一個有序的陣列和一個元素隨機排列的陣列所用時間一樣長

插入排序:和整理牌的方法類似,拿到一張牌,將它插入到已經有序的牌中的適當位置。也就是,當前索引左邊的元素都是有序的,但是它們的最終位置還沒有確定,有可能為了給更小的元素騰出空間而被移動。取下一個未排序的元素,將它放到前面元素的適當位置中。也可以在腦子中想象出排序的畫面。
插入排序的特點:對於部分有序的陣列很有效。當倒置的數量很少時,插入排序可能比其他演算法都要快。也很適合小規模陣列。這很重要,因為這些型別的陣列在實際應用中經常出現,並且也是高階排序演算法的中間過程。所以高階排序演算法中經常會用到插入排序。

希爾排序:基於“插入排序”的演算法。為什麼要基於插入排序呢?因為排序之初,每個子陣列都很短(因為間隔很大),排序之後的子陣列都是部分有序的,這兩種情況都適合插入排序。所以希爾排序更高效的原因是它權衡了子陣列的規模和有序性。
希爾排序的特點:與選擇和插入排序不同,希爾排序可以用於大型陣列,並且陣列越大,優勢越大。
什麼時候用希爾排序:對於中等大小的陣列它的速度可以接受,並且優點是程式碼量很小,不需要使用額外記憶體。對於之後的高效的排序演算法,除了對於很大的N,它們可能只會比希爾排序快兩倍(可能還達不到),而且更復雜。

歸併排序:優點是時間複雜度為O(NlgN),所以可用於大型陣列的排序;缺點是所需額外空間和N成正比。
共有兩種實現,自頂向下以及自底向上。和我們通常所理解的一樣,自頂向下是用的遞迴方法,實現起來更加容易理解;雖然自底向上的實現方法程式碼量更少,但是其中的細節比較容易讓人困惑。

快速排序:優點:實現簡單,原地排序,將長度為N的陣列排序所需時間和NlgN成正比。缺點:比較脆弱,需要非常小心才能避免低劣的效能。
和歸併排序一樣,快排也是分治的演算法。將一個數組分成兩個子陣列,將兩部分獨立的排序。快排和歸併排序的區別可以在腦子中很形象的想象出來:
歸併排序是將陣列分為兩個子陣列分別排序,再將有序的子陣列歸併以將整個陣列排序;
快速排序則是先將小於某個值的元素放到左邊,大於某個值的元素放到右邊,這樣的話將左右兩個子陣列分別排序後,這個陣列就自然有序了。
快速排序在最好情況下所用比較次數滿足分治遞迴的C(N)=2C(N/2)+N,該公式的解C(N)~NlgN。不是最好的情況下的結果也是類似的。
快排的一個缺點是:在切分不平衡時這個程式可能會極為低效。比如第一次從最小的元素開始切分,第二次從第二小的元素開始切分,等等。每次呼叫只移除一個元素,會導致一個大的子陣列需要切分很多次。最多需要N+(N-1)+(N-2)+…+1=(N+1)N/2~N^2/2次比較。所以我們在一開始就將陣列打亂,就是為了使產生糟糕的切分的可能性降到最低。
關於快排的改進:1.和歸併排序一樣,排序小的子陣列時切換到插入排序。
2.對於含有大量重複元素的陣列(現實應用中經常出現),之前的實現還有大量的改進空間,可將線性對數級別的效能提高到線性級別。使用三向切分(參見程式碼中的QuickSort3way_helper),簡單的說,就是將元素分為小於,等於,大於三個部分,而不是原來的小於大於,大於等於兩個部分,即將等於切分元素的單獨提取出來。

優先佇列:
適用情形:有些時候程式需要處理有序的元素,但不要求全部有序,或不要求一次將它們排序。比如說我們想要其中最大的一個,或者其中最大的M個,這時候如果將它們全部排序就是沒有必要的了。
二叉堆:一組能夠用堆有序的完全二叉樹排序的元素,並且在陣列中按照層級儲存(不使用陣列的第一個元素),簡稱為堆。
在一個堆中,位置為k的節點的父節點的位置為k/2(取整),它的兩個子節點的位置為2k和2k+1。

堆排序:
1.注意下標是從1開始的,位置為0的元素未參與排序,一般作為哨兵
2.堆排序主要是兩步,一是堆的構造,二是反覆刪除最大的元素。
關於這裡程式碼在堆的構造中為什麼是用sink()來從右到左地來構造子堆,而不是我們直覺上的從左到右的使用swim()類似插入元素一樣來構造子堆:
1)陣列中從右向左至少有一半是完全二叉樹的葉子節點,就算二叉樹的最下面一層不滿也是這樣的。這些葉子節點本身就是大小為1的堆,因此可以跳過這一半的元素,而如果使用swim()來構造的話,則必須把所有元素都給掃描了。一般來說,會快上20%
2)因為沒有用到swim(),所以整個的程式碼更少,更簡潔。

堆排序的優缺點:
優點:最壞的情況下也能保證使用O(NlgN)的時間複雜度和恆定的額外空間,所以空間十分緊張的時候很有用。
缺點:很致命,無法利用快取。陣列元素很少和相鄰的其他元素相比較,因此快取未命中的次數遠遠高於大多數比較都在相鄰元素間進行的演算法,如快速排序,歸併排序,甚至是希爾排序,所以在現代系統的許多應用很少使用它。
但是另一方面,用堆來實現的優先佇列在現代應用程式中越來越重要,因為它能在插入操作和刪除最大元素操作混合的動態場景中保證對數級別的執行時間。

各個排序演算法的比較(來自algs4):
在這裡插入圖片描述

該選用哪種排序演算法:
大多數情況下,快排是最佳選擇:之所以快是因為它的內迴圈指令很少,並且能夠利用快取,因為它總是順序地訪問資料,所以執行時間的增長數量級為~cNlgN,這裡的c比其他的線性對數級別的排序演算法的相應常數都要小。
但是如果穩定性很重要而且空間不是問題,歸併排序可能最好。
其他情況,比如陣列已經部分有序時,或者對於小陣列排序(作為複雜排序演算法的基礎)時,插入排序可能更好;堆排序可能用處不大,但是優先佇列的卻很重要。