【學點數據結構和算法】06-二叉堆和優先隊列

寫在前面: 博主是一名軟件工程系大數據應用開發專業大二的學生,暱稱來源於《愛麗絲夢遊仙境》中的Alice和自己的暱稱。作爲一名互聯網小白,寫博客一方面是爲了記錄自己的學習歷程,一方面是希望能夠幫助到很多和自己一樣處於起步階段的萌新。由於水平有限,博客中難免會有一些錯誤,有紕漏之處懇請各位大佬不吝賜教!個人小站:http://alices.ibilibili.xyz/ , 博客主頁:https://alice.blog.csdn.net/
儘管當前水平可能不及各位大佬,但我還是希望自己能夠做得更好,因爲一天的生活就是一生的縮影。我希望在最美的年華,做最好的自己

        上一篇博客👉《【學點數據結構和算法】05-樹》藉助《小灰算法》爲初入數據結構大門的朋友們帶來了一場視覺盛宴。本篇,要介紹的二叉堆,同樣是一種基礎數據結構,但它也是一種特殊的二叉樹。具體是怎麼一回事呢?讓我們繼續往下看!
在這裏插入圖片描述


1、初識二叉堆

        二叉堆本質上是一種完全二叉樹,它分爲兩個類型。

  1. 最大堆
  2. 最小堆

        什麼是最大堆呢?最大堆的任何一個父節點的值,都大於或等於它左、右孩子節點 的值。
在這裏插入圖片描述
        什麼是最小堆呢?最小堆的任何一個父節點的值,都小於或等於它左、右孩子節點的值。
在這裏插入圖片描述
        二叉堆的根節點叫作堆頂。

        最大堆和最小堆的特點決定了:最大堆的堆頂是整個堆中的最大元素;最小堆的堆
頂是整個堆中的最小元素

2、二叉堆的自我調整

        對於二叉堆,有如下幾種操作。

  1. 插入節點。
  2. 刪除節點。
  3. 構建二叉堆。

        這幾種操作都基於堆的自我調整。所謂堆的自我調整,就是把一個不符合堆性質的完全二叉樹,調整成一個堆。下面讓我們以最小堆爲例,看一看二叉堆是如何進行自我調整的。

2.1 插入節點

        當二叉堆插入節點時,插入位置是完全二叉樹的最後一個位置。例如插入一個新節 點,值是 0。
在這裏插入圖片描述
        這時,新節點的父節點5比0大,顯然不符合最小堆的性質。於是讓新節點“上浮”,和 父節點交換位置。
在這裏插入圖片描述
        繼續用節點0和父節點3做比較,因爲0小於3,則讓新節點繼續“上浮”。
在這裏插入圖片描述
        繼續比較,最終新節點0“上浮”到了堆頂位置。
在這裏插入圖片描述

2.2 刪除節點

        二叉堆刪除節點的過程和插入節點的過程正好相反,所刪除的是處於堆頂的節點。例 如刪除最小堆的堆頂節點1。
在這裏插入圖片描述
        這時,爲了繼續維持完全二叉樹的結構,我們把堆的最後一個節點10臨時補到原本堆 頂的位置。
在這裏插入圖片描述
        接下來,讓暫處堆頂位置的節點10和它的左、右孩子進行比較,如果左、右孩子節點 中最小的一個(顯然是節點2)比節點10小,那麼讓節點10“下沉”。
在這裏插入圖片描述
        繼續讓節點10和它的左、右孩子做比較,左、右孩子中最小的是節點7,由於10大於 7,讓節點10繼續“下沉”。
在這裏插入圖片描述
        這樣一來,二叉堆重新得到了調整。

2.3 構建二叉堆

        構建二叉堆,也就是把一個無序的完全二叉樹調整爲二叉堆,本質就是讓所有非葉子節點依次“下沉”

        下面舉一個無序完全二叉樹的例子,如下圖所示。
在這裏插入圖片描述
        首先,從最後一個非葉子節點開始,也就是從節點10開始。如果節點10大於它左、右 孩子節點中最小的一個,則節點10“下沉”。
在這裏插入圖片描述
        接下來輪到節點3,如果節點3大於它左、右孩子節點中最小的一個,則節點3“下 沉”。
在這裏插入圖片描述
        然後輪到節點1,如果節點1大於它左、右孩子節點中最小的一個,則節點1“下沉”。 事實上節點1小於它的左、右孩子,所以不用改變。

        接下來輪到節點7,如果節點7大於它左、右孩子節點中最小的一個,則節點7“下 沉”。

在這裏插入圖片描述
        節點7繼續比較,繼續“下沉”。

在這裏插入圖片描述
        經過上述幾輪比較和“下沉”操作,最終每一節點都小於它的左、右孩子節點,一個無序的完全二叉樹就被構建成了一個最小堆。

