引言
相對於其他的排序算法,堆排序可以說算數比較難理解的,而且學習堆排序之前排序提前學習堆的定義。
不過不用擔心,這篇文章會用通俗易懂的方式讓你儘可能的學會堆排序!!!
本文將從以下幾個問題對堆排序進行分析和講解:
- 預備知識:堆是什麼?
- 堆排序是什麼?(★重要★)
- 堆排序的具體過程是什麼?(★★★重要★★★)
- 堆排序的代碼實現。
- 堆排序的代碼詳解。
一、堆是什麼?
說起堆,不得不的說起二叉樹。先看二叉樹的定義
百度百科的二叉樹定義:
二叉樹(Binary tree)是樹形結構的一個重要類型。許多實際問題抽象出來的數據結構往往是二叉樹形式,即使是一般的樹也能簡單地轉換爲二叉樹,而且二叉樹的存儲結構及其算法都較爲簡單,因此二叉樹顯得特別重要。二叉樹特點是每個結點最多隻能有兩棵子樹,且有左右之分 。
簡單來說,二叉樹就是每個結點能分出兩個叉(所以叫二叉樹)。下面看一個二叉樹的圖。
二叉樹的幾個名詞,就拿結點B來說,結點A是結點B的雙親結點,結點D是結點B的子結點(左)。
葉子結點點是指沒有孩子結點的結點,上圖的G,E,H都是葉子結點
其實堆就是和二叉樹有關,下面看圖
上面這個圖可以看出來,不論哪個結點,它的子結點都比他小(葉子結點除外);不論哪個結點,它的雙親結點都比他大(根結點除外)
所以上面的這個又叫大頂堆(最大的數在最上面),大頂堆是用來解決從小到大排序的(注意),本文講解的就是從小到大排序。
上面這個圖可以看出來,不論哪個結點,它的孩子結點都比他大(葉子結點除外);不論哪個結點,它的雙親結點都比他小(根結點出外)
所以上面的這個又叫小頂堆(最小的數在最上面),小頂堆是用來解決從大到小排序的(注意)。
注意看上圖的角標,它的編號方式就是從上到下,從左到右,依次遞增,如果有缺的的也算數(比如角標爲2的結點就有沒有右孩子,但是它的右孩子角標就是5)。
重要結論:
上面的角標存在一個關係;對某個角標爲 i 的結點 。如果存在左孩子和右孩子,那麼它的角標分別是 2*i 和 2*i+1
如果他的雙親結點存在,那麼他的雙親結點的角標就是 i/2
截止到現在爲止,必須要理解什麼是大頂堆,什麼是小頂堆,對於某個結點,他和他的雙親結點、孩子結點有什麼關係。
二、什麼是堆排序?
說起堆排序,我們目標就是對給定的數組進行排序,這個排序方法採取堆的形式進行。所有我們堆排序分以下幾步:
- 把我們要排序的數組構造成一個大頂堆(從小到大排序),這裏要注意,我們一開始有一個數組,數組有下標,這裏的下標就和我們的角標一樣。所以我們如果一旦有了一個數組,其實就有了一個堆,只不過這個對可能不滿足最大堆或者最小堆的情況。假如我們要排序的數組是20,30,50,40,10,70,60,80,那我們就有下面這個堆。
- 既然我們要對這個數組從小到大排序,那就把現有的堆變成大頂堆(至於爲啥是大頂堆後面說)。
- 變成大頂堆之後,堆定元素就是最大值,再把這個最大值和這個數組的最後一個元素交換位置,這樣一來數組的最大值就放到了數組的尾部。再看這個堆(這時候這個堆不包括數組的最後一個元素了),這時候堆就不是最大堆了,又把這個堆需要變成最大堆,等變成最大堆之後,再把堆頂元素放到數組的倒數第二個位置。
- 重複上面的操作,就可以實現數組的從小到大排序了。
- 如果採取的是小頂堆,那麼每次是把最小的數放到數組後面,重複這個操作,就可以實現從大到小排序了
三、堆排序的具體過程是什麼?
在概括一下上面說的堆排序的步驟,其實就分兩大步:
- 第一步,把這個堆變成大頂堆
- 第二步,把最大元素放到數組尾部,再重複第一步
下面先看第一步,假設我們要排序的數組爲 20,30,50,40,10,70,60,80。(爲了方便,數組下標從1開始)
上面是我們要排序的數組,下面是數組對應的堆。既然要變成大頂堆,就要最大的數移動到頂部,
那我們的一個堆怎樣變成大頂堆呢,那我們就可以從根結點開始遍歷,如果這個根結點的左孩子和右孩子有比根結點大的,那就選擇一個較大的孩子,和這個根結點交換值,這樣就達到較大的值在根部了。
上面的根結點函數就是可以把一個結點的值和他的孩子結點比較,如果孩子結點大於他,然後交換值。但是他有限制,他只能和他的孩子結點交換,並不能和他孩子結點的孩子結點交換, 所以通過調用這一個函數並不能直接把一個任意的堆變成大頂堆,這個函數能實現的功能就是把某個結點和他的對應的孩子結點交換,如果交換成功,孩子結點還可以和孩子的孩子結點交換,達到一個下沉的效果。下面看這個函數代碼:
void HeapAdjust(int arr[],int first,int last) { int temp=arr[first];//暫存“根”結點 int j;//子結點 for(j=2*first;j<=last;j=j*2) { //下面if語句的作用是找出子結點中比較大的那個 //j是左節點,j+1是右節點, //如果右節點大,那j+1就可以了,如果左節點大那就不用+1 //執行完下面的語句,j下標是較大的那個子結點的下標 if(j<last &&arr[j]<arr[j+1]) j++; //下面if語句的作用是如果“根”結點大於子結點, //結束查找即可 if(temp>arr[j]) break; //理解下面兩條語句可以類比插入排序, //還記得插入排序中的元素後移嗎? 這裏是“下移” arr[first]=arr[j]; first=j; //如果下移,記錄對應的下標,方便下次下移 } //同樣類比插入排序,把要插入的元素,放到合適的位置 //不過這個first會在循環中會更新 arr[first]=temp; }
截止到現在爲止,我們要知道這個函數的功能,更要知道這個函數的限制(不能直接把任意一個堆變成大頂堆)。
上面的所有就是實現大頂堆的函數,但是它並不能完成實現大頂堆,下面看第二步
第二步也是寫一個函數來實現把最大元素放到數組尾部,重複第一步,但是這執行把最大元素放到數組尾部的時候,要處理第一步留下的問題。
解決辦法:
我們首先看上面的圖,我們函數的功能是可以交換一個結點和他的孩子結點的值,那麼我們再看上圖的所有綠色的葉子結點。
如果我們從這些綠色的葉子結點的根結點(也可說上圖的非葉子結點,白色的圓圈)都使用一遍那個函數(從角標大的開始,因爲角標大的可能作爲角標小的孩子結點),那麼我們是不是就可以實現從任意一個堆到大頂堆的轉換了。
比如從角標爲4的葉子結點使用大頂堆函數,那麼40就會和80互換位置,
再對角標爲2和角標3(他的兩個孩子結點是80和10,而不是交換之前的40和10)的結點使用大頂堆函數,那50就會和70(70比60大,選擇孩子結點中較大的)交換位置,20和80交換位置
再對角標爲1的結點使用大頂堆函數,20就會和80交換位置。
經過上面的步驟就實現了堆向大頂堆轉換(注意大頂堆的頂部最大,對其他位置沒有要求)。
上面的步驟就可以用下面的三行代碼實現。(注意是從非葉子結點的角標最大值開始,往角標小的遍歷)
注意,上面的非葉子結點的角標最大值是數值最後一個元素角標除以2得到的。遍歷下面代碼即可。
for(int i=len/2;i>0;i--) { HeapAdjust(arr,i,len); }
下面就需要把大頂堆的元素和這棵樹的最後一個元素交換位置就可了,等到交換完位置,這時候這個堆就不是大頂堆了,然後調用一次大頂堆函數,就又可以把大頂堆變換出來了(想想爲啥這時候調用一個函數就可以,初始的堆調 函數爲啥就不可以變成大頂堆),下面看代碼
void HeapSort(int arr[],int len) { for(int i=len/2;i>0;i--)//把堆變成大頂堆 { HeapAdjust(arr,i,len); } for(int i=len;i>0;i--)//需要交換幾次位置的次數 { //下面三行的代碼是把堆頂最大的元素和堆尾最後一個元素換位置 //這樣一來,最大元素就在數組尾部了, //因此大頂堆 是用來從小 到大排序的 int temp=arr[1]; arr[1]=arr[i]; arr[i]=temp; //對堆剩下的元素繼續排序。 HeapAdjust(arr,1,i-1); } }
四、堆排序的代碼實現
下面看完整的代碼
#include<iostream> using namespace std; //堆排序函數 穩定 void HeapAdjust(int arr[],int first,int last) { int temp=arr[first];//暫存“根”結點 int j;//子結點 for(j=2*first;j<=last;j=j*2) { //下面if語句的作用是找出子結點中比較大的那個 //j是左節點,j+1是右節點, //如果右節點大,那j+1就可以了,如果左節點大那就不用+1 //執行完下面的語句,j下標是較大的那個子結點的下標 if(j<last &&arr[j]<arr[j+1]) j++; //下面if語句的作用是如果“根”結點大於子結點, //結束查找即可 if(temp>arr[j]) break; //理解下面兩條語句可以類比插入排序, //還記得插入排序中的元素後移嗎? 這裏是“下移” arr[first]=arr[j]; first=j; //如果下移,記錄對應的下標,方便下次下移 } //同樣類比插入排序,把要插入的元素,放到合適的位置 arr[first]=temp; } void HeapSort(int arr[],int len) { for(int i=len/2;i>0;i--) { HeapAdjust(arr,i,len); } for(int i=len;i>0;i--)//需要交換幾次位置的次數 { //下面三行的代碼是把堆頂最大的元素和堆尾最後一個元素換位置 //這樣一來,最大元素就在數組尾部了, //因此大頂堆 是用來從小 到大排序的 int temp=arr[1]; arr[1]=arr[i]; arr[i]=temp; //對堆剩下的元素繼續排序。 HeapAdjust(arr,1,i-1); } } //輸出數組的值 void printf(int arr[],int len) { for(int i=1;i<=len;i++) cout<<arr[i]<<" "; cout<<endl; } int main() { //要排序的數組 ,爲了方便理解,數組下標從1開始 int arr[]={0,3, 44,38, 5,47,15,36,26,27,2 ,46,4 ,19,50,48}; int len=15;//要排序的數組長度 //排序 HeapSort(arr,len); //輸出 printf(arr,len); return 0; }
運行結果:
五、堆排序的代碼詳解
- 首先是要了解轉換大頂堆的函數,在符合條件的情況下,他是一個可以把根結點逐漸下稱的過程。並不能可以把隨便一個堆變成大頂堆
- 懂得怎樣把隨便一個堆變成大頂堆
- 交換堆頂元素和堆的最後一個元素,在調用大頂堆函數
本文參考以及引用:
如果對其他的算法還有興趣,可以點擊下面的鏈接。
創作不易,如果本文對你起到了一些幫助,何不點個贊再走呢!!!