堆排序(大頂推)

原文鏈接:https://www.cnblogs.com/lanhaicode/p/10546257.html

堆排序

圖片和描述轉載於https://www.cnblogs.com/lanhaicode/p/10546257.html,代碼是根據該描述以自己的理解自己實現的。

1、什麼是堆?

堆是一種非線性結構,(本篇隨筆主要分析堆的數組實現)可以把堆看作一個數組,也可以被看作一個完全二叉樹,通俗來講堆其實就是利用完全二叉樹的結構來維護的一維數組

按照堆的特點可以把堆分爲大頂堆小頂堆

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

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

(堆的這種特性非常的有用,堆常常被當做優先隊列使用,因爲可以快速的訪問到“最重要”的元素)

 

2、堆的特點(數組實現)

 

(圖片來源:https://www.cnblogs.com/chengxiao/p/6129630.html)

我們對堆中的結點按層進行編號,將這種邏輯結構映射到數組中就是下面這個樣子

(圖片來源:https://www.cnblogs.com/chengxiao/p/6129630.html)

 

我們用簡單的公式來描述一下堆的定義就是:(讀者可以對照上圖的數組來理解下面兩個公式)

大頂堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] 

小頂堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] 

 

3、堆和普通樹的區別

內存佔用:

普通樹佔用的內存空間比它們存儲的數據要多。你必須爲節點對象以及左/右子節點指針分配額外的內存。堆僅僅使用數組,且不使用指針

(可以使用普通樹來模擬堆,但空間浪費比較大,不太建議這麼做)

 

平衡

二叉搜索樹必須是“平衡”的情況下,其大部分操作的複雜度才能達到O(nlog2n)。你可以按任意順序位置插入/刪除數據,或者使用 AVL 樹或者紅黑樹,但是在堆中實際上不需要整棵樹都是有序的。我們只需要滿足對屬性即可,所以在堆中平衡不是問題。因爲堆中數據的組織方式可以保證O(nlog2n) 的性能


搜索:

在二叉樹中搜索會很快,但是在堆中搜索會很慢。在堆中搜索不是第一優先級,因爲使用堆的目的是將最大(或者最小)的節點放在最前面,從而快速的進行相關插入、刪除操作

 

 

4、堆排序的過程

先了解下堆排序的基本思想:

將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就爲最大值。然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值,

如此反覆執行,便能得到一個有序序列了,建立最大堆時是從最後一個非葉子節點開始從下往上調整的(這句話可能不好太理解),下面會舉一個例子來理解堆排序的基本思想

 

給一個無序序列如下

int a[6] = {7, 3, 8, 5, 1, 2};

現在可以根據數組將完全二叉樹還原出來

好了,現在我們要做的事情就是要把7,3,8,5,1,2變成一個有序的序列,如果想要升序就是1,2,3,5,7,8   如果想要降序就是8,7,5,3,2,1 ,這兩種就是我們要的最終結果,然後我們就可以根據我們想要的結果來選擇

適合類型的堆來進行排序

升序----使用大頂堆

降序----使用小頂堆

5、爲什麼升序要用大頂堆呢

上面提到過大頂堆的特點:每個結點的值都大於等於其左右孩子結點的值,我們把大頂堆構建完畢後根節點的值一定是最大的,然後把根節點的和最後一個元素(也可以說最後一個節點)交換位置,那麼末尾元素此時就是最大元素了(理解這點很重要)

知道了堆排序的原理下面就可以來操作了,在進行操作前先理清一下步驟

 

(假設我們想要升序的排列)

第一步:先n個元素的無序序列,構建成大頂堆