堆的插入和刪除操作,時間複雜度是O(logn),但構建堆的時間複雜度是O(n)

2.4 二叉堆的代碼實現

        在展示代碼之前,我們還需要明確一點:二叉堆雖然是一個完全二叉樹,但它的存儲 方式並不是鏈式存儲,而是順序存儲。換句話說,二叉堆的所有節點都存儲在數組中
在這裏插入圖片描述
        在數組中,在沒有左、右指針的情況下,如何定位一個父節點的左孩子和右孩子呢?

        像上圖那樣,可以依靠數組下標來計算。

        假設父節點的下標是parent,那麼它的左孩子下標就是 2×parent+1;右孩子下標就 是2×parent+2。

        例如上面的例子中,節點6包含9和10兩個孩子節點,節點6在數組中的下標是3,節點 9在數組中的下標是7,節點10在數組中的下標是8。

        有了這個前提,下面的代碼就更好理解了。

import java.util.Arrays;

public class HeapOperator {

    /**
     * 上浮調整
     * @param array     待調整的堆
     */
    public static void upAdjust(int[] array) {
        int childIndex = array.length-1;
        int parentIndex = (childIndex-1)/2;
        // temp保存插入的葉子節點值,用於最後的賦值
        int temp = array[childIndex];
        while (childIndex > 0 && temp < array[parentIndex])
        {
            //無需真正交換,單向賦值即可
            array[childIndex] = array[parentIndex];
            childIndex = parentIndex;
            parentIndex = (parentIndex-1) / 2;
        }
        array[childIndex] = temp;
    }

    /**
     * 下沉調整
     * @param array     待調整的堆
     * @param parentIndex    要下沉的父節點
     * @param length    堆的有效大小
     */
    public static void downAdjust(int[] array, int parentIndex, int length) {
        // temp保存父節點值,用於最後的賦值
        int temp = array[parentIndex];
        int childIndex = 2 * parentIndex + 1;
        while (childIndex < length) {
            // 如果有右孩子,且右孩子小於左孩子的值,則定位到右孩子
            if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) {
                childIndex++;
            }
            // 如果父節點小於任何一個孩子的值,直接跳出
            if (temp <= array[childIndex])
                break;
            //無需真正交換,單向賦值即可
            array[parentIndex] = array[childIndex];
            parentIndex = childIndex;
            childIndex = 2 * childIndex + 1;
        }
        array[parentIndex] = temp;
    }

    /**
     * 構建堆
     * @param array     待調整的堆
     */
    public static void buildHeap(int[] array) {
        // 從最後一個非葉子節點開始,依次下沉調整
        for (int i = (array.length-2)/2; i >= 0; i--) {
            downAdjust(array, i, array.length);
        }
    }

    public static void main(String[] args) {
        int[] array = new int[] {1,3,2,6,5,7,8,9,10,0};
        upAdjust(array);
        System.out.println(Arrays.toString(array));

        array = new int[] {7,1,3,10,5,2,8,9,6};
        buildHeap(array);
        System.out.println(Arrays.toString(array));
    }
}

        代碼中有一個優化的點,就是在父節點和孩子節點做連續交換時,並不一定要真的交 換,只需要先把交換一方的值存入temp變量,做單向覆蓋,循環結束後,再把temp的值存入交換後的最終位置即可。

2.5 二叉堆的作用

        二叉堆是實現堆排序優先隊列的基礎。有一道很經典的算法題,在一個無序數組,要求找出數組中第k大的元素,這個就可以用二叉堆巧妙解決。下面,我們就來學習優先隊列
        

3、優先隊列

        既然優先隊列中出現了“隊列”兩個字,那讓我們先來回顧一下之前所介紹的隊列的特性。

        在之前的章節中已經講過,隊列的特點是先進先出(FIFO)

        入隊列,將新元素置於隊尾:

在這裏插入圖片描述
        出隊列,隊頭元素最先被移出:

在這裏插入圖片描述
        那麼,優先隊列又是什麼樣子呢?

        優先隊列不再遵循先入先出的原則,而是分爲兩種情況

  • 最大優先隊列,無論入隊順序如何,都是當前最大的元素優先出隊
  • 最小優先隊列,無論入隊順序如何,都是當前最小的元素優先出隊

        例如有一個最大優先隊列,其中的最大元素是8,那麼雖然8並不是隊頭元素,但出隊時仍然讓元素8首先出隊。
在這裏插入圖片描述
        要實現以上需求,利用線性數據結構並非不能實現,但是時間複雜度較高。

