排序問題:###
輸入:n個數的一個序列<a1,a2,...,an>
輸出:輸入序列的一個排列<a1',a2',...an'>,滿足a1'<=a2'<=,...<=an'
下面先上總結,然后再逐一說明各個算法。
排序方法 | 平均時間 | 最差時間 | 額外空間 | 是否穩定 |
---|---|---|---|---|
插入排序 | O(n*n) | O(n*n) | O(1) | 是 |
歸并排序 | O(nlgn) | O(nlgn) | O(n) | 是 |
堆排序 | O(nlgn) | O(nlgn) | O(1) | 否 |
選擇排序 | O(n*n) | O(n*n) | O(1) | 否 |
快速排序 | O(nlgn) | O(n*n) | O(lgn)->O(n) | 否 |
計數排序 | O(n+k) | O(n+k) | O(n+k) | 是 |
基數排序 | O(d(n+k)) | O(d(n+k)) | O(n+k) | 是 |
桶排序 | O(n) | O(n*n) | O(n) | 是 |
鴿巢排序 | O(n) | O(n) | O(range) | 否 |
梳排序 | O(n*n) | O(n*n) | O(1) | 否 |
希爾排序 | O(n*n) | O(n*n) | O(1) | 否 |
冒泡排序 | O(n*n) | O(n*n) | O(1) | 是 |
煎餅排序 | O(n*n) | O(n*n) | O(1) | 否 |
1.插入排序
(1)原理說明
每次將一個新的數放到已經排好序的數組中
初始只有一個數,這是排好序的
以后每次加一個新的數,都選好位置讓它插入,使得插入后的數組還是有序的
這就像打撲克時摸牌整理一樣
(2)算法簡單實現如下:Github插入排序
(3)算法性能分析:
插入排序對已排序或近似排序的序列會有較好性能,因為此時內循環~O(1),所以整體時間->O(n),而對恰好反序排列的序列要耗時O(n*n),平均情況耗時O(n*n)
由于只有常數個額外空間使用,所以空間為O(1)
此內循環的終止條件(<而不是<=)決定了此排序算法是穩定的
2.歸并排序
(1)原理說明:
歸并排序基于這樣一種基本算法:我有兩個已經排好序的數組,怎樣把它們合并成一個有序的數組
這個基本算法實現很簡單,每次比較兩個數組當前的最小元素的大小,把其中小的放進結果數組中,重復這一過程直到其中一個數組元素耗盡,之后把沒有耗盡的數組剩下的數全部放到結果數組中去即可。其實為了減少這種元素個數是否耗盡的情況判斷,我們可以在兩個數組的最后添加一個哨兵元素(值為MAX以致兩個數組中出現的元素都不可能比它大),這樣當一個數組原本的元素耗盡則只剩下設置的哨兵元素,另一個數組元素永遠比這個哨兵元素小就會一直把此數組中的剩余元素放進結果數組中,達到了減少判斷元素耗盡的判斷。
歸并排序是分治思想(詳見算法思想——分治法)的一種應用。
歸并排序將一個數組不斷地二分,直到子數組元素個數為1個;這時1個元素顯然為已經排好序的,然后將這些數組兩兩合并,得到元素個數更多的排好序的子數組,這樣不斷重復這一過程最重會得到原始數組排好序的結果。一個例子如下圖所示:
(2)算法簡單實現如下:Github歸并排序
(3)算法性能分析:
為了解決一個數組排序(T(n)),我們需要解決這個數組的兩個子數組(T(n/2))的排序并將其合并(O(n)),所以:
T(n)=T(n/2)+O(n)
所以耗時為O(nlgn)
由于進行了數組的拷貝,所以額外空間消耗為O(n)
3.堆排序
(1)原理說明:
這是利用堆(詳見基本數據結構(棧、隊列、鏈表、樹、堆))這種數據結構來進行排序的算法。具體我們使用的是最大堆。
具體堆排序算法如下:
首先我們構建最大堆
然后每次將堆頭(heap[0]也即剩余堆中的最大值)與堆尾交換,這樣當前最大的值就被放在了最后面
這時我們將堆大小減一,再對堆頭維護最大堆性質(交換破壞了最大堆特性),維護后的堆重復進行上述交換,直到最后堆只剩下一個元素,即為最小值在第一個位置,完成排序。示例如下圖所示:
(2)算法簡單實現如下:Github堆排序
(3)算法性能分析:
建堆耗時O(n)
維護堆耗時O(h)=O(lgn)
維護堆在循環中所以整體堆排序耗時O(nlgn)
因為沒有占用額外空間(原址排序),所以空間為O(1)
由此可見此算法結合了插入排序(空間復雜度低)和歸并排序(時間復雜度低)的優點。
這種堆排序是不穩定的。
4.選擇排序
(1)原理說明:
每次選擇剩余元素中最小的,和已排序的數組的后面一個元素交換。初始剩余元素為全部,已排序個數為0,即應該把第一次選出來的元素和位置為0的元素交換。例子如下圖所示:
(2)算法簡單實現如下:Github選擇排序
(3)算法性能分析:
兩層循環耗時O(n*n)
沒有額外為數據分配空間,所以空間消耗為O(1)
由于涉及到swap,所以是不穩定的,考慮(1,2,2,1)
不過如果額外分配O(n)空間,可以使其變穩定
5.快速排序
(1)原理說明:
快速排序也是基于分治細想的一種算法,只是其與歸并排序采取的具體分治策略不一樣。
快排每次選擇待排序列中的一個數,把小于這個數的數放在此數左邊,大于這個數的數放在此數右邊,然后再遞歸地對此數的左半部分和右半部分重復之行上述策略,直到最后每個部分只剩下一個數,這時原數組就被排好序了。
由此可見快排的關鍵在于如何選擇“主元”,并把數據按“主元”將數據分為左右兩部分,然而answer is“主元”的選取可以隨機,也可以每次選固定位置的數。??而數據的劃分可以引進一個坐標記錄k,記錄當前小于“主元”的最后一個數所在位置。例子如下所示:
(2)算法簡單實現如下:Github快速排序
(3)算法性能分析:
平均情況下時間O(nlgn)
最壞情況下O(n*n),這發生在每次劃分都不均勻的情況下(9,8,7,6,5,4,3,2,1),所以隨機選擇主元可以有效避免最壞情況發生
空間消耗O(lgn)
快排是不穩定的,但存在穩定版本
6.計數排序
(1)原理說明:
這是一種專門針對整數的算法(準確講應該是非負整數,不過如果是負數我們可以添加offset使其變為正整數)。如果我們知道待排序非負整數都小于一個值max,則我們可以定義長度為max數組,記錄序列中每個元素有多少個元素比它小,則這個記錄的位置就為其在最后排序結果中應該在的位置。注意如果存在重復的元素,則確定完其中一個的位置后我們應當對記錄數組進行修改,以確保兩次放置不會放到同一個位置上。例子如下圖所示:
(2)算法簡單實現如下:Github計數排序
(3)算法性能分析:
因為都是單層for循環,所以時間為O(n+k)
空間消耗為O(n+k)
注意上面算法實現中要從后往前逐一放置元素,這保證了算法的穩定性。
7.基數排序
(1)原理說明:
基數排序也是針對整數的排序(其實非整數可以乘以一個大的10^k使所有待排序數都變為整數,負數可以單獨拿出來變為正數排序后進行合并),如果我們知道待排數據中最大數據的位數,則可以對從低到高對每一位進行排序(使用基數排序,注意計數排序的穩定性保證了基數排序的正確性),則當排好最高位后即得到排好序的結果。一個例子如下所示:
(2)算法簡單實現如下:Github基數排序
(3)算法性能分析:
有d位進行計數排序,所以時間消耗O(n+k)
額外空間與計數排序一樣
其也是穩定的
8.桶排序
(1)原理說明:
這是針對[0,1)之間的小數進行排序的一種算法(其實可以將序列同除以一個大的10^k使它們都落在[0,1)之間),假設有n個數的序列,則我們將[0,1)劃分為n個子區間(形象地稱為“桶”)然后我們將這n個數放進這n個桶里,假設這些數服從隨機分布,則不太可能集中分布在一個桶里,我們針對每個桶里的數進行插入排序,然后再把每個排過序的桶按序合并起來,即得到最終的排序結果。例子如下圖所示:
(2)算法簡單實現如下:Github桶排序
(3)算法性能分析:
平均時間為O(n)
但如果不幸落在同一桶里,就是個插入排序了,所以最差O(n*n),不過如果采用高效的比較算法比如快排、合并排序、堆排序等等則會最差O(nlgn)
因為要準備“桶”,所以消耗額外O(n)空間
加入我們采用插入排序,則此時桶排序時穩定的。
9.鴿巢排序
(1)原理說明:這個也是針對整數排序的算法,如果我們知道待排序列最小和最大的數,則我們可以首先減去最小值,使序列offset到以0為起點的數據,然后定義一個數組,大小為序列的range(max-min),然后遍歷序列將每個數據存儲在相應位置上,這樣最后我們就得到一個各個元素出現次數統計的數組,沒有出現過的記錄為0,這樣我們從小到大遍歷這個數組,將個數不為0的元素加上最小值(恢復偏置)輸出,即得到最后的輸出結果。例子如下圖所示:
(2)算法簡單實現如下:Github鴿巢排序
(3)算法性能分析:
鴿巢排序耗時為O(n)
但是建造鴿巢要耗費O(range)空間,如果范圍很大,則這很浪費空間
講道理這個和計數排序好像的,但鴿巢排序是不穩定的,放在同一個鴿巢里的數據如果沒有額外映射關系,則不能確定原來的順序。而計數排序是按原數組從后往前的位置查找其應該放的位置,所以是穩定的。
10.梳排序
(1)原理說明:
我們從頭到尾遍歷比較位置為i和i+gap的兩個數,如果后者小于前者,則進行交換。上述為一次循環。gap初值設為length/1.3(準確為1.247,這個設的太小收斂過慢,設的太大會返回錯誤排序結果),每進行一次循環gap/=1.3,直到gap變為最小值1,進行最后一次遍歷得到最終結果。一個例子如下圖所示:
(2)算法簡單實現如下:Github梳排序
(3)算法性能分析:
時間消耗為O(n*n)
空間消耗為O(1)
它是不穩定排序
11.希爾排序
(1)原理說明:
我們從頭開始選擇增量為n的length/n個數對他們分別進行插入排序(這會有n組結果),這是一次循環。增量從length/2開始一直除以2直到增量變為1,循環結束則得到最終排序結果。例子如下圖所示:
(2)算法簡單實現如下:Github希爾排序
(3)算法性能分析:
時間消耗為O(n*n)
空間消耗為O(1)
這是不穩定的排序算法,考慮(1,2,2,1)
12.冒泡排序
(1)原理說明:
這種排序大部分情況下只用在教學,是個toy algorithm。每次從首位開始逐一向后比較當前數和后一個數的大小關系,如果后者大于前者則進行次序交換,這樣當第一輪遍歷到最后時最大的數就“浮”到最上面了。進行n-1次這樣的遍歷,則每次遍歷都會“浮出”剩余序列中最大的數組,所有n-1次遍歷完成后,就得到最終排好序的數列。例子如下圖所示:
(2)算法簡單實現如下:Github冒泡排序
(3)算法性能分析:
時間消耗O(n*n)
空間消耗O(1)
是穩定的
13.煎餅排序
(1)原理說明:
煎餅排序如其名,和煎餅的時候翻面很相似。我們選擇當前剩余最大的數,一鏟子鏟下去從上到下翻一個個兒,則此時最大的值到了最上面,然后選擇下面已排好順序的數的上面(初始情況就是最下面)再一鏟子鏟下去,則此時最上面的剩余最大的數就翻轉到了其應該在的位置,重復這樣的步驟,就可以得到排好序的序列。翻轉煎餅的例子如下圖所示:
(2)算法簡單實現如下:Github煎餅排序
(3)算法性能分析:
時間消耗O(n*n)
因為借用了棧,所以空間消耗O(n),其實可以不用棧進行操作的
是不穩定的
其實還有很多排序算法,這里不再進行說明。(PS:不是說它們不重要,只是我們更側重于了解這些排序算法內在的思想,而不僅僅是算法本身)
另PS:在算法可視化網上的sort中可以看到以上所有排序算法的動態演示