排序算法總結

1.      排序定義:Sorting

所謂排序,就是使一串記錄,按照其中的某個或某些關鍵字的大小,遞增或遞減的排列起來的操作。排序算法,就是如何使得記錄按照要求排列的方法。排序算法在很多領域得到相當地重視,尤其是在大量數據的處理方面。一個優秀的算法可以節省大量的資源。在各個領域中考慮到數據的各種限制和規範,要得到一個符合實際的優秀算法,得經過大量的推理和分析

2.      在計算機科學所使用的排序算法通常被分類爲:

(a)計算的複雜度(最差、平均、和最好性能),依據列表(list)的大小(n)。

一般而言,好的性能是 O(nlogn),且壞的性能是 O(n^2)。對於一個排序理想的性能是 O(n)。

而僅使用一個抽象關鍵比較運算的排序算法總平均上總是至少需要 O(nlogn)。

(b)存儲器使用量(空間複雜度)(以及其他電腦資源的使用)

(c)穩定度:穩定的排序算法會依照相等的關鍵(換言之就是值)維持紀錄的相對次序。

(d)一般的方法:插入、交換、選擇、合併等等。交換排序包含冒泡排序快速排序插入排序包含希爾排序選擇排序包括堆排序等。

3.      C++自帶的algorithm庫函數中提供了排序算法sort

4.      排序算法簡介及分類:

(a)    穩定的排序算法:

冒泡排序(bubble sort) — O(n^2)

雞尾酒排序(Cocktailsort,雙向的冒泡排序) — O(n^2)

插入排序(insertionsort)— O(n^2)

桶排序(bucket sort)— O(n); 需要 O(k) 額外空間

計數排序(countingsort) — O(n+k); 需要 O(n+k) 額外空間

合併排序(merge sort)—O(nlog n); 需要 O(n) 額外空間

原地合併排序— O(n^2)

二叉排序樹排序 (Binary tree sort) —O(nlog n)期望時間; O(n^2)最壞時間;需要 O(n) 額外空間

鴿巢排序(Pigeonholesort) — O(n+k); 需要 O(k) 額外空間

基數排序(radix sort)— O(n·k); 需要 O(n) 額外空間

Gnome 排序— O(n^2)

圖書館排序— O(nlog n) with high probability,需要 (1+ε)n額外空間

(b)    不穩定的

選擇排序(selectionsort)— O(n^2)

希爾排序(shell sort)—O(nlog n) 如果使用最佳的現在版本

組合排序— O(nlog n)

堆排序(heapsort)—O(nlog n)

平滑排序— O(nlog n)

快速排序(quicksort)—O(nlog n) 期望時間,O(n^2) 最壞情況;對於大的、亂數列表一般相信是最快的已知排序

Introsort— O(nlog n)

Patience sorting— O(nlog n+ k) 最壞情況時間,需要 額外的O(n+ k) 空間,也需要找到最長的遞增子串行(longest increasing subsequence)

(c)    不實用的

Bogo排序— O(n× n!) 期望時間,無窮的最壞情況。

Stupid sort— O(n^3); 遞歸版本需要 O(n^2) 額外存儲器

珠排序(Bead sort) — O(n) or O(√n),但需要特別的硬件

Pancake sorting— O(n),但需要特別的硬件

stooge sort——O(n^2.7)很漂亮但是很耗時

5.      八大排序算法:








(a)    插入排序--直接插入排序(Straight Insertion Sort)

時間複雜度:O(n^2)

算法描述:

一般來說,插入排序都採用in-place在數組上實現。具體算法描述如下:

從第一個元素開始,該元素可以認爲已經被排序;

取出下一個元素,在已經排序的元素序列中從後向前掃描;

如果該元素(已排序)大於新元素,將該元素移到下一位置;

重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;

將新元素插入到該位置後;

重複步驟2~5。

要點:設立哨兵,作爲臨時存儲和判斷數組邊界之用。

如果碰見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,所以插入排序是穩定的。

直接插入排序示例:

如果比較操作的代價比交換操作大的話,可以採用二分查找法來減少比較操作的數目。該算法可以認爲是插入排序的一個變種,稱爲二分查找插入排序

(b)    插入排序--希爾排序(遞減增量排序算法)(Shell-Metzner)

時間複雜度:O(n log2 n)

是插入排序的一種更高效的改進版本,是非穩定的排序算法。

希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率

但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位。

基本思想:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。

步長的選擇是希爾排序的重要部分。只要最終步長爲1任何步長序列都可以工作。算法最開始以一定的步長進行排序。然後會繼續以一定步長進行排序,最終算法以步長爲1進行排序。當步長爲1時,算法變爲插入排序,這就保證了數據一定會被排序。

Donald Shell最初建議步長選擇爲(n/2)並且對步長取半直到步長達到1。

操作方法:

例如,假設有這樣一組數[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我們以步長爲5開始進行排序,我們可以通過將這列表放在有5列的表中來更好地描述算法,這樣他們就應該看起來是這樣:

13 14 94 33 82

25 59 94 65 23

45 27 73 25 39

10

然後我們對每列進行排序:

10 14 73 25 23

13 27 94 33 39

25 59 94 65 82

45

將上述四行數字,依序接在一起時我們得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].這時10已經移至正確位置了,然後再以3爲步長進行排序:

10 14 73

25 23 13

27 94 33

39 25 59

94 65 82

45

排序之後變爲:

10 14 13

25 23 33

27 25 59

39 65 73

45 94 82

94

最後以1步長進行排序(此時就是簡單的插入排序了)。

(c)    選擇排序--簡單選擇排序(Simple Selection Sort)(直接選擇排序)

時間複雜度:O(n^2)

基本思想:

在要排序的一組數中,選出最小(或者最大)的一個數與第1個位置的數交換;然後在剩下的數當中再找最小(或者最大)的與第2個位置的數交換,依次類推,直到第n-1個元素(倒數第二個數)和第n個元素(最後一個數)比較爲止。

選擇排序的主要優點與數據移動有關。如果某個元素位於正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對n個元素的表進行排序總共進行至多n-1次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於非常好的一種。

選擇排序的交換操作介於0和(n-1)次之間。選擇排序的比較操作爲(n(n-1)/2)次之間。選擇排序的賦值操作介於0和3(n-1)次之間。比較次數O(n^2),比較次數與關鍵字的初始狀態無關,總的比較次數N=(n-1)+(n-2)+…+1=n*(n-1)/2。交換次數O(n),最好情況是,已經有序,交換0次;最壞情況是,逆序,交換(n-1)次。交換次數比冒泡排序較少,由於交換所需CPU時間比比較所需的CPU時間多,n值較小時,選擇排序比冒泡排序快。

原地操作幾乎是選擇排序的唯一優點,當方度(space complexity)要求較高時,可以考慮選擇排序;實際適用的場合非常罕見。

簡單選擇排序的示例:

 

 簡單選擇排序的改進——二元選擇排序:

簡單選擇排序,每趟循環只能確定一個元素排序後的定位。我們可以考慮改進爲每趟循環確定兩個元素(當前趟最大和最小記錄)的位置,從而減少排序所需的循環次數。改進後對n個數據進行排序,最多隻需進行[n/2]趟循環即可。

(d)    交換排序—冒泡排序(Bubble Sort)

基本思想:

重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端。

冒泡排序對n個項目需要O(n^2)的比較次數,且可以原地排序。儘管這個算法是最簡單瞭解和實現的排序算法之一,但它對於少數元素之外的數列排序是很沒有效率的。

冒泡排序是與插入排序擁有相等的運行時間,但是兩種算法在需要的交換次數卻很大地不同。在最好的情況,冒泡排序需要O(n^2)次交換,而插入排序只要最多O(n)交換。冒泡排序的實現(類似下面)通常會對已經排序好的數列拙劣地運行O(n^2),而插入排序在這個例子只需要O(n)個運算。因此很多現代的算法教科書避免使用冒泡排序,而用插入排序替換之。冒泡排序如果能在內部循環第一次運行時,使用一個旗標來表示有無需要交換的可能,也可以把最好的複雜度降低到O(n)。在這個情況,已經排序好的數列就無交換的需要。若在每次走訪數列時,把走訪順序反過來,也可以稍微地改進效率。有時候稱爲雞尾酒排序,因爲算法會從數列的一端到另一端之間穿梭往返。

操作步驟分爲4步:

1.      比較相鄰的元素。如果第一個比第二個大,就交換他們兩個;

2.      對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數;

3.      針對所有的元素重複以上的步驟,除了最後一個;

4.      持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

冒泡排序的示例:

 

(e)    交換排序--快速排序(Quick Sort)

快速排序使用分治法(Divide andconquer)策略來把一個序列(list)分爲兩個子序列(sub-lists)。

步驟爲:

1.      從數列中挑出一個元素,稱爲"基準"(pivot);

2.      重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區結束之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作。

3.      遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。

遞歸的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞歸下去,但是這個算法總會結束,因爲在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。

 

在平均狀況下,排序n個項目要Ο(n log n)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他Ο(n log n)算法更快,因爲它的內部循環(inner loop)可以在大部分的架構上很有效率地被實現出來。

