十大排序算法第二彈:從堆到堆排序

一、何為“堆”

(一)基本概念
1、滿二叉樹

?一個二叉樹,如果每一個層的結(jié)點數(shù)都達(dá)到最大值,則這個二叉樹就是滿二叉樹。也就是說,如果一個二叉樹的層數(shù)為k,且結(jié)點總數(shù)是(2k) -1 ,則它就是滿二叉樹。如下圖所示例子:

滿二叉樹

2、完全二叉樹

?完全二叉樹的結(jié)點數(shù)是任意的,從形式上講它是個缺失的的三角形,但所缺失的部分一定是右下角某個連續(xù)的部分,最后那一行可能不是完整的,對于k層的完全二叉樹,結(jié)點數(shù)的范圍2(k-1)-1 < N< 2k – 1;
?設(shè)二叉樹的深度為h,除第 h 層外,其它各層 (1~h-1) 的結(jié)點數(shù)都達(dá)到最大個數(shù),第 h 層所有的結(jié)點都連續(xù)集中在最左邊,這就是完全二叉樹。如下圖所示例子:

完全二叉樹

?有關(guān)二叉樹的知識,小編并沒有很系統(tǒng)的去總結(jié),接下來也將做一個深入分享。

(二)堆的定義及其分類
1、 定義

?堆(英語:heap)是計算機(jī)科學(xué)中一類特殊的數(shù)據(jù)結(jié)構(gòu)的統(tǒng)稱,是一顆完全二叉樹,通常是一個可以被看做一棵樹的數(shù)組對象
?堆是具有以下性質(zhì)的完全二叉樹:
?(1)每個結(jié)點的值都大于或等于其左右孩子結(jié)點的值,稱為大頂堆;
?(2)每個結(jié)點的值都小于或等于其左右孩子結(jié)點的值,稱為小頂堆。

2、二叉堆的分類

(1)大頂堆
?大頂堆中,每個結(jié)點的值都大于其左孩子和右孩子結(jié)點的值。我們來畫個圖演示下:

大頂堆
?對堆中的結(jié)點按層進(jìn)行編號,將這種邏輯結(jié)構(gòu)映射到數(shù)組中就是下面這個樣子:
大頂堆映射的數(shù)組

(2)小頂堆
?小頂堆中,每個結(jié)點的值都小于其左孩子和右孩子結(jié)點的值。同樣給個例子如下:
小頂堆
?對堆中的結(jié)點按層進(jìn)行編號,將這種邏輯結(jié)構(gòu)映射到數(shù)組中就是下面這個樣子:
小頂堆映射的數(shù)組

(三)堆結(jié)點訪問

?通常堆是通過一維數(shù)組來實現(xiàn)的。在數(shù)組起始位置為0的情形中,父結(jié)點和子結(jié)點的位置關(guān)系如下:
?(1)索引為i的左孩子的索引是 (2* i+1)
?(2)索引為i的右孩子的索引是 (2* i+2)
?(3)索引為i的父結(jié)點的索引是 (i -1)/2
?注意:這里得到的關(guān)系由數(shù)組的起始位置來決定。小伙伴們可以對照上述大頂堆和小頂堆的圖來分析分析索引的對應(yīng)關(guān)系。
?因此,在第一個元素的索引為0時:
?對于大頂堆有:arr(i)>arr(2* i+1) && arr(i)>arr(2* i+2)
?對于小頂堆有:arr(i)<arr(2* i+1) && arr(i)<arr(2* i+2)

二、構(gòu)造初始堆(堆調(diào)整)

?將無序數(shù)組構(gòu)造成一個堆(升序用大頂堆,降序用小頂堆)。假設(shè)存在以下數(shù)組:

無序數(shù)組
?我們先將它轉(zhuǎn)換成一棵樹,如下圖所示:
數(shù)組轉(zhuǎn)換成樹結(jié)構(gòu)

?首先,我們找到當(dāng)前這棵樹的最后一個非葉子結(jié)點,對從該結(jié)點開始的所有非葉子結(jié)點進(jìn)行結(jié)構(gòu)判斷,并依次進(jìn)行調(diào)整。注意,這里是自底向上,從右至左對非葉子結(jié)點進(jìn)行判斷。但對于每個結(jié)點的調(diào)整而言,其實是向下調(diào)整。

(一)調(diào)整成大頂堆
第一步

?找到最后一個非葉子結(jié)點為“數(shù)組長度/2-1=5/2-1=1【索引i=1】”,也就是上圖中的結(jié)點36。,開始進(jìn)行判斷該子樹是否滿足堆的性質(zhì)。
?可以發(fā)現(xiàn)以該結(jié)點為根結(jié)點的子樹不滿足大頂堆的結(jié)構(gòu),找到子結(jié)點元素最大的那一個【兩個子結(jié)點為arr[i* 2+1]與arr[i* 2+2],也就是arr[3]與arr[4]】,進(jìn)行調(diào)整,如下圖所示:

36和47交換
?換下后的結(jié)點屬于葉子結(jié)點,無需再進(jìn)行判斷是否滿足堆的性質(zhì)。

第二步

?繼續(xù)找到上一個非葉子結(jié)點,也就是結(jié)點18【索引i=0】,按照相同方法進(jìn)行調(diào)整。


47和18交換

?結(jié)點調(diào)換之后,需要繼續(xù)判斷換下后的結(jié)點是否符合堆的特性【也就是以當(dāng)前結(jié)點18為根的子樹】。發(fā)現(xiàn)不符合,繼續(xù)調(diào)整。此時結(jié)點18的索引i=1,從其左右孩子結(jié)點中選擇最大的一個進(jìn)行調(diào)換,結(jié)果如下:


40和18交換
第三步

?發(fā)現(xiàn)沒有非葉子結(jié)點了,這時調(diào)整結(jié)束。此時得到的就是最大堆。
?仔細(xì)分析,發(fā)現(xiàn)可以通過一個外層的循環(huán)從最后一個非葉子結(jié)點開始進(jìn)行遍歷【所有非葉子結(jié)點】,注意這里非葉子結(jié)點的訪問順序是自底向上。循環(huán)體用來判斷是否滿足堆的性質(zhì)來決定是否進(jìn)行調(diào)整。每次進(jìn)行調(diào)整時需要判斷交換后的結(jié)點是否也滿足堆的性質(zhì),不滿足要繼續(xù)進(jìn)行同樣的調(diào)整,這里是一個向下調(diào)整的過程??梢园l(fā)現(xiàn)這其實是一個遞歸的過程【當(dāng)然也可以采用迭代的方式實現(xiàn)】。因此代碼設(shè)計上可以采用“循環(huán)+遞歸”或者“循環(huán)+迭代”的方法。

示例代碼如下:
#include<iostream>
using namespace std;
/**
    * 遞歸實現(xiàn) 
    * 調(diào)整索引為 index 處的數(shù)據(jù),使其符合堆的特性。
    * @param arr 列表
    * @param index 需要堆化處理的數(shù)據(jù)的索引
    * @param len   未排序的堆(數(shù)組)的長度
*/
void AdjustDown(int *arr, int index, int len) 
{
    int li = (index * 2) + 1; // 左子節(jié)點索引
    int ri = li + 1;           // 右子節(jié)點索引
    int cMax = li;             // 子節(jié)點值最大索引,默認(rèn)左子節(jié)點。

    // 左子節(jié)點索引超出計算范圍,直接返回。
    if (li > len) 
    {
        return;
    }

    // 先判斷左右子節(jié)點,哪個較大。
    if (ri <= len && arr[li]<arr[ri]) 
    {
        cMax = ri;
    }
    //如果根結(jié)點小于葉子結(jié)點,交換 
    if (arr[index]<arr[cMax]) 
    {
        int item=arr[index];
        arr[index]=arr[cMax];
        arr[cMax]=item;
        AdjustDown(arr, cMax, len);  // 如果父節(jié)點被子節(jié)點調(diào)換,則需要繼續(xù)判斷換下后的父節(jié)點是否符合堆的特性。
    }
}

/**
    * 迭代實現(xiàn) 
    * 調(diào)整索引為 index 處的數(shù)據(jù),使其符合堆的特性。
    * @param arr 列表
    * @param index 需要堆化處理的數(shù)據(jù)的索引
    * @param len   未排序的堆(數(shù)組)的長度
*/
void maxHeapAdjust(int *arr, int index, int len) 
{
    int li = (index * 2) + 1; // 左子節(jié)點索引
    int ri = li + 1;           // 右子節(jié)點索引
    int item=arr[index];      //存儲該索引結(jié)點 
    while(li<len) 
    {
        int cMax = li;             // 子節(jié)點值最大索引,默認(rèn)左子節(jié)點。
        // 先判斷左右子節(jié)點,哪個較大。
        if (ri<=len && arr[li]<arr[ri]) 
        {
            cMax=ri;
        }
        //如果根結(jié)點小于葉子結(jié)點,交換 
        if (item>=arr[cMax]) 
        {
           break;
        }
        arr[li/2]=arr[cMax];//把子節(jié)點值賦值給父結(jié)點 
        li=cMax*2+1;//找到以cMax值為索引的結(jié)點的左孩子結(jié)點
        ri=li+1; 
    }
    arr[li/2]=item; //找到正確位置賦值 
}

int main()
{
    int len=15; 
    int arr[len]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
    int maxIndex = len - 1; //最大索引 
    int beginIndex = len/ 2 - 1;  //最后一個非葉子結(jié)點索引 
    /*
        *  將數(shù)組堆化
        *  beginIndex = 第一個非葉子節(jié)點。
        *  從第一個非葉子節(jié)點開始即可。無需從最后一個葉子節(jié)點開始。
        *  葉子節(jié)點可以看作已符合堆要求的節(jié)點,根節(jié)點就是它自己且自己以下值為最大。
        *  循環(huán)遍歷 
    */
    for (int i = beginIndex; i >= 0; i--) 
    {
       //maxHeapAdjust(arr, i, maxIndex);
       AdjustDown(arr, i, maxIndex);
    }
    
    for(int i=0;i<len;i++)
    {
        cout<<arr[i]<<"  ";
     } 
     return 0;
} 
(二)調(diào)整成小頂堆

?我們來把上述得到的最大堆來調(diào)整成最小堆。也就是下圖:
大頂堆
第一步

?找到最后一個非葉子結(jié)點為“數(shù)組長度/2-1=5/2-1=1【索引i=1】”,也就是上圖中的結(jié)點40。,開始進(jìn)行判斷該子樹是否滿足堆的性質(zhì)。
?可以發(fā)現(xiàn)以該結(jié)點為根結(jié)點的子樹不滿足最小堆的結(jié)構(gòu),找到子結(jié)點元素最小的那一個【兩個子結(jié)點為arr[i* 2+1]與arr[i* 2+2],也就是arr[3]與arr[4]】,進(jìn)行調(diào)整,如下圖所示:

40和18交換
?換下后的結(jié)點屬于葉子結(jié)點,無需再進(jìn)行判斷是否滿足堆的性質(zhì)。

第二步

?繼續(xù)找到上一個非葉子結(jié)點,也就是結(jié)點47【索引i=0】,按照相同方法進(jìn)行調(diào)整。
47和18交換

?結(jié)點調(diào)換之后,需要繼續(xù)判斷換下后的結(jié)點是否符合堆的特性【也就是以當(dāng)前結(jié)點47為根的子樹】。發(fā)現(xiàn)不符合,繼續(xù)調(diào)整。此時結(jié)點47的索引i=1,從其左右孩子結(jié)點中選擇最小的一個進(jìn)行調(diào)換,結(jié)果如下:
47和36交換
第三步

?發(fā)現(xiàn)沒有非葉子結(jié)點了,這時調(diào)整結(jié)束。此時得到的就是最大堆。
?代碼設(shè)計邏輯基本上是一樣的。

示例代碼
#include<iostream>
using namespace std;
/**
    * 遞歸實現(xiàn) 
    * 調(diào)整索引為 index 處的數(shù)據(jù),使其符合堆的特性。
    * @param arr 列表
    * @param index 需要堆化處理的數(shù)據(jù)的索引
    * @param len   未排序的堆(數(shù)組)的長度
*/
void AdjustDown(int *arr, int index, int len) 
{
    int li = (index * 2) + 1; // 左子節(jié)點索引
    int ri = li + 1;           // 右子節(jié)點索引
    int cMax = li;             // 子節(jié)點值最大索引,默認(rèn)左子節(jié)點。

    // 左子節(jié)點索引超出計算范圍,直接返回。
    if (li > len) 
    {
        return;
    }

    // 先判斷左右子節(jié)點,哪個較小。
    if (ri <= len && arr[ri]<arr[li]) 
    {
        cMax = ri;
    }
    //如果根結(jié)點大于葉子結(jié)點,交換 
    if (arr[index]>arr[cMax]) 
    {
        int item=arr[index];
        arr[index]=arr[cMax];
        arr[cMax]=item;
        AdjustDown(arr, cMax, len);  // 如果父節(jié)點被子節(jié)點調(diào)換,則需要繼續(xù)判斷換下后的父節(jié)點是否符合堆的特性。
    }
}

/**
    * 迭代實現(xiàn) 
    * 調(diào)整索引為 index 處的數(shù)據(jù),使其符合堆的特性。
    * @param arr 列表
    * @param index 需要堆化處理的數(shù)據(jù)的索引
    * @param len   未排序的堆(數(shù)組)的長度
*/
void maxHeapAdjust(int *arr, int index, int len) 
{
    int li = (index * 2) + 1; // 左子節(jié)點索引
    int ri = li + 1;           // 右子節(jié)點索引
    int item=arr[index];      //存儲該索引結(jié)點 
    while(li<len) 
    {
        int cMax = li;             // 子節(jié)點值最大索引,默認(rèn)左子節(jié)點。
        // 先判斷左右子節(jié)點,哪個較小。
        if (ri<=len && arr[ri]<arr[li]) 
        {
            cMax=ri;
        }
        //如果根結(jié)點小于葉子結(jié)點,交換 
        if (item<arr[cMax]) 
        {
           break;
        }
        arr[li/2]=arr[cMax];//把子節(jié)點值賦值給父結(jié)點 
        li=cMax*2+1;//找到以cMax值為索引的結(jié)點的左孩子結(jié)點
        ri=li+1; 
    }
    arr[li/2]=item; //找到正確位置賦值 
}

int main()
{
    int len=5; 
    int arr[len]={47,40,45,18,36};
    int maxIndex = len - 1; //最大索引 
    int beginIndex = len/ 2 - 1;  //最后一個非葉子結(jié)點索引 
    /*
        *  將數(shù)組堆化
        *  beginIndex = 第一個非葉子節(jié)點。
        *  從第一個非葉子節(jié)點開始即可。無需從最后一個葉子節(jié)點開始。
        *  葉子節(jié)點可以看作已符合堆要求的節(jié)點,根節(jié)點就是它自己且自己以下值為最小。
        *  循環(huán)遍歷 
    */
    for (int i = beginIndex; i >= 0; i--) 
    {
       //maxHeapAdjust(arr, i, maxIndex);
       AdjustDown(arr, i, maxIndex);
    }
    
    for(int i=0;i<len;i++)
    {
        cout<<arr[i]<<"  ";
     } 
     return 0;
} 
(三)時間復(fù)雜度分析

