最詳細版圖解優先隊列(堆)

1、隊列與優先隊列的區別

  1. 隊列是一種FIFO(First-In-First-Out)先進先出的數據結構,對應於生活中的排隊的場景,排在前面的人老是先經過,依次進行
  2. 優先隊列是特殊的隊列,從「優先」一詞,可看出有「插隊現象」。好比在火車站排隊進站時,就會有些比較急的人來插隊,他們就在前面先經過驗票。優先隊列至少含有兩種操做的數據結構:insert(插入),即將元素插入到優先隊列中(入隊);以及deleteMin(刪除最小者),它的做用是找出、刪除優先隊列中的最小的元素(出隊)。
優先隊列
優先隊列

2、優先隊列(堆)的特性

  • 優先隊列的實現常選用二叉堆在數據結構中,優先隊列通常也是指堆java

  • 堆的兩個性質:數組

  1. 結構性堆是一顆除底層外被徹底填滿的二叉樹,底層的節點從左到右填入,這樣的樹叫作徹底二叉樹。數據結構

  2. 堆序性:因爲咱們想很快找出最小元,則最小元應該在根上,任意節點都小於它的後裔,這就是小頂堆(Min-Heap);若是是查找最大元,則最大元應該在根上,任意節點都要大於它的後裔,這就是大頂堆(Max-heap)。app

    結構性:

    完成二叉樹
    完成二叉樹

經過觀察發現,徹底二叉樹能夠直接使用一個數組表示而不須要使用其餘數據結構。因此咱們只須要傳入一個size就能夠構建優先隊列的結構(元素之間使用compareTo方法進行比較)。測試

public class PriorityQueue<T extends Comparable<? super T>> 
    public PriorityQueue(int capacity) {
        currentSize = 0;
        array = (T[]) new Comparable[capacity + 1];
    }
}
徹底二叉樹的數組實現
徹底二叉樹的數組實現

對於數組中的任意位置 i 的元素,其左兒子在位置 2i 上,則右兒子2i+1 上,父節點在 在 i/2(向下取整)上。一般從數組下標1開始存儲,這樣的好處在於很方便找到左右、及父節點。若是從0開始,左兒子在2i+1,右兒子在2i+2,父節點在(i-1)/2(向下取整)。ui

堆序性:

咱們這創建最小堆,即對於每個元素X,X的父親中的關鍵字小於(或等於)X中的關鍵字,根節點除外(它沒有父節點)。spa

堆

如圖所示,只有左邊是堆,右邊紅色節點違反堆序性。根據堆序性,只須要常O(1)找到最小元。3d

3、基本的堆操做

  1. insert(插入)
  • 上濾爲了插入元素X,咱們在下一個可用的位置創建空穴(不然會破壞結構性,不是徹底二叉樹)。若是此元素放入空穴不破壞堆序性,則插入完成;不然,將父節點下移到空穴,即空穴向根的方向上冒一步。繼續該過程,直到X插入空穴爲止。這樣的過程稱爲上濾。
創建空穴
創建空穴
完成插入
完成插入

圖中演示了18插入的過程,在下一個可用的位置創建空穴(知足結構性),發現不能直接插入,將父節點移下來,空穴上冒。繼續這個過程,直到知足堆序性。這樣就實現了元素插入到優先隊列(堆)中。code

  • java實現上濾
     /**
     * 插入到優先隊列,維護堆序性
     *
     * @param x :插入的元素
     */

    public void insert(T x) {
        if (null == x) {
            return;
        }
        //擴容
        if (currentSize == array.length - 1) {
            enlargeArray(array.length * 2 + 1);
        }
        //上濾
        int hole = ++currentSize;
        for (array[0] = x; x.compareTo(array[hole / 2]) < 0; hole /= 2) {
            array[hole] = array[hole / 2];
        }
        array[hole] = x;
    }

    /**
     * 擴容方法
     *
     * @param newSize :擴容後的容量,爲原來的2倍+1
     */

    private void enlargeArray(int newSize) {
        T[] old = array;
        array = (T[]) new Comparable[newSize];
        System.arraycopy(old, 0, array, 0, old.length);
    }

能夠反覆使用交換操做來進行上濾過程,但若是插入X上濾d層,則須要3d次賦值;咱們這種方式只須要d+1次賦值。orm

若是插入的元素是新的最小元從而一直上濾到根處,那麼這種插入的時間長達O(logN)。但平均來看,上濾終止得要早。業已證實,執行依次插入平均須要2.607次比較,所以平均insert操做上移元素1.607層。上濾次數只比插入次數少一次。

  1. deleteMin(刪除最小元)
  • 下濾:相似於上濾操做。由於咱們創建的是最小堆,因此刪除最小元,就是將根節點刪掉,這樣就破壞告終構性。因此咱們在根節點處創建空穴,爲了知足結構性,堆中最後一個元素X必須移動到合適的位置,若是能夠直接放到空穴,則刪除完成(通常不可能);不然,將空穴的左右兒子中較小者移到空穴,即空穴下移了一層。繼續這樣的操做,直到X能夠放入到空穴中。這樣就能夠知足結構性與堆序性。這個過程稱爲下濾。
刪除最小元
刪除最小元
完成刪除最小元
完成刪除最小元

如圖所示:在根處創建空穴,將最後一個元素放到空穴,已知足結構性;爲知足堆序性,須要將空穴下移到合適的位置。

注意:堆的實現中,常常發生的錯誤是只有偶數個元素即有一個節點只有一個兒子。因此須要測試右兒子的存在性。

/**
     * 刪除最小元
     * 若優先隊列爲空,拋出UnderflowException
     *
     * @return :返回最小元
     */

    public T deleteMin() {
        if (isEmpty()) {
            throw new UnderflowException();
        }

        T minItem = findMin();
        array[1] = array[currentSize--];
        percolateDown(1);

        return minItem;
    }

     /**
     * 下濾方法
     *
     * @param hole :從數組下標hole1開始下濾
     */

    private void percolateDown(int hole) {
        int child;
        T tmp = array[hole];

        for (; hole * 2 <= currentSize; hole = child) {
            //左兒子
            child = hole * 2;
            //判斷右兒子是否存在
            if (child != currentSize &&
                    array[child + 1].compareTo(array[child]) < 0) {
                child++;
            }
            if (array[child].compareTo(tmp) < 0) {
                array[hole] = array[child];
            } else {
                break;
            }
        }
        array[hole] = tmp;
    }

這種操做最壞時間複雜度是O(logN)。平均而言,被放到根處的元素幾乎下濾到底層(即來自的那層),因此平均時間複雜度是O(logN)。

4、總結

優先隊列常使用二叉堆實現,本篇圖解了二叉堆最基本的兩個操做:插入及刪除最小元。insert以O(1)常數時間執行,deleteMin以O(logN)執行。相信你們看了以後就能夠去看java的PriorityQueue源碼了。今天只說了二叉堆最基本的操做,還有一些額外操做及分析下次再說。好比,如何證實buildHeap是線性的?以及優先隊列的應用等。

聲明:圖文皆原創,若有轉載,請註明出處。若有錯誤,請幫忙指出,歡迎討論;若以爲能夠,點下推薦支持支持。

相關文章
相關標籤/搜索