快速排序是通常被認爲在同數量級(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按關鍵碼有序或基本有序時,快排序反而蛻化爲冒泡排序。爲改進之,通常以“三者取中法”來選取基準記錄,即將排序區間的兩個端點與中點三個記錄關鍵碼居中的調整爲支點記錄。快速排序是一個不穩定的排序方法。

(f)     歸併排序(Merge Sort)

歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。

歸併操作的過程如下:

1.      申請空間,使其大小爲兩個已經排序序列之和,用來存放合併後的序列

2.      設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置

3.      比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置

4.      重複步驟3直到某一指針到達序列尾

5.      將另一序列剩下的所有元素直接複製到合併序列尾

首先考慮下如何將將二個有序數列合併。這個非常簡單,只要從比較二個數列的第一個數,誰小就先取誰,取了後就在對應數列中刪除這個數。然後再進行比較,如果有數列爲空,那直接將另一個數列的數據依次取出即可。

可以看出合併有序數列的效率是比較高的,可以達到O(n)。

解決了上面的合併有序數列問題,再來看歸併排序,其的基本思路就是將數組分成二組A,B,如果這二組組內的數據都是有序的,那麼就可以很方便的將這二組數據進行排序。如何讓這二組組內數據有序了?

可以將A,B組各自再分成二組。依次類推,當分出來的小組只有一個數據時,可以認爲這個小組組內已經達到了有序,然後再合併相鄰的二個小組就可以了。這樣通過先遞歸的分解數列,再合併數列就完成了歸併排序。

歸併排序的效率是比較高的,設數列長爲N,將數列分開成小數列一共要logN步,每步都是一個合併有序數列的過程,時間複雜度可以記爲O(N),故一共爲O(N*logN)。因爲歸併排序每次都是在相鄰的數據中進行操作,所以歸併排序在O(N*logN)的幾種排序方法(快速排序,歸併排序,希爾排序,堆排序)也是效率比較高的。 

有人對冒泡排序,直接插入排序,歸併排序及直接使用系統的qsort()進行比較(均在Release版本下)

對20000個隨機數據進行測試:


對50000個隨機數據進行測試:


再對200000個隨機數據進行測試:


注:有的書上是在mergearray()合併有序數列時分配臨時數組,但是過多的new操作會非常費時。因此作了下小小的變化。只在MergeSort()中new一個臨時數組。後面的操作都共用這一個臨時數組。

 

(g)    選擇排序堆排序(Heap Sort

堆排序是一種樹形選擇排序,是對直接選擇排序的有效改進。

堆的定義如下:具有n個元素的序列(k1,k2,...,kn),當且僅當滿足



時稱之爲堆。由堆的定義可以看出,堆頂元素(即第一個元素)必爲最小項(小頂堆)。
若以一維數組存儲一個堆,則堆對應一棵完全二叉樹,且所有非葉結點的值均不大於(或不小於)其子女的值,根結點(堆頂元素)的值是最小(或最大)的。如:

(a)大頂堆序列:(96, 83,27,38,11,09)

  (b)  小頂堆序列:(12,36,24,85,47,30,53,91)


初始時把要排序的n個數的序列看作是一棵順序存儲的二叉樹(一維數組存儲二叉樹),調整它們的存儲序,使之成爲一個堆,將堆頂元素輸出,得到n 個元素中最小(或最大)的元素,這時堆的根節點的數最小(或者最大)。然後對前面(n-1)個元素重新調整使之成爲堆,輸出堆頂元素,得到n 個元素中次小(或次大)的元素。依此類推,直到只有兩個節點的堆,並對它們作交換,最後得到有n個節點的有序序列。稱這個過程爲堆排序。

因此,實現堆排序需解決兩個問題:
1. 如何將n 個待排序的數建成堆;
2. 輸出堆頂元素後,怎樣調整剩餘n-1 個元素,使其成爲一個新堆。


首先討論第二個問題:輸出堆頂元素後,對剩餘n-1元素重新建成堆的調整過程。
調整小頂堆的方法:

1)設有m 個元素的堆,輸出堆頂元素後,剩下m-1 個元素。將堆底元素送入堆頂((最後一個元素與堆頂進行交換),堆被破壞,其原因僅是根結點不滿足堆的性質。

2)將根結點與左、右子樹中較小元素的進行交換。

3)若與左子樹交換:如果左子樹堆被破壞,即左子樹的根結點不滿足堆的性質,則重複方法(2).

4)若與右子樹交換,如果右子樹堆被破壞,即右子樹的根結點不滿足堆的性質。則重複方法(2).

5)繼續對不滿足堆性質的子樹進行上述交換操作,直到葉子結點,堆被建成。

稱這個自根結點到葉子結點的調整過程爲篩選。如圖:


再討論對n 個元素初始建堆的過程。
建堆方法:對初始序列建堆的過程,就是一個反覆進行篩選的過程。

1)n 個結點的完全二叉樹,則最後一個結點是第個結點的子樹。

2)篩選從第個結點爲根的子樹開始,該子樹成爲堆。

3)之後向前依次對各結點爲根的子樹進行篩選,使之成爲堆,直到根結點。

如圖建堆初始過程:無序序列:(49,38,65,97,76,13,27,49)


                             

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