?假設(shè)二叉樹的高度為k,則從倒數(shù)第二層右邊的節(jié)點開始,這一層的節(jié)點都要執(zhí)行子節(jié)點比較然后交換(如果順序是對的就不用交換);倒數(shù)第三層呢,則會選擇其子節(jié)點進(jìn)行比較和交換,如果沒交換就可以不用再執(zhí)行下去了。如果交換了,那么又要選擇一支子樹進(jìn)行比較和交換;高層也是這樣逐漸遞歸。
?小編來分析下這個時間復(fù)雜度求解的過程。
?1、n為結(jié)點的個數(shù),樹的高度(即堆的高度)為h【樹的高度與深度相等】
?2、由于是自底向上,從右至左調(diào)整構(gòu)建堆,因此在調(diào)整上層元素的時候,并不需要同下層所有元素進(jìn)行比較,只需要同其中一個分支進(jìn)行比較,而做比較的次數(shù)則是樹的高度減去當(dāng)前結(jié)點的高度。第i層上結(jié)點元素的個數(shù)為2(i-1),(h-i)表示每個結(jié)點要比較的次數(shù)。因此,第i層所有結(jié)點的總比較次數(shù)為T(i)=2(i-1) * (h-i)。
?3、因此,總的計算時間:
?S=T(h-1)+T(h-2)+……+T(1)=2(h-2)* 1+2(h-3)* 2+……+(h-1)
?注意:因為葉子層不用交換,所以i從h-1開始到1,第一個非葉子結(jié)點所在層所有結(jié)點的高度都為1。
?可以發(fā)現(xiàn)該數(shù)列為等差數(shù)列和等比數(shù)列的乘積,利用錯位相減法可進(jìn)行解決。
?2S=2(T(h-1)+T(h-2)+……T(1))=2(h-1)* 1+2(h-2)* 2+……+2* (h-1)
?2S-S=2(h-1)+2(h-2)+2(h-3)+……+2-(h-1)
?除最后一項外,這就是個等比數(shù)列,直接用上求和公式:
?Sn=a1* (1-qn)/(1-q)(q≠1)
?得到:S=2h-h-1
?因為h為完全二叉樹的高度,因此有:2(h-1)<n<2h,總之可以認(rèn)為:h=logn (實際計算得到應(yīng)該是 log2n+1 < h <= log2n );
?綜上所述得到:S = n - logn -1
?所以時間復(fù)雜度為:O(n)(堆的初始化過程)。

三、堆元素的插入

?剛已經(jīng)分析了堆的初始化過程,那接下來我們來探究下,怎樣往一個堆里面去插入數(shù)據(jù)。

(一)基本插入過程

?堆的插入操作是自底向上進(jìn)行的【這里指每個結(jié)點是向上進(jìn)行調(diào)整】,每次從堆的最后一個結(jié)點開始插入(將插入的值放入完全二叉樹的最后一個結(jié)點),為了維持堆的性質(zhì),還要從插入結(jié)點開始依次往前遞歸,去維持堆的三個特性。
?取插入結(jié)點的父節(jié)點,比較父節(jié)點的值和插入結(jié)點的值,將較小的值交換到父節(jié)點位置。再以父節(jié)點為當(dāng)前結(jié)點,重復(fù)上一步操作,直到遇到父節(jié)點比插入結(jié)點值小,就可以結(jié)束遞歸。

(二)實例分析

?這里僅以大頂堆作為例子講解,用的還是上述得到的大頂堆,我們來往堆里插入新的元素。
大頂堆

?我們現(xiàn)在有這樣的三個數(shù)46、70、50等待我們插入,一起來分析下。

插入46:

?得到下圖:


插入46圖片1

?取插入結(jié)點46的父節(jié)點45,比較父結(jié)點的值和插入結(jié)點的值,發(fā)現(xiàn)不滿足最大堆的性質(zhì),因此將較大的值交換到父節(jié)點位置。得到下圖:
45和46交換
?以父結(jié)點46為當(dāng)前結(jié)點,判斷交換后的結(jié)點是否滿足堆的性質(zhì)。向上找到結(jié)點46的父結(jié)點47,比較它們的值,發(fā)現(xiàn)滿足最大堆的性質(zhì),不需要再進(jìn)行調(diào)整,本次插入操作結(jié)束。
?
插入70

?得到下圖:


插入70

