數據結構-二叉堆(大頂堆&小頂堆)

簡介

二叉堆就是一顆二叉樹,是一顆完全二叉樹,最直觀表現一個二叉樹左邊最多比右邊深 1 層,二叉堆我們常常討論的就是大頂堆和小頂堆,其中大頂堆根結點最大,左右節點依次遞歸,小頂堆類似

二叉堆算是一種比較重要的數據結構,實際中我們的堆排序就涉及到二叉堆,它也是優先級隊列的基礎

所以這裏我可以說下二叉堆的性質

  • 完全二叉樹
    完全二叉樹比平衡二叉樹條件更嚴格,平衡二叉樹可能左邊深一點,也可能右邊深一點,但是完全二叉樹只能左邊深。而且只要一談到完全二叉樹,就應該要直接想到一顆完全二叉樹完全可以用數組去實現,而且用數組存儲每一層後有這樣的規律:(若當前二叉堆結點下標是 currentIndex)

    // 當前結點下標
    int currentIndex;
    
    /* 對應着刪除的下沉操作 */
    // 當前結點的左孩子結點下標
    int leftChildIndex = 2 * currentIndex + 1;
    // 當前結點的右孩子結點下標
    int rightChildIndex = 2 * currentIndex + 2;
    
    /* 對應着新增的上浮操作 */
    // 當前結點雙親結點的下標(當前結點不論是雙籤的左孩子還是右孩子都是滿足的)
    int parentsIndex = (currentIndex - 1) / 2;
    

    這個規律幾乎成了二叉堆變換的關鍵

  • 遵循一定的大小規律

    這裏討論的大頂堆和小頂堆遵循一定的大小規律

  • 自行調整的特性

    一般我們討論二叉堆,都是默認二叉堆的自動調整功能,比如二叉堆的插入後自動調整,刪除結點自動調整等。需要注意的是對於插入操作,我們默認從二叉堆的最後面插入,也就是數組的最後插入,對於數組而言這也不叫插入了,其實就是新增;刪除操作對於二叉堆我們默認直接刪除對頂元素,對於數組而言其實就是移除頭部元素,這兩種操作有可能使得二叉堆不具有大頂堆或者小頂堆的特性,因此還需要進行一定的二叉堆調整操作

Java 實現

邏輯思路

首先聲明二叉堆是一顆完全二叉樹,完全二叉樹是可以存儲在一個數組中,因爲存儲在數組中的完全二叉樹找到其左右孩子結點甚至雙親節點很容易,這得要通過下標去尋找

  • 插入操作

    插入默認從完全二叉樹最後面插入,其實就是新增結點,通過上浮操作,最後上浮到不能上浮爲止,新的完全二叉堆就成功構建

  • 刪除操作

    刪除默認刪除完全二叉樹的根結點,然後我們還需要將最後一個結點直接放在根結點出,然後進行下沉,下沉到不能在變動的時候新的二叉堆就成功構建了

  • 構建二叉堆

    構建二叉堆默認就是把一個無序的完全二叉樹通過自我調整,變成一個二叉堆的過程。具體怎麼做呢?可以從完全二叉樹最後一個非葉子結點開始進行下沉操作,然後依次遍歷到所有的非葉子結點,你要是從根結點這個打頭的非葉子結點開始也行

由於二叉堆用數組存儲,因此遵循的規律如下:(這裏重複寫一遍)

// 當前結點下標
int currentIndex;

/* 對應着刪除的下沉操作 */
// 當前結點的左孩子結點下標
int leftChildIndex = 2 * currentIndex + 1;
// 當前結點的右孩子結點下標
int rightChildIndex = 2 * currentIndex + 2;

/* 對應着新增的上浮操作 */
// 當前結點雙親結點的下標(當前結點不論是雙籤的左孩子還是右孩子都是滿足的)
int parentsIndex = (currentIndex - 1) / 2;

結構圖解

