常見的五類排序算法圖解和實現(選擇類:簡單選擇排序,錦標賽排序,樹形選擇排序,堆排序)


選擇類的排序算法

簡單選擇排序算法

采用最簡單的選擇方式,從頭到尾掃描待排序列,找一個最小的記錄(遞增排序),和第一個記錄交換位置,再從剩下的記錄中繼續反復這個過程,直到全部有序。

具體過程:

首先通過 n –1 次關鍵字比較,從 n 個記錄中找出關鍵字最小的記錄,將它與第一個記錄交換。

再通過 n –2 次比較,從剩余的 n –1 個記錄中找出關鍵字次小的記錄,將它與第二個記錄交換。

重復上述操作,共進行 n –1 趟排序后,排序結束。

如圖

 

過程圖解

令 k=i;j = i + 1;

目的是使用 k 找出剩余的 n-1個記錄中,一趟排序的最值,如果 j 記錄小於 k 記錄,k=j;

繼續比較,j++,如果 j 記錄不小於 k 記錄,繼續 j++,這里使用 k 來找出一趟排序的最值

直到 j=n 為止

交換k 記錄和第 i 個記錄(i 從頭開始的),然后 i++,進行下一趟選擇排序過程

整個過程圖示

直到無序序列為0為止

代碼如下:

 1 //簡單選擇遞增排序
 2 void selectSort(int List[], int len)
 3 {
 4     //簡單選擇排序的循環
 5     for (int i = 0; i < len; i++) {
 6         int k = i;
 7         //一次排序過程,終止條件是 j 掃描到了最后一個記錄處
 8         for (int j = i + 1; j <= len; j++) {
 9             if (List[j] < List[k]) {
10                 k = j;
11             }
12         }
13         //掃描完畢,交換最值,先判斷是否重復
14         if (i != k) {
15             //交換
16             List[i] = List[i] + List[k];
17             List[k] = List[i] - List[k];
18             List[i] = List[i] - List[k];
19         }// end of if
20     }//end of for
21 }
22 
23 int main(void)
24 {
25     
26     int source[7] = {49, 38, 65, 97, 76, 13, 27};
27     
28     selectSort(source, 7);
29     
30     for (int i = 1; i < 8; i++) {
31         printf(" %d  ", source[i]);
32     }
33 
34     return 0;
35 }

 13   27   38   49   65   76   97  Program ended with exit code: 0

空間復雜度:O(1)

比較次數:

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

簡單選擇排序的穩定性:不穩定

 

錦標賽排序和樹形選擇排序

錦標賽排序也叫樹形選擇排序,是一種按照錦標賽的思想進行選擇的排序方法,該方法是在簡單選擇排序方法上的改進。簡單選擇排序,花費的時間大部分都浪費在值的比較上面,而錦標賽排序剛好用樹保存了前面比較的結果,下一次比較時直接利用前面比較的結果,這樣就大大減少比較的時間,從而降低了時間復雜度,由O(n^2)降到O(nlogn),但是浪費了比較多的空間,“最大的值”也比較了多次。

大概過程如下:

首先對n個記錄進行兩兩比較,然后優勝者之間再進行兩兩比較,如此重復,直至選出最小關鍵字的記錄為止。

類似甲乙丙三隊比賽,前提是有這樣一種傳遞關系:若乙勝丙,甲勝乙,則認為甲必能勝丙。

錦標賽排序圖解如下

初始序列,這么多隊伍參加比賽

兩兩比較之,用一個完全二叉樹表示,反復直到一趟比較后,選出冠軍

找到了 bao,是冠軍,選出冠軍的比較次數為   2^2+2^1+2^0 = 2^3 -1 = n-1,然后繼續比較,把原始序列的 bao 去掉

選了 cha,選出亞軍的比較次數為 3,即 log2 n 次。 同理,把 cha 去掉,繼續兩兩比較

找到了 diao,其后的 n-2 個人的名次均如此產生

所以對於 n 個參賽選手來說,即對 n 個記錄進行錦標賽排序,總的關鍵字比較次數至多為  (n-1)log2 n + n -1,故時間復雜度為: O(nlogn)。

此法除排序結果所需的 n 個單元外,尚需 n-1 個輔助單元。 

這個過程可用一棵有n個葉子結點的完全二叉樹表示,根節點中的關鍵字即為葉子結點中的最小關鍵字。在輸出最小關鍵字之后,根據關系的可傳遞性,欲選出次小關鍵字, 僅需將葉子結點中的最小關鍵字改為“最大值”,如∞,然后從該葉子結點開始,和其左(右)兄弟的關鍵字進行比較,修改從葉子結點到根的路徑上各結點的關鍵字,則根結點的關鍵字即為次小關鍵字。也就是所謂的樹形選擇排序,這種算法的缺點在於:輔助存儲空間較多、最大值進行多余的比較。

 