?取插入結(jié)點70的父節(jié)點46,比較父結(jié)點的值和插入結(jié)點的值,發(fā)現(xiàn)不滿足最大堆的性質(zhì),因此將較大的值交換到父節(jié)點位置。得到下圖:
46和70交換
?以父結(jié)點70為當(dāng)前結(jié)點,判斷交換后的結(jié)點是否滿足堆的性質(zhì)。向上找到結(jié)點70的父結(jié)點,發(fā)現(xiàn)不滿足最大堆的性質(zhì),需要再進(jìn)行調(diào)整,交換父結(jié)點與子結(jié)點的位置,得到下圖:
47和70交換

?發(fā)現(xiàn)交換后的結(jié)點70已經(jīng)是根節(jié)點,那么本次插入到此結(jié)束。

插入50:

?得到下圖:
插入50

?取插入結(jié)點50的父節(jié)點18,比較父節(jié)點的值和插入結(jié)點的值,發(fā)現(xiàn)不滿足最大堆的性質(zhì),將較大的值交換到父節(jié)點位置。重復(fù)相同的操作,最終得到下圖:


最終結(jié)果
(三)代碼示例

?根據(jù)上述分析,可得到代碼如下:

//迭代
bool Heap<T>::insert(const T& val)
{
    
    /*
        最大堆特點是根結(jié)點比子節(jié)點都大
        根據(jù)該性質(zhì)進(jìn)行調(diào)整
        (1)索引為i的左孩子的索引是 (2i+1)
       (2)索引為i的右孩子的索引是 (2i+2)
       (3)索引為i的父結(jié)點的索引是 (i-1)/2
    */

    int i=len++;
    while(i>0 && heap[(i-1)/2]<val)
    {
        heap[i]=heap[(i-1)/2];
        i=(i-1)/2;
    }
    heap[i]=val;
    return true;
}

//遞歸
template<typename T>
void Heap<T>::AdjustUp(const T& val)
{
    int i=len++;
    AdjustUps(val,i);
}

template<typename T>
void Heap<T>::AdjustUps(const T& val,int i)
{
    if(i<0)
    {
        return;
    }
    if(heap[(i-1)/2]<val)
    {
        heap[i]=heap[(i-1)/2];
        heap[(i-1)/2]=val;
        i=(i-1)/2;
        AdjustUps(val,i);
    }
}
(四)時間復(fù)雜度分析

?我們可以看到,每個結(jié)點的插入都和樹的深度有關(guān),并且都是不斷的把樹折半來實現(xiàn)插入,因此是典型的遞歸【當(dāng)然用迭代法也可實現(xiàn),道理一樣】。
?插入一個新的結(jié)點后樹的結(jié)點數(shù)為n,高度為h,那么此時結(jié)點要交換的最多次數(shù)為:S=h-1
?因為h為完全二叉樹的高度,因此有:2(h-1)<n<2h,總之可以認(rèn)為:h=logn (實際計算得到應(yīng)該是 log2n+1< h < log2n );
?所以S=O(logn)-1
?插入的時間復(fù)雜度為O(logn)。

四、堆元素的查找

?堆元素的查找相對還是比較簡單,對于一顆滿二叉樹來說,n個節(jié)點的二叉排序樹的高度為log2(n+1),其查找效率為O(log2n),也就是O(logn),近似于折半查找。
?同樣,查找可以有迭代和遞歸兩種實現(xiàn)方法,代碼如下所示:

//非遞歸實現(xiàn) 
template<typename T>
int Heap<T>::search(const T& val)
{
    int i=0;
    while(i<len)
    {
        if(heap[i]==val)
        {
            return i;
        }
        if(heap[2*i+1]==val)
        {
            return 2*i+1;
        }
        if(heap[2*i+2]==val)
        {
            return 2*i+2; 
        }
        i++;
    }
}

//遞歸實現(xiàn) 
template<typename T>
int Heap<T>::find(const T& val)
{
    _find(val,0);
}

template<typename T>
int Heap<T>::_find(const T& val,int i)
{
    if(heap[i]==val)
    {
        return i;
    }
    if(heap[2*i+1]==val)
    {
        return 2*i+1;
    }
    if(heap[2*i+2]==val)
    {
        return 2*i+2; 
    }
    _find(val,i++);
    
}

五、堆元素的刪除

?接下來我們來關(guān)注下堆里元素的刪除。我們還是以下圖作為例子來看看怎么進(jìn)行刪除【大頂堆】。

大頂堆
?我們來刪除結(jié)點50:
?首先找到結(jié)點50對應(yīng)位置的索引,把當(dāng)前堆的最后一個結(jié)點的值賦到當(dāng)前位置,然后刪除最后一個結(jié)點,堆的當(dāng)前長度減1。得到下圖:
刪除50

?那么接下來就是對當(dāng)前結(jié)點18進(jìn)行調(diào)整。調(diào)整的方式與上述一致。這里只放個圖片:
調(diào)整

?因此對于堆里元素的刪除,基本過程就是:
?(1)找到待刪除元素的位置索引
?(2)將堆的最后一個元素賦值到該位置上,堆的容量減1
?(3)進(jìn)行堆結(jié)構(gòu)的調(diào)整
?代碼如下所示:

template<typename T>
bool Heap<T>::deletes(const T& val)
{
    int i=search(val);
    heap[i]=heap[len-1];
    len--;
    AdjustDown(heap,i);
    //maxHeapAdjust(heap,i);
}