堆排序涉及到構建堆,下圖就是構建一個大頂堆,從最後一個非葉子結點開始進行下沉操作,循環到根結點也進行下沉操作,最後結束

a

代碼實現

下面是針對於大頂堆的代碼

下面代碼中,對於插入的結點的代碼中,數組已經將結點插入到尾部,代碼只需要進行上浮操作

刪除結點的代碼中,數組已經將指定位置的結點刪除,然後將最後一個結點放置於被刪除的結點處,然後代碼只需要進行下沉操作

// 插入結點(需要將插入到尾部的結點上浮操作,該數組中已經將插入的結點放置於數組尾部)
public void upAdjust(int[] arr) {
    int currentIndex = arr.length - 1;
    int parentsIndex = (currentIndex - 1) / 2;
    // 臨時保存插入的結點值
    int tmp = arr[currentIndex];
    // 上浮操作循環
    while (parentsIndex >= 0 && arr[parentsIndex] < tmp) {
        // 單向賦值,無交換
        arr[currentIndex] = arr[parentsIndex];
        currentIndex = parentsIndex;
        parentsIndex = (currentIndex - 1) / 2;
    }
    // 最後將臨時儲存的結點值賦給 currentIndex 指向的結點值
    arr[currentIndex] = tmp;
}


// 刪除結點(需要將指定的結點刪除,將尾部結點放在被刪除結點處,數組中已放,需要進行下沉操作)
public void downAdjust(int[] arr, int currentIndex) {
    int tmp = arr[currentIndex];
    int childIndex = 2 * currentIndex + 1;
    while (childIndex < arr.length) {
        // 判斷是左孩子還是右孩子
        if (childIndex + 1 < arr.length && arr[childIndex] < arr[childIndex+1]) {
            childIndex++;
        }
        // 若發現不滿足循環條件即可跳出
        if (tmp >= arr[childIndex]) {
            break;
        }
        // 單向賦值,無交換
        arr[currentIndex] = arr[childIndex];
        currentIndex = childIndex;
        childIndex = 2 * currentIndex + 1;
    }
    arr[currentIndex] = tmp;
}


// 創建二叉堆(創建一個大頂堆,一致有一個完全二叉樹的數組)
public void buildHeap(int[] arr) {
    // 實際是 ((arr.length - 1) - 1) / 2
    for (int i = (arr.length - 2) / 2; i >= 0; i++) {
        // 每個非葉子結點都需要做下沉操作,從最後一個開始往前遍歷
        downAdjust(arr, i);
    }
}

時間複雜度

滿二叉樹的結點數是 n,那麼其深度應該是 log 以 2 爲底 n 的對數

  • 插入操作時間複雜度

    二叉堆是一個完全二叉樹,其插入操作是上浮操作,最差情況下每層值進行一次,所以其時間複雜度爲 O(logn),最差情況下就是 O(logn),最好情況 O(1),平均時間複雜度表示爲 O(logn)

  • 刪除操作時間複雜度

    刪除是下沉操作,最差情況每層也只用進行一次,所以時間複雜度爲 O(logn),最差時間複雜度爲 O(logn),最好情況就是 1 次 O(1),平均時間複雜度表示爲 O(logn)

  • 構建二叉堆的時間複雜度

    有興趣的筒子們可以用數學知識證明一下,時間複雜度爲 O(n)

一些疑問

  1. 爲什麼刪除操作不能是刪除根結點,然後不把最後一個元素移到頭部,直接去做篩選元素上浮呢?

    答:因爲如果過這麼去做,會發現最終結果可能無法構成一個完全二叉樹!打個比方如下:

            1
        3		2
            
    4	5        
    

    現在刪除 1,不把 5 移動到頭部,那麼 2 直接上浮佔據了 1 的位置,此時 2 的位置置空,那麼左邊深度要比右邊大 2 了,這也就不是一顆完全二叉樹了,自然也不能存儲數組裏去了!

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