數據結構與算法八: 8)排序算法--堆排序

這是我參與8月更文挑戰的第13天,活動詳情查看:8月更文挑戰node

關注我,如下內容持續更新面試

數據結構與算法(一):時間複雜度和空間複雜度算法

數據結構與算法(二):桟api

數據結構與算法(三):隊列數組

數據結構與算法(四):單鏈表markdown

數據結構與算法(五):雙向鏈表數據結構

數據結構與算法(六):哈希表函數

數據結構與算法(七):樹oop

數據結構與算法(八):排序算法post

數據結構與算法(九):經典算法面試題

前言

在介紹堆排序以前,先簡單回顧下徹底二叉樹和堆

  • 徹底二叉樹:若是二叉樹中除去最後一層節點爲滿二叉樹,且最後一層的結點依次從左到右分佈,則此二叉樹被稱爲徹底二叉樹。

  • 是一棵順序存儲徹底二叉樹,堆的存儲通常用數組來實現

    • 大頂堆: 每一個結點的值都大於或等於其左右孩子結點的值

    • 小頂堆: 每一個結點的值都小於或等於其左右孩子結點的值

關於徹底二叉樹和堆,具體的能夠看數據結構與算法(七):樹, 明天會更新

堆排序思路

堆排序是一種選擇排序,能夠用一維數組順序存儲堆. 那麼如何利用堆進行排序呢?拿升序排序來說,先把待排序序列按照從上至下從左至右的順序構建成一個大頂堆,那麼大頂堆的根節點就是最大的元素,把根節點取出來,再把剩下的元素再構形成一個新堆(如何保證取走最大值更高效的構建新堆呢,就是把根節點arr[0]和最後一個葉子節點arr[n-1]進行調換,而後取走最後一個葉子節點arr[n-1],此時堆不知足大頂堆的條件,只能說它是一個徹底二叉樹,因此要遞歸調整徹底二叉樹使它成爲一個大頂堆),一樣知足根節點是最大的元素,此時再取出根節點,繼續構建新堆,循環執行.

例如把數組{100,33,3,7,11,6,8,5}進行升序排序的過程以下:

第一步:構建徹底二叉樹

首先根據數組創建一個徹底二叉樹,以下圖

(代碼中這一步無需操做,由於徹底二叉樹能夠用一維數組來存儲,初始數組默認就是一個徹底二叉樹)

徹底二叉樹.png

第二步:把徹底二叉樹構建成大頂堆

從最後一個父節點開始,也就是從下標爲3的7開始調整,由於7做爲父節點,大於它的孩子節點5,因此不用調整;再看下標爲2的3,由於3小於孩子節點,把最大的孩子節點8與3進行交換;而後再看下標爲 1 的 33,它大於兩個孩子節點,不用交換;再看下標爲0的100,它大於孩子節點,不用交換;此時大頂堆已構建完成. 如圖所示 截屏2021-08-11 19.35.14.png

這裏須要注意:若是要構造完整的堆,要從下面往上構造,並且每次交換後,都要遞歸維護以"交換的那個孩子節點"爲父節點的下面的堆,由於若是交換的節點很是小,那麼可能小於它下面的堆,因此要遞歸向下維護堆,遞歸維護堆的函數在代碼中是heapify方法(heapify方法後面會貼代碼),heapify方法裏邊若是遞歸過程當中父節點比某個孩子節點小,那麼就交換位置,繼續從被交換的那個孩子節點的位置往下遞歸維護堆;若是遞歸過程當中父節點比兩個孩子節點都大,不用交換,固然這個父節點確定會大於孩子節點下邊的堆,因此不須要繼續遞歸下面的堆

第三步:取出最大值,並調整成爲新堆 這時大頂堆的堆頂元素是最大值,把它與最後一個元素進行交換,而後從堆中拿走最後一個元素,也就是最大值,以下圖

截屏2021-08-11 19.41.55.png

這時已經不知足堆的特色,只能說它是一個徹底二叉樹,因此此時要進行調整,注意調整堆和建立堆順序不同,調整堆應該從根節點開始調整,父節點和孩子節點交換後,下次遞歸調整的子堆就是以交換後的那個孩子爲父節點的子堆,例如這一步,從下標爲0的5開始調整,5和33交換後,下次遞歸就要調整下標爲1的5爲父節點的子堆,由於 5 比它的右孩子 11 大,因此交換位置(注意33 和 5 交換後,下標爲2的8做爲父節點的子堆不須要遞歸調整,由於交換後,以8爲父節點的子堆沒有動過,依然知足堆的特色)

此時已經調整好成爲新堆,再把對頂元素和最後一個元素交換位置,即 33 和 3 交換,而後把取走33,如圖

截屏2021-08-11 19.57.34.png

以此類推,中間的調整過程省略,最後的一次調整完如圖所示

截屏2021-08-11 20.00.55.png

每一次交換完堆頂元素和最後一個元素後,也就是數組的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), 它是不穩定的排序

其餘排序算法

排序算法:1)直接插入排序

排序算法:2)希爾排序

排序算法:3)冒泡排序

排序算法:4)快速排序

排序算法:5)選擇排序

排序算法:6)歸併排序

排序算法:7)基數排序

排序算法:8)堆排序

相關文章
相關標籤/搜索