六、堆排序

(一)基本思想

?1、將待排序序列構(gòu)造成一個大頂堆【小頂堆】,此時,整個序列的最大值【最小值】就是堆頂?shù)母?jié)點。
?2、將其與末尾元素進(jìn)行交換,此時末尾就為最大值【最小值】。
?3、然后將剩余n-1個元素重新構(gòu)造成一個堆,這樣會得到n個元素的次小值【次大值】。如此反復(fù)執(zhí)行,便能得到一個有序序列了。
?注意:降序排序使用大頂堆,升序排序使用小頂堆。

(二)基本步驟
1、構(gòu)造初始堆

?小編前面已經(jīng)分析了大頂堆與小頂堆的各種有關(guān)操作。所以這里構(gòu)造初始堆的步驟不再做解釋。堆創(chuàng)建成功之后,接下來就是堆的調(diào)整使最后的數(shù)組有序。

2、調(diào)整堆

?這里小編僅以大頂堆作為例子。將堆頂元素與末尾元素進(jìn)行交換,使末尾元素最大。然后繼續(xù)調(diào)整堆,再將堆頂元素與末尾元素交換,得到第二大元素。如此反復(fù)進(jìn)行交換、重建、交換。小編用上述得到的大頂堆來進(jìn)行演示,也就是下圖:

大頂堆

?1、將堆頂元素47和末尾元素36進(jìn)行交換
47和36交換

?重新調(diào)整結(jié)構(gòu),使其繼續(xù)滿足堆定義,得到下圖:
調(diào)整

?2、再將堆頂元素45與末尾元素18進(jìn)行交換,得到下圖:
45和18交換

?重新調(diào)整結(jié)構(gòu),使其繼續(xù)滿足堆定義,得到下圖:
調(diào)整
?3、再將堆頂元素40與末尾元素36進(jìn)行交換,得到下圖:
40和36交換
?滿足堆結(jié)構(gòu),不需調(diào)整繼續(xù)下一步。

?4、再將堆頂元素36與末尾元素18進(jìn)行交換,得到下圖:
36和18交換

?到了這一步,發(fā)現(xiàn)當(dāng)前堆里只剩一個元素,無需調(diào)整。排序結(jié)束,如下所示:
最終排序
(三)時間復(fù)雜度分析

?調(diào)整堆的復(fù)雜度計算和初始化堆差不多,都為O(logn)。又因為堆排序每個結(jié)點都要進(jìn)行一次調(diào)整,因此堆排序的總時間復(fù)雜度為O(nlogn)。

(四)示例代碼

?小編自己寫了大頂堆的相關(guān)操作代碼,包括創(chuàng)建,堆化、查找、刪除、排序等等。小頂堆這里不做分享,原理一樣的。代碼如下所示:

#include <stdio.h>
#include <iostream>
using namespace std;
/*
    本程序中堆的起始索引為0 

*/ 
 
// 堆的實現(xiàn),二叉樹用數(shù)組來表示
template<typename T>
class Heap
{
public:
    Heap(const int& size);  //創(chuàng)建堆
    ~Heap();
    bool insert(const T& val);  //堆插入非遞歸 
    bool deletes(const T& val);//堆刪除非遞歸 
    void AdjustUp(const T& val);//堆插入調(diào)整 
    void AdjustUps(const T& val,int i);//向上調(diào)整 
    void Delete(const T& val);  //刪除堆中某個數(shù)
    int search(const T& val);   //迭代查找堆中某個數(shù)所在位置
    int find(const T& val); //遞歸查找堆中某個數(shù)所在位置
    int _find(const T& val,int i);  //遞歸查找堆中某個數(shù)所在位置
    void AdjustDown(T* arr, int index) ;//遞歸調(diào)整最大堆 ---向下調(diào)整 
    void maxHeapAdjust(T* arr, int index) ;//迭代調(diào)整最大堆 
    void ArrToHeap(T* arr); //數(shù)組轉(zhuǎn)換為堆
    void AdjustToHeap(T* arr,int arrlength) ;//將數(shù)組元素拷貝到堆中再進(jìn)行調(diào)整,調(diào)整后的heap支持插入刪除等操作 
    void HeapSort();        //大頂堆排序,升序,最大值放最后位置
    void ArrToHeapToSort(T* arr);       //傳入數(shù)組轉(zhuǎn)換成堆進(jìn)行排序
    void print();               //打印堆元素
    
private:
    T* heap;                    //堆
    int MaxSize,len,temp;           //MaxSize為堆最大容量,Nel為堆目前元素個數(shù),
};


template<typename T>
Heap<T>::Heap(const int& size)
{
    MaxSize = 2*size;
    heap =new T[MaxSize];
    len=size;
}

template<typename T>
Heap<T>::~Heap()
{
    delete []heap;
    heap = NULL;
    MaxSize = 0;
}

