一.堆排序的優缺點(pros and cons)html
(仍是簡單的說說這個,畢竟沒有必要浪費時間去理解一個糟糕的的算法)算法
優勢:數組
缺點:(從上面看,堆排序幾乎是完美的,那麼爲何最經常使用的內部排序算法是快排而不是堆排序呢?)優化
二.內部原理spa
首先要知道堆排序的步驟:htm
按小根堆排序結果是降序(或者說是非升序,不要在乎這種細節..),按大根堆排序的結果是升序blog
上面這句話乍看好像不對(小根堆中最小元素在堆頂,數組組堆頂元素就是a[0],怎麼會是降序?),不過不用質疑這句話的正確性,看了下面這幾幅圖就明白了:排序
假設待排序序列是a[] = {7, 1, 6, 5, 3, 2, 4},而且按大根堆方式完成排序get
{7, 5, 6, 1, 3, 2, 4}已經知足了大根堆,第一步完成it
無圖,眼睛畫瞎了,mspaint實在很差用。。到第二步應該差很少了吧,剩下的用筆也就畫出來了。。
其實核心就是「斷尾」,但可悲的是全部的資料上都沒有明確說出來,但是,還有比「斷尾」更貼切的描述嗎?
三.實現細節
原理介紹中給出的圖基本上也說清楚了實現細節,因此這裏只關注代碼實現
#include<stdio.h> //構造大根堆(讓a[m]到a[n]知足大根堆) void HeapAdjust(int a[], int m, int n){ int temp; int max; int lc;//左孩子 int rc;//右孩子 while(1){ //獲取a[m]的左右孩子 lc = 2 * m + 1; rc = 2 * m + 2; //比較a[m]的左右孩子,max記錄較大者的下標 if(lc >= n){ break;//不存在左孩子則跳出 } if(rc >= n){ max = lc;//不存在右孩子則最大孩子爲左孩子 } else{ max = a[lc] > a[rc] ? lc : rc;//左右孩子都存在則找出最大孩子的下標 } //判斷並調整(交換) if(a[m] >= a[max]){//父親比左右孩子都大,不須要調整,直接跳出 break; } else{//不然把小父親往下換 temp = a[m]; a[m] = a[max]; a[max] = temp; //準備下一次循環,注意力移動到孩子身上,由於交換以後以孩子爲根的子樹可能不知足大根堆 m = max; } } } void HeapSort(int a[], int n){ int i,j; int temp; //自下而上構造小根堆(初始堆) for(i = n / 2 - 1;i >= 0;i--){//a[n/2 - 1]剛好是最後一個非葉子節點(葉子節點已經知足小根堆,只須要調整全部的非葉子節點),一點小小的優化 HeapAdjust(a, i, n); } printf("初始堆: "); for(i = 0;i < n;i++){ printf("%d ", a[i]); } printf("\n"); for(i = n - 1;i > 0;i--){ //首尾交換,斷掉尾巴 temp = a[i]; a[i] = a[0]; a[0] = temp; //斷尾後的部分從新調整 HeapAdjust(a, 0, i); /* printf("第%d次(i - 1 = %d): ", n - i, i - 1); for(j = 0;j < n;j++){ printf("%d ", a[j]); } printf("\n"); */ } } main(){ //int a[] = {5, 6, 3, 4, 1, 2, 7}; //int a[] = {1, 2, 3, 4, 5, 6, 7}; //int a[] = {7, 6, 5, 4, 3, 2, 1}; int a[] = {7, 1, 6, 5, 3, 2, 4}; int m, n; int i; m = 0; n = sizeof(a) / sizeof(int); //HeapAdjust(a, m, n); HeapSort(a, n); printf("結果: "); for(i = 0;i < n;i++){ printf("%d ", a[i]); } printf("\n"); }
P.S.代碼中註釋極其詳盡,由於是徹底一步一步本身想着寫出來的,應該不難理解。看代碼說話,在此多說無益。
#include<stdio.h> void HeapAdjust(int a[], int m, int n){ int i; int t = a[m]; for(i = 2 * m + 1;i <= n;i = 2 * i + 1){ if(i < n && a[i + 1] > a[i])++i; if(t >= a[i])break; //把空缺位置往下放 a[m] = a[i]; m = i; } a[m] = t;//只作一次交換,步驟上的優化 } void HeapSort(int a[], int n){ int i; int t; //自下而上構造大根堆 for(i = n / 2 - 1;i >= 0;--i){ HeapAdjust(a, i, n - 1); } printf("初始堆: "); for(i = 0;i < n;i++){ printf("%d ", a[i]); } printf("\n"); for(i = n - 1;i > 0;i--){ //首尾交換,斷掉尾巴 t = a[i]; a[i] = a[0]; a[0] = t; //對斷尾後的部分從新建堆 HeapAdjust(a, 0, i - 1); } } main(){ //int a[] = {5, 6, 3, 4, 1, 2, 7}; //int a[] = {1, 2, 3, 4, 5, 6, 7}; //int a[] = {7, 6, 5, 4, 3, 2, 1}; int a[] = {7, 1, 6, 5, 3, 2, 4}; int m, n; int i; m = 0; n = sizeof(a) / sizeof(int); //HeapAdjust(a, m, n); HeapSort(a, n); printf("結果: "); for(i = 0;i < n;i++){ printf("%d ", a[i]); } printf("\n"); }
P.S.書本上的代碼短了很多,不只僅是篇幅上的優化,也有實實在在的步驟上的優化,細微差異也在註釋中說明了。但這種程度的優化卻使得代碼的可讀性大大下降,因此一次次拿起算法書,又一次次放下。。(實際應用中咱們能夠對書本上的代碼作形式上的優化,在保持其高效性的同時儘量的提高其可讀性。。)
#include<stdio.h> //構造小根堆(讓a[m]到a[n]知足小根堆) void HeapAdjust(int a[], int m, int n){ int i; int t = a[m]; int temp; for(i = 2 * m + 1;i <= n;i = 2 * i + 1){ //a[m]的左右孩子比較,i記錄較小者的下標 if(i < n && a[i + 1] < a[i]){ i = i + 1; } if(t <= a[i]){ break; } else{//把空缺位置往下換 //把較小者換上去 temp = a[m]; a[m] = a[i]; a[i] = temp; //準備下一次循環 m = i; } } } void HeapSort(int a[], int n){ int i, j; int temp; //自下而上構造小根堆(初始堆) for(i = n / 2 - 1;i >= 0;i--){//a[n/2 - 1]剛好是最後一個非葉子節點(葉子節點已經知足小根堆,只須要調整全部的非葉子節點),一點小小的優化 HeapAdjust(a, i, n); } printf("初始堆: "); for(i = 0;i < n;i++){ printf("%d ", a[i]); } printf("\n"); //把每一個元素都調整到應該去的位置 for(i = n - 1; i > 0;i--){ //首尾交換 temp = a[i]; a[i] = a[0]; a[0] = temp; //斷尾後剩餘部分從新調整 HeapAdjust(a, 0, i - 1); } } main(){ //int a[] = {7, 6, 5, 4, 3, 2, 1}; //int a[] = {1, 5, 6, 4, 3, 2, 7}; int a[] = {1, 2, 3, 4, 5, 6, 7}; int m, n; int i; m = 0; n = sizeof(a) / sizeof(int); //HeapAdjust(a, m, n); HeapSort(a, n); printf("結果: "); for(i = 0;i < n;i++){ printf("%d ", a[i]); } printf("\n"); }
P.S.註釋依然詳盡,看代碼,不廢話
四.總結
堆排序的步驟就幾個字而已:建堆 -> 首尾交換,斷尾重構 -> 重複第二步,直到斷掉全部尾巴
還有比這更清晰更明瞭的描述嗎?
到如今咱們已經掌握了幾個有用的排序算法了:
那麼實際應用中要如何選擇呢?有這些選擇標準:
說明:在理解「斷尾」的過程當中參考了前輩的博文,特此感謝