3.1 優先隊列的實現

        先來回顧一下二叉堆的特性。

        1. 最大堆的堆頂是整個堆中的最大元素。

        2. 最小堆的堆頂是整個堆中的最小元素。

        因此,可以用最大堆來實現最大優先隊列,這樣的話,每一次入隊操作就是堆的插入操作,每一次出隊操作就是刪除堆頂節點。

        入隊操作具體步驟如下。

        1、插入新節點5。

在這裏插入圖片描述
        2. 新節點5“上浮”到合適位置。
在這裏插入圖片描述
        出隊操作具體步驟如下。

        1. 讓原堆頂節點10出隊。

在這裏插入圖片描述
        2. 把最後一個節點1替換到堆頂位置。

在這裏插入圖片描述
        3. 節點1“下沉”,節點9成爲新堆頂。
在這裏插入圖片描述

二叉堆節點“上浮”和“下沉”的時間複雜度都是O(logn),所以優先隊列入隊和出隊的時間複雜度也是O(logn)!

3.2 優先隊列的代碼

public class PriorityQueue {

    private int[] array;
    private int size;

    public PriorityQueue(){
        //隊列初始長度32
        array = new int[32];
    }

    /**
     * 入隊
     * @param key  入隊元素
     */
    public void enQueue(int key) {
        //隊列長度超出範圍,擴容
        if(size >= array.length){
            resize();
        }
        array[size++] = key;
        upAdjust();
    }

    /**
     * 出隊
     */
    public int deQueue() throws Exception {
        if(size <= 0){
            throw new Exception("the queue is empty !");
        }
        //獲取堆頂元素
        int head = array[0];
        //最後一個元素移動到堆頂
        array[0] = array[--size];
        downAdjust();
        return head;
    }

    /**
     * 上浮調整
     */
    private void upAdjust() {
        int childIndex = size-1;
        int parentIndex = (childIndex-1)/2;
        // temp保存插入的葉子節點值,用於最後的賦值
        int temp = array[childIndex];
        while (childIndex > 0 && temp > array[parentIndex])
        {
            //無需真正交換,單向賦值即可
            array[childIndex] = array[parentIndex];
            childIndex = parentIndex;
            parentIndex = (parentIndex-1) / 2;
        }
        array[childIndex] = temp;
    }

    /**
     * 下沉調整
     */
    private void downAdjust() {
        // temp保存父節點值,用於最後的賦值
        int parentIndex = 0;
        int temp = array[parentIndex];
        int childIndex = 1;
        while (childIndex < size) {
            // 如果有右孩子,且右孩子大於左孩子的值,則定位到右孩子
            if (childIndex + 1 < size && array[childIndex + 1] > array[childIndex]) {
                childIndex++;
            }
            // 如果父節點大於任何一個孩子的值,直接跳出
            if (temp >= array[childIndex])
                break;
            //無需真正交換,單向賦值即可
            array[parentIndex] = array[childIndex];
            parentIndex = childIndex;
            childIndex = 2 * childIndex + 1;
        }
        array[parentIndex] = temp;
    }

    /**
     * 隊列擴容
     */
    private void resize() {
        //隊列容量翻倍
        int newSize = this.size * 2;
        this.array = Arrays.copyOf(this.array, newSize);
    }

    public static void main(String[] args) throws Exception {
        PriorityQueue priorityQueue = new PriorityQueue();
        priorityQueue.enQueue(3);
        priorityQueue.enQueue(5);
        priorityQueue.enQueue(10);
        priorityQueue.enQueue(2);
        priorityQueue.enQueue(7);
        System.out.println("出隊元素:" + priorityQueue.deQueue());
        System.out.println("出隊元素:" + priorityQueue.deQueue());
    }
}

        上述代碼採用數組來存儲二叉堆的元素,因此當元素數量超過數組長度時,需要進行擴容來擴大數組長度。


        本篇博客中代碼和彩圖來源於《漫畫算法》,應本書作者要求,加上本書公衆號《程序員小灰》二維碼。
在這裏插入圖片描述
        感興趣的朋友可以去購買正版實體書,確實不錯,非常適合小白入門。
在這裏插入圖片描述


小結

  • 什麼是二叉堆?

        二叉堆是一種特殊的完全二叉樹,分爲最大堆最小堆

        在最大堆中,任何一個父節點的值,都大於或等於它左、右孩子節點的值。

        在最小堆中,任何一個父節點的值,都小於或等於它左、右孩子節點的值。

  • 什麼是優先隊列

        優先隊列分爲最大優先隊列最小優先隊列

        在最大優先隊列中,無論入隊順序如何,當前最大的元素都會優先出隊,這是基於最大堆實現的。

        在最小優先隊列中,無論入隊順序如何,當前最小的元素都會優先出隊,這是基於最 小堆實現的。

        
        如果本文對您有所幫助,不妨點個贊支持一下博主🙏

        希望我們都能在學習的道路上越走越遠😉
在這裏插入圖片描述

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