/**
    * 遞歸實現(xiàn) 
    * 調(diào)整索引為 index 處的數(shù)據(jù),使其符合堆的特性。
    * @param arr 列表
    * @param index 需要堆化處理的數(shù)據(jù)的索引
    * @param len   未排序的堆(數(shù)組)的長度
*/
template<typename T>
void Heap<T>::AdjustDown(T* arr, int index) 
{
    int li = (index * 2) + 1; // 左子節(jié)點索引
    int ri = li + 1;           // 右子節(jié)點索引
    int cMax = li;             // 子節(jié)點值最大索引,默認(rèn)左子節(jié)點。
    int maxIndex = len - 1; //最大索引
    // 左子節(jié)點索引超出計算范圍,直接返回。
    if (li > maxIndex) 
    {
        return;
    }

    // 先判斷左右子節(jié)點,哪個較大。
    if (ri <= maxIndex && arr[li]<arr[ri]) 
    {
        cMax = ri;
    }
    //如果根結(jié)點小于葉子結(jié)點,交換 
    if (arr[index]<arr[cMax]) 
    {
        int item=arr[index];
        arr[index]=arr[cMax];
        arr[cMax]=item;
        AdjustDown(arr, cMax);  // 如果父節(jié)點被子節(jié)點調(diào)換,則需要繼續(xù)判斷換下后的父節(jié)點是否符合堆的特性。
    }
}

/**
    * 迭代實現(xiàn) 
    * 調(diào)整索引為 index 處的數(shù)據(jù),使其符合堆的特性。
    * @param arr 列表
    * @param index 需要堆化處理的數(shù)據(jù)的索引
    * @param len   未排序的堆(數(shù)組)的長度
*/
template<typename T>
void Heap<T>::maxHeapAdjust(T* arr, int index) 
{
    int li = (index * 2) + 1; // 左子節(jié)點索引
    int ri = li + 1;           // 右子節(jié)點索引
    int item=arr[index];      //存儲該索引結(jié)點 
    int maxIndex = len - 1; //最大索引
    while(li<maxIndex) 
    {
        int cMax = li;             // 子節(jié)點值最大索引,默認(rèn)左子節(jié)點。
        // 先判斷左右子節(jié)點,哪個較大。
        if (ri<=maxIndex && arr[li]<arr[ri]) 
        {
            cMax=ri;
        }
        //如果根結(jié)點小于葉子結(jié)點,交換 
        if (item>=arr[cMax]) 
        {
           break;
        }
        arr[li/2]=arr[cMax];//把子節(jié)點值賦值給父結(jié)點 
        li=cMax*2+1;//找到以cMax值為索引的結(jié)點的左孩子結(jié)點
        ri=li+1; 
    }
    arr[li/2]=item; //找到正確位置賦值 
}

/*
    將數(shù)組調(diào)整為堆
*/
template<typename T>
void Heap<T>::ArrToHeap(T* arr)
{

    int beginIndex = len/ 2 - 1;  //最后一個非葉子結(jié)點索引 
    /*
        *  將數(shù)組堆化
        *  beginIndex = 第一個非葉子節(jié)點。
        *  從第一個非葉子節(jié)點開始即可。無需從最后一個葉子節(jié)點開始。
        *  葉子節(jié)點可以看作已符合堆要求的節(jié)點,根節(jié)點就是它自己且自己以下值為最大。
        *  循環(huán)遍歷 
    */
    for (int i = beginIndex; i >= 0; i--) 
    {
       maxHeapAdjust(arr, i);
       // AdjustDown(arr, i);
    }
    
}

/*
    將數(shù)組元素拷貝到heap數(shù)組中再進(jìn)行調(diào)整
    調(diào)整后的heap支持插入刪除等操作 
    
*/

template<typename T>
void Heap<T>:: AdjustToHeap(T* arr,int arrlength)
{
    for(int i=0;i<arrlength;i++)
    {
        heap[i]=arr[i];
    }
    int beginIndex = len/ 2 - 1;  //最后一個非葉子結(jié)點索引 
    /*
        *  將數(shù)組堆化
        *  beginIndex = 第一個非葉子節(jié)點。
        *  從第一個非葉子節(jié)點開始即可。無需從最后一個葉子節(jié)點開始。
        *  葉子節(jié)點可以看作已符合堆要求的節(jié)點,根節(jié)點就是它自己且自己以下值為最大。
        *  循環(huán)遍歷 
    */
    for (int i = beginIndex; i >= 0; i--) 
    {
       maxHeapAdjust(heap, i);
       // AdjustDown(arr, i);
    }
}

template<typename T>
bool Heap<T>::insert(const T& val)
{
    
    /*
        最大堆特點是根結(jié)點比子節(jié)點都大
        根據(jù)該性質(zhì)進(jìn)行調(diào)整
        (1)索引為i的左孩子的索引是 (2i+1)
       (2)索引為i的右孩子的索引是 (2i+2)
       (3)索引為i的父結(jié)點的索引是 (i-1)/2
    */

    int i=len++;
    while(i>0 && heap[(i-1)/2]<val)
    {
        heap[i]=heap[(i-1)/2];
        i=(i-1)/2;
    }
    heap[i]=val;
    return true;
}