第二步:將根節點與最後一個元素交換位置,(將最大元素"沉"到數組末端

第三步:交換過後可能不再滿足大頂堆的條件,所以需要將剩下的n-1個元素重新構建成大頂堆

第四步:重複第二步、第三步直到整個數組排序完成

 

6、圖解交換過程(得到升序序列,使用大頂堆來調整)

這裏以int a[6] = {7, 3, 8, 5, 1, 2}爲例子

先要找到最後一個非葉子節點,數組的長度爲6,那麼最後一個非葉子節點就是:長度/2-1,也就是6/2-1=2,然後下一步就是比較該節點值和它的子樹值,如果該節點小於其左\右子樹的值就交換(意思就是將最大的值放到該節點)

8只有一個左子樹,左子樹的值爲2,8>2不需要調整

 

 

下一步,繼續找到下一個非葉子節點(其實就是當前座標-1就行了),該節點的值爲3小於其左子樹的值,交換值,交換後該節點值爲5,大於其右子樹的值,不需要交換

下一步,繼續找到下一個非葉子節點,該節點的值爲7,大於其左子樹的值,不需要交換,再看右子樹,該節點的值小於右子樹的值,需要交換值

下一步,檢查調整後的子樹,是否滿足大頂堆性質,如果不滿足則繼續調整(這裏因爲只將右子樹的值與根節點互換,只需要檢查右子樹是否滿足,而8>2剛好滿足大頂堆的性質,就不需要調整了,

如果運氣不好整個數的根節點的值是1,那麼就還需要調整右子樹)

 

到這裏大頂堆的構建就算完成了,然後下一步交換根節點(8)與最後一個元素(2)交換位置(將最大元素"沉"到數組末端),此時最大的元素就歸位了,然後對剩下的5個元素重複上面的操作

(這裏用粉紅色來表示已經歸位的元素)

剩下只有5個元素,最後一個非葉子節點是5/2-1=1,該節點的值(5)大於左子樹的值(3)也大於右子樹的值(1),滿足大頂堆性質不需要交換

找到下一個非葉子節點,該節點的值(2)小於左子樹的值(5),交換值,交換後左子樹不再滿足大頂堆的性質再調整左子樹,左子樹滿足要求後再返回去看根節點,根節點的值(5)小於右子樹的值(7),再次交換值

 

得到新的大頂堆,如下圖,再把根節點的值(7)與當前數組最後一個元素值(1)交換,再重構大頂堆->交換值->重構大頂堆->交換值····,直到整個數組都變成有序序列

 

最後得到的升序序列如下圖

7、大頂堆排序的代碼實現

#include <stdio.h>

void Swap(int *heap, int i,int j);        /* 交換元素的值 */
void BuildMaxHeap(int *heap, int len);/* 構建大頂堆 */

int main()
{
    int a[] = {7, 3, 8, 5, 1, 2};
    int len = sizeof(a)/sizeof(int);    /* 數組長度 */
    int i,j;

    for (i = len; i > 0; i--)// 需要構建len次。
    {   
        BuildMaxHeap(a, i); 
        Swap(a, 0,i-1);//構建完成一次後,將,堆頂和最後一個元素交換位置,此時,最後一個元素爲最大值,下一次不需要處理最後一個元素了。
    }   
    for (i = 0; i < len; i++)
    {   
        printf("%d ", a[i]);
    }   
    printf("\n");

    return 0;
}
int HeapAdjust(int *heap,int i,int len)
{
    int l,r,max;
    max =i; 
    l=2*i+1;
    r=2*i+2;
    if (l < len && heap[max] < heap[l])    /* 根節點大於左子樹,構建小頂堆只需修改比較條件 */
    {   
        min =l; 
    }   
    if (r < len && heap[max] < heap[r])    /* 根節點大於右子樹,構建小頂堆只需修改比較條件 */
    {
        max =r;
    }
    if(max !=i)
    {
        Swap(heap,max,i);
        HeapAdjust(heap,max,len);//交換完以後,二叉樹下面的節點可能又變成無序的了,因此遞歸向下調整一下。
    }
}


/* Function: 構建大頂堆 */
void BuildMaxHeap(int *heap,int len)
{
    int i;
    for (i = len/2-1; i >= 0; i--)//從最後一個非葉子節點開始。進行堆調整。
    {
        HeapAdjust(heap,i,len);
    }
}

/* Function: 交換交換根節點和數組末尾元素的值*/
void Swap(int *heap, int i,int j)
{
    int temp;

    temp = heap[i];
    heap[i] = heap[j];
    heap[j] = temp;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章