最小/大堆的操作及堆排序


摘自:《啊哈算法》

我們要用1, 2, 5, 12, 7, 17, 25, 19, 36, 99, 22, 28, 46, 92來建立最小堆,並且刪除最小的數,並增加一個數23

如何建立這個堆:

//建堆
n = 0;
for (int i = 1; i <= m; i++) {
    n++;
    h[n] = a[n];
    shiftup(n);
}

我們還有更快的方法可以建立一個堆

思路:直接把1, 2, 5, 12, 7, 17, 25, 19, 36, 99, 22, 28, 46, 92放入一個完全二叉樹中,在這個二叉樹中,我們從最后一個節點開始,依次判斷以這個節點為根的子樹是否符合最小堆的特性。如果所有的子樹都符合最小堆的特性,那么整棵樹就是最小堆了。(注意完全二叉樹有一個性質,最后一個非葉子節點是第n/2個節點,從1數起而不是從0數起)

算法:把n個元素建立一個堆,首先我們可以將這n個節點以自頂向下,從左到右的方式從1到n編碼,可以將這n個節點轉換成一棵完全二叉樹。緊接着從最后一個非葉節點(節點編號為n/2)開始到根節點(節點編號為1),逐個掃描所有的節點,根據需要將當前節點向下調整,直到以當前結點為根節點的子樹符合堆的特性。時間復雜度是O(N)

代碼如下:

void create_heap() {
    for (int i = n/2; i >= 1; i--) {
        shiftdown(i);
    }
}

建堆后是完全二叉樹,並且所有父節點都比子節點小

接下來,我們將堆頂刪除,把新增加的23放在堆頂。

顯然加了數后已經不符合最小堆的特性了,我們需要將新增加的數調整到合適的位置。

向下調整,將這個數與它的兩個兒子2和5比較,選擇較小的一個與它交換

此時我們發現還是不滿足最小堆,於是繼續將23與它的兩個兒子中較小的一個交換。

再交換

向下調整的代碼:

void shiftdown(int i) { //傳入一個需要向下調整的結點編號i
    int t, flag = 0; //flag用來標記是否需要繼續向下調整
    while (i * 2 <= n && flag == 0) {
        //首先判斷它和左兒子的關系,並用t記錄值較小的節點編號
        if (h[i] > h[i*2]) {
            t = i*2;
        } else {
            t = i;
        }

        //如果它有右兒子,再對右兒子進行討論
        if (i*2 + 1 <= n) {
            //如果它的右兒子的值更小,更新較小的結點編號
            if (h[t] < h[i*2 + 1])
                t = i * 2 + 1;
        }

        //如果發現最小的編號不是自己,說明子結點中有比父節點更小的
        if (t != i) {
            swap(t, i);
            i = t;
        } else {
            flag = 1;
        }
    }
}

如果只是想新增一個數,而不是刪除最小值,只需要將新元素插入到末尾,再根據情況判斷新元素是否需要上移,直到滿足新的特性位置。

加入我們現在要加入一個3

先將3與它的父節點25比較,發現比父節點小,需要與父節點交換。以此類推

向上調整的代碼:

//新增加一個元素
void shiftup(int i) { //傳入一個需要向上調整的結點編號i
    int flag = 0; //用來標記是否需要繼續向上調整
    if (i == 1) return ; //如果是堆頂,就返回,不需要再調整了
    //不在堆頂,並且當前結點i的值比父節點小的時候就繼續向上調整
    while (i != 1 && flag == 0) {
        //判斷是否比父節點的小
        if (h[i] < h[i/2]) {
            swap(i, i/2);
        } else{
            flag = 1;
        }
        i = i/2;
    }
}

--------------------------------------------------------------------------------------------------

堆排序:

時間復雜度是O(NlogN),假如要從小到大排序,那么只需要建立最小堆,然后每次刪除頂部元素並將頂部元素輸出或者放入到一個新的數組中,直到堆為空為止。

int deleteMax() {
    int t;
    t = h[1];
    h[1] = h[n];
    n--; //堆的元素減少1
    shiftdown(1); //向下調整
    return t;
}

一種更好的方法是,從小到大排序的時候不是建立最小堆而是建立最大堆,最大堆建立好后,最大的元素在h[1],因為我們的需求是從小到大排序,希望最大的放在最后,因此我們將h[1]和h[n]交換,此時h[n]就是數組中的最大的元素。交換后將h[1]向下調整以保持堆的特性。

void heapsort() {
    while (n > 1) {
        swap(1, n);
        n--;
        shiftdown(1);
    }
}

-------------------------------------------------------------------------------------------------

堆的其他應用:

1.求一個數列中的第K大的數

建立一個大小為K的最小堆,堆頂就是第K大的數

例如,假設有10個數,要求求第3大的數,第一步選取任意的3個數,比如說是前3個,將這3個數建成最小堆,然后從第4個數開始,與堆頂
的數比較,如果比堆頂的數要小,那么這個數就不要,如果比堆頂的數大,則舍棄當前的堆頂而將這個數作為新的堆頂,並再去維護堆


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM