選擇類的排序算法
簡單選擇排序算法
采用最簡單的選擇方式,從頭到尾掃描待排序列,找一個最小的記錄(遞增排序),和第一個記錄交換位置,再從剩下的記錄中繼續反復這個過程,直到全部有序。
具體過程:
首先通過 n –1 次關鍵字比較,從 n 個記錄中找出關鍵字最小的記錄,將它與第一個記錄交換。
再通過 n –2 次比較,從剩余的 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電子書,資料,幫忙內推,歡迎拍磚!