「這是我參與8月更文挑戰的第13天,活動詳情查看:8月更文挑戰」node
關注我,如下內容持續更新面試
數據結構與算法(二):桟api
數據結構與算法(四):單鏈表markdown
數據結構與算法(五):雙向鏈表數據結構
數據結構與算法(七):樹oop
數據結構與算法(八):排序算法post
在介紹堆排序以前,先簡單回顧下徹底二叉樹和堆
徹底二叉樹:若是二叉樹中除去最後一層節點爲滿二叉樹,且最後一層的結點依次從左到右分佈,則此二叉樹被稱爲徹底二叉樹。
堆是一棵順序存儲的徹底二叉樹,堆的存儲通常用數組來實現
大頂堆: 每一個結點的值都大於或等於其左右孩子結點的值
小頂堆: 每一個結點的值都小於或等於其左右孩子結點的值
關於徹底二叉樹和堆,具體的能夠看數據結構與算法(七):樹, 明天會更新
堆排序是一種選擇排序,能夠用一維數組順序存儲堆. 那麼如何利用堆進行排序呢?拿升序排序來說,先把待排序序列按照從上至下從左至右的順序構建成一個大頂堆,那麼大頂堆的根節點就是最大的元素,把根節點取出來,再把剩下的元素再構形成一個新堆(如何保證取走最大值更高效的構建新堆呢,就是把根節點arr[0]和最後一個葉子節點arr[n-1]進行調換,而後取走最後一個葉子節點arr[n-1],此時堆不知足大頂堆的條件,只能說它是一個徹底二叉樹,因此要遞歸調整徹底二叉樹使它成爲一個大頂堆),一樣知足根節點是最大的元素,此時再取出根節點,繼續構建新堆,循環執行.
例如把數組{100,33,3,7,11,6,8,5}進行升序排序的過程以下:
第一步:構建徹底二叉樹
首先根據數組創建一個徹底二叉樹,以下圖
(代碼中這一步無需操做,由於徹底二叉樹能夠用一維數組來存儲,初始數組默認就是一個徹底二叉樹)
第二步:把徹底二叉樹構建成大頂堆
從最後一個父節點開始,也就是從下標爲3的7開始調整,由於7做爲父節點,大於它的孩子節點5,因此不用調整;再看下標爲2的3,由於3小於孩子節點,把最大的孩子節點8與3進行交換;而後再看下標爲 1 的 33,它大於兩個孩子節點,不用交換;再看下標爲0的100,它大於孩子節點,不用交換;此時大頂堆已構建完成. 如圖所示
這裏須要注意:若是要構造完整的堆,要從下面往上構造,並且每次交換後,都要遞歸維護以"交換的那個孩子節點"爲父節點的下面的堆,由於若是交換的節點很是小,那麼可能小於它下面的堆,因此要遞歸向下維護堆,遞歸維護堆的函數在代碼中是heapify方法(heapify方法後面會貼代碼),heapify方法裏邊若是遞歸過程當中父節點比某個孩子節點小,那麼就交換位置,繼續從被交換的那個孩子節點的位置往下遞歸維護堆;若是遞歸過程當中父節點比兩個孩子節點都大,不用交換,固然這個父節點確定會大於孩子節點下邊的堆,因此不須要繼續遞歸下面的堆
第三步:取出最大值,並調整成爲新堆 這時大頂堆的堆頂元素是最大值,把它與最後一個元素進行交換,而後從堆中拿走最後一個元素,也就是最大值,以下圖
這時已經不知足堆的特色,只能說它是一個徹底二叉樹,因此此時要進行調整,注意調整堆和建立堆順序不同,調整堆應該從根節點開始調整,父節點和孩子節點交換後,下次遞歸調整的子堆就是以交換後的那個孩子爲父節點的子堆,例如這一步,從下標爲0的5開始調整,5和33交換後,下次遞歸就要調整下標爲1的5爲父節點的子堆,由於 5 比它的右孩子 11 大,因此交換位置(注意33 和 5 交換後,下標爲2的8做爲父節點的子堆不須要遞歸調整,由於交換後,以8爲父節點的子堆沒有動過,依然知足堆的特色)
此時已經調整好成爲新堆,再把對頂元素和最後一個元素交換位置,即 33 和 3 交換,而後把取走33,如圖
以此類推,中間的調整過程省略,最後的一次調整完如圖所示
每一次交換完堆頂元素和最後一個元素後,也就是數組的arr[0]和 arr[n-i],例如第一次交換就是 arr[0]和 arr[n-1],第二次交換就是 arr[0]和 arr[n-2],以此類推,最後一次調整完成後,數組的元素就是升序排列.
注意
取出最大值後,調整堆時,若是要構造完整的堆,也要調用heapify方法遞歸維護下面的堆,可是這裏說明一點,對於這裏的堆排序來說能夠不用遞歸維護,由於只須要保證堆頂元素是最大的就能夠,沒必要維護成爲完整的堆.
//8 堆排序
-(void)heapSort:(NSMutableArray*)arr{
//1. 構建大頂堆
[self buildHeap:arr];
int n = (int)arr.count;
//2. 依次取出最大值,而後繼續調整堆
for (int i = n - 1; i>=0; i--) {
[self swapArray:arr index1:0 index2:i];//取出最大值放到數組的最後,也就是把堆頂的元素與最後一個交換
[self heapify:arr parent:0 n:i];//下次堆的個數逐漸減1,由於最大值取出後,就要從堆中拿走
}
}
/**
1,針對結點 i,將其兩個子節點找出來,此三個結點構成一個最小單位的徹底二叉樹(越界的忽略)
2,找到這個最小單位的徹底二叉樹 的最大值,並將其交換至父節點的位置
3,遞歸調用,維護交換後 子節點與其子結點被破壞的堆關係,遞歸出口爲葉節點
*/
-(void)heapify:(NSMutableArray*)arr parent:(int)i n:(int)n{
int c1 = i*2+1;//左孩子節點下標
int c2 = i*2+2;//右孩子節點下標
int max = i;
//找出三個節點中的最大值的下標,記錄爲max
if (c1 < n && [arr[c1] intValue] > [arr[max] intValue]) {
max = c1;
}
if (c2 < n && [arr[c2] intValue] > [arr[max] intValue]) {
max = c2;
}
//max != i,也就是最大值不是原來的父節點,須要交換位置,可是交換後,被交換位置的新元素可能比下面的堆還小,因此須要遞歸向下維護堆
if (max != i) {
[self swapArray:arr index1:max index2:i];
[self heapify:arr parent:max n:n];
}
}
-(void)buildHeap:(NSMutableArray*)arr{
int n = (int)arr.count;
//徹底二叉樹的下標與數組的下標一一對應,因此徹底二叉樹的最後一個節點的下標爲last_node
int last_node = n - 1;
//最下面一個堆的父節點 根據求父節點的公式來的:p = (i-1)/2
int parent = (last_node-1)/2;
for (int i = parent; i>=0; i--) {
[self heapify:arr parent:i n:n];
}
}
複製代碼
堆排序的最好、最好和平均時間複雜度均爲O(nlogn),空間複雜度是 O(1), 它是不穩定的排序