template<typename T>
void Heap<T>::AdjustUp(const T& val)
{
    int i=len++;
    AdjustUps(val,i);
}

template<typename T>
void Heap<T>::AdjustUps(const T& val,int i)
{
    if(i<0)
    {
        return;
    }
    if(heap[(i-1)/2]<val)
    {
        heap[i]=heap[(i-1)/2];
        heap[(i-1)/2]=val;
        i=(i-1)/2;
        AdjustUps(val,i);
    }
}

//非遞歸實現(xiàn) 
template<typename T>
int Heap<T>::search(const T& val)
{
    int i=0;
    while(i<len)
    {
        if(heap[i]==val)
        {
            return i;
        }
        if(heap[2*i+1]==val)
        {
            return 2*i+1;
        }
        if(heap[2*i+2]==val)
        {
            return 2*i+2; 
        }
        i++;
    }
}

//遞歸實現(xiàn) 
template<typename T>
int Heap<T>::find(const T& val)
{
    _find(val,0);
}

template<typename T>
int Heap<T>::_find(const T& val,int i)
{
    if(heap[i]==val)
    {
        return i;
    }
    if(heap[2*i+1]==val)
    {
        return 2*i+1;
    }
    if(heap[2*i+2]==val)
    {
        return 2*i+2; 
    }
    _find(val,i++);
    
}



template<typename T>
bool Heap<T>::deletes(const T& val)
{
    int i=search(val);
    heap[i]=heap[len-1];
    len--;
    AdjustDown(heap,i);
    //maxHeapAdjust(heap,i);
}

template<typename T>
void Heap<T>::HeapSort()
{
    int index=len-1;
    int tmplen=len;
    while(index>=0)
    {
        int tmp=heap[index];
        heap[index]=heap[0];
        heap[0]=tmp;
        len--;
        index--;
        AdjustDown(heap,0);
    } 
    len=tmplen;
}

template<typename T>
void Heap<T>::ArrToHeapToSort(T* arr)
{
    ArrToHeap(arr); 
    int index=len-1;
    int tmplen=len;
    while(index>=0)
    {
        int tmp=arr[index];
        arr[index]=arr[0];
        arr[0]=tmp;
        len--;
        index--;
        AdjustDown(arr,0);
    } 
    len=tmplen;//恢復(fù)堆長度 
    for(int i=0;i<len;i++)
    {
        cout<<arr[i]<<" ";
    }
}


template<typename T>
void Heap<T>::print()
{
    for(int i=0;i<len;i++)
    {
        cout<<heap[i]<<"  ";
    }
}


int main()
{
    int arr[5]={18,36,45,40,47};
    int arrlength=sizeof(arr)/sizeof(arr[0]);
    Heap<int> max_heap(arrlength);
    //max_heap.ArrToHeapToSort(arr);
    max_heap.AdjustToHeap(arr,arrlength);
    //max_heap.HeapSort();
    max_heap.AdjustUp(46);
    max_heap.AdjustUp(70);
    max_heap.AdjustUp(50);
    //cout<<max_heap.search(50)<<endl;
    cout<<max_heap.find(50)<<endl;
    //max_heap.insert(46);
    //max_heap.deletes(50);
    max_heap.print();
    return 0;
}

七、堆應(yīng)用之“優(yōu)先級隊列”

(一)隊列與優(yōu)先隊列的區(qū)別

?1、隊列是一種FIFO(First-In-First-Out)先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),對應(yīng)于生活中的排隊的場景,排在前面的人總是先通過,依次進(jìn)行。
?2、優(yōu)先隊列是特殊的隊列,從“優(yōu)先”一詞,可看出有“插隊現(xiàn)象”。比如在火車站排隊進(jìn)站時,就會有些比較急的人來插隊,他們就在前面先通過驗票。優(yōu)先隊列至少含有兩種操作的數(shù)據(jù)結(jié)構(gòu):insert(插入),即將元素插入到優(yōu)先隊列中(入隊);以及deleteMin(刪除最小者),它的作用是找出、刪除優(yōu)先隊列中的最小的元素(出隊)。
?3、優(yōu)先隊列的實現(xiàn)常選用二叉堆,在數(shù)據(jù)結(jié)構(gòu)中,優(yōu)先隊列一般也是指堆。

(二)C++優(yōu)先隊列簡介

?優(yōu)先隊列在頭文件#include <queue>中;其聲明格式為:priority_queue <int> q;【聲明一個名為q的整形的優(yōu)先隊列】。
?支持的操作:
?q.empty() //如果隊列為空,則返回true,否則返回false
?q.size() //返回隊列中元素的個數(shù)
?q.pop() //刪除隊首元素,但不返回其值
?q.top() //返回具有最高優(yōu)先級的元素值,但不刪除該元素
?q.push(item) //在基于優(yōu)先級的適當(dāng)位置插入新元素
?有關(guān)操作不做介紹了。

八、一點總結(jié)

?堆的使用場景還是非常豐富的,掌握堆的各種操作很有必要。啰里啰唆寫了一大堆,或多或少存在些錯誤,也請指正,謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容