樹形選擇排序

思想:首先對 n 個記錄的關鍵字進行兩兩比較,然后在其中 不大於 n/2  的整數個較小者之間再進行兩兩比較,直到選出最小關鍵字的記錄為止。可以用一棵有 n 個葉子結點的完全二叉樹表示。

樹形選擇排序圖解如下:

對 n 個關鍵字兩兩比較,直到選出最小關鍵字為止,一趟排序結束

       

反復這個過程,僅需將葉子結點的最小關鍵字改為最大值∞,即可

然后從該葉子結點開始,繼續和其左右兄弟的關鍵字比較,找出最值

時間復雜度:由於含有 n 個葉子結點的完全二叉樹的深度為,則在樹形選擇排序中,除了最小關鍵字外,每選擇一個次小關鍵字僅需進行   次比較,故時間復雜度為 O(n logn)。

缺點:  1、與“∞”的比較多余;  2、輔助空間使用多。

為了彌補這些缺點,1964年,堆排序誕生。

 

堆排序

堆的定義:n 個元素的序列 (k1, k2, …, kn),當且僅當滿足下列關系:任何一個非終端結點的值都大於等於(或小於等於)它左右孩子的值時,稱之為堆。若序列{k1,k2,…,kn}是堆,則堆頂元素(即完全二叉樹的根)必為序列中n個元素的最小值(或最大值) 。

可將堆序列看成完全二叉樹,則: k2i 是 ki 的左孩子; k2i+1 是 ki 的右孩子。所有非終端結點的值均不大(小)於其左右孩子結點的值。堆頂元素必為序列中 n 個元素的最小值或最大值。

若:ki <= k2i  ,  ki <= k2i+1,也就是說父小孩大,則為小頂堆(小根堆,正堆),反之,父大孩小,叫大頂堆(大根堆,逆堆)

堆排序定義:將無序序列建成一個堆,得到關鍵字最小(大)的記錄;輸出堆頂的最小(大)值后,將剩余的 n-1 個元素重又建成一個堆,則可得到 n 個元素的次小值;如此重復執行,直到堆中只有一個記錄為止,每個記錄出堆的順序就是一個有序序列,這個過程叫堆排序。

堆排序需解決的兩個問題:

  1、如何由一個無序序列建成一個堆?

  2、在輸出堆頂元素后,如何將剩余元素調整為一個新的堆? 

第二個問題解決方法——篩選:

所謂“篩選”指的是,對一棵左/右子樹均為堆的完全二叉樹,“調整”根結點使整個二叉樹也成為一個堆。具體是:輸出堆頂元素之后,以堆中最后一個元素替代之;然后將根結點值與左、右子樹的根結點值進行比較,並與其中小者進行交換;重復上述操作,直至葉子結點,將得到新的堆,稱這個從堆頂至葉子的調整過程為“篩選”。

例: (13, 38, 27, 49, 76, 65, 49, 97) 

輸出堆頂元素之后,以堆中最后一個元素替代之;

然后將根結點值與左、右子樹的根結點值進行比較,並與其中小者進行交換

輸出堆頂元素之后,以堆中最后一個元素替代之;

然后將根結點值與左、右子樹的根結點值進行比較,並與其中小者進行交換

輸出堆頂元素之后,以堆中最后一個元素替代之;

然后將根結點值與左、右子樹的根結點值進行比較,並與其中小者進行交換

輸出堆頂元素之后,以堆中最后一個元素替代之;

然后將根結點值與左、右子樹的根結點值進行比較,並與其中小者進行交換

輸出堆頂元素之后,以堆中最后一個元素替代之;

然后將根結點值與左、右子樹的根結點值進行比較,並與其中小者進行交換

對深度為 k 的堆,“篩選”所需進行的關鍵字比較的次數至多為 2(k-1)。

第一個問題解決方法:從無序序列的第 個元素(即無序序列對應的完全二叉樹的最后一個內部結點)起,至第一個元素止,進行反復篩選。建堆是一個從下往上進行“篩選”的過程。把原始的序列一一對應的(從左到右,從上到下)建立一個完全二叉樹即可,然后建立小頂堆(大頂堆)

建堆

調整,篩選過程

一趟堆排序完畢,選出了最值和堆里最后一個元素交換,繼續

第二趟堆排序完畢,選出了次最值和剩下的元素的最后一個元素交換

第三趟堆排序完畢,重復往復,這樣進行堆調整。

第四躺排序完畢,繼續

第五躺排序完畢

第六趟排序完畢

最后全部堆排序完畢

這是整個建堆,調整為小頂堆的過程,也就是遞增排序。具體是自上而下調整完全二叉樹里的關鍵字,使其成為一個大頂堆(遞減排序過程)

操作過程如下:

1)初始化堆:將R[1..n]構造為堆;

2)將當前無序區的堆頂元素R[1]同該區間的最后一個記錄交換,然后將新的無序區調整為新的堆。

對於堆排序,最重要的兩個操作就是構造初始堆和調整堆,其實構造初始堆事實上也是調整堆的過程,只不過構造初始堆是對所有節點都進行調整

調整堆的代碼如下:

 1 //堆排序的堆的調整過程
 2 // 已知 H.r[s..m]中記錄的關鍵字除 H.r[s] 之外均滿足堆的特征,本函數自上而下調整 H.r[s] 的關鍵字,使 H.r[s..m] 成為一個大頂堆
 3 void heapAdjust(int List[], int s, int length)
 4 {
 5     //s 為 當前子樹 的 臨時 堆頂,先把堆頂暫存到 temp
 6     int maxTemp = s;
 7     //s 結點 的 左孩子 2 * s , 2 * s + 1是 s結點 的右孩子,這是自上而下的篩選過程,length是序列的長度
 8     int sLchild = 2 * s;
 9     int sRchild = 2 * s + 1;
10     //完全二叉樹的葉子結點不需要調整,沒有孩子
11     if (s <= length / 2) {
12         //如果 當前 結點的左孩子比當前結點記錄值大,調整,大頂堆
13         if (sLchild <= length && List[sLchild] >  List[maxTemp]) {
14             //更新 temp
15             maxTemp = sLchild;
16         }
17         //如果 當前 結點的右孩子比當前結點記錄值大,調整,大頂堆
18         if (sRchild <= length && List[sRchild] > List[maxTemp]) {
19             maxTemp = sRchild;
20         }
21         //如果調整了就交換,否則不需要交換
22         if ( List[maxTemp] != List[s]) {
23             List[maxTemp] = List[maxTemp] + List[s];
24             List[s] = List[maxTemp] - List[s];
25             List[maxTemp] = List[maxTemp] - List[s];
26             //交換完畢,防止調整之后的新的以 maxtemp 為父節點的子樹不是大頂堆,再調整一次
27             heapAdjust(List, maxTemp, length);
28         }
29     }
30 }

建立堆的過程,本質還是堆調整的過程

 1 //建堆,就是把待排序序列一一對應的建立成完全二叉樹(從上到下,從左到右的順序填滿完全二叉樹),然后建立大(小)頂堆
 2 void bulidHeap(int List[], int length)
 3 {
 4     //明確,具有 n 個結點的完全二叉樹(從左到右,從上到下),編號后,有如下關系,設 a 結點編號為 i,若 i 不是第一個結點,那么 a 結點的雙親結點的編號為[i/2]
 5     //非葉節點的最大序號值為 length / 2
 6     for (int i = length / 2; i >= 0; i--) {
 7         //從頭開始調整為大頂堆
 8         heapAdjust(List, i, length);
 9     }
10 }

堆排序過程

 1 //堆排序過程
 2 void heapSort(int List[], int length)
 3 {
 4     //建大頂堆
 5     bulidHeap(List, length);
 6     //調整過程
 7     for (int i = length; i >= 1; i--) {
 8         //將堆頂記錄和當前未經排序子序列中最后一個記錄相互交換
 9         //即每次將剩余元素中的最大者list[0] 放到最后面 list[i]
10         List[i] = List[i] + List[0];
11         List[0] = List[i] - List[0];
12         List[i] = List[i] - List[0];
13         //重新篩選余下的節點成為新的大頂堆
14         heapAdjust(List, 0, i - 1);
15     }
16 }

測試數據

int source[8] = {49, 38, 65, 97, 76, 13, 27, 49};

 13   27   38   49   49   65   76   97  Program ended with exit code: 0

 

堆排序的時間復雜度和空間復雜度:

1.   對深度為 k 的堆,“篩選”所需進行的關鍵字比較的次數至多為 2(k-1);

2.   對 n 個關鍵字,建成深度為  的堆,所需進行的關鍵字比較的次數至多 4n;

3.   調整“堆頂” n-1 次,總共進行的關鍵字比較的次數不超過,因此,堆排序的時間復雜度為 O(nlogn),與簡單選擇排序 O(n^2)  相比時間效率提高了很多。 

空間復雜度:S(n) = O(1) 

堆排序是一種速度快且省空間的排序方法。相對於快速排序的最大優點:最壞時間復雜度和最好時間復雜度都是 O(n log n),適用於記錄較多的場景(記錄較少就不實用),類似折半插入排序,在 T(n)=O(n log n)的排序算法中堆排序的空間復雜度最小為1。

堆排序的穩定性:不穩定排序算法

 

歡迎關注

dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM