數據結構基礎篇-二叉堆

二叉堆分爲兩種,最大堆和最小堆,咱們只討論最小堆的性質,最大堆具備相同的原理。
最小堆是一種符合下面兩個特性的樹形結構:java

  1. 最小堆是一顆徹底二叉樹,即最小堆的每一個節點要麼沒有子節點,要麼只有一個左子節點,要麼有兩個子節點。
  2. 最小堆的每一個節點都小於等於它的子節點。

堆的基本操做

從性質2能夠得出一個結論,最小堆的堆頂元素,必定是堆中最小的元素。
最小堆能夠支持如下幾種基本操做:git

  1. pop,移除堆頂元素,而後堆要作一些操做,維護堆的性質,這樣保證了每次都能pop出堆中最小元素
  2. peek,查看堆頂元素
  3. push,向堆中添加元素,這個元素要被移動到合適的位置,以維護堆的性質
  4. union,將兩個堆合併成一個堆,須要額外的操做維護堆的性質

這其中,peek的時間複雜度是O(1)的,pop和push的操做是O(logN)的,union操做是O(N+M) 的,其中N和M是堆元素個數。這裏先有個概念便可,下面會詳細分析這些操做。
由堆的這些性質能夠看出來,堆在處理動態數據的時候有很是大的優點,在不少優先級相關的場景下發揮着重大的做用。
堆刪除和新增元素後的維護操做,咱們分別稱之爲shiftUpshiftDown,下圖展現了一個最小堆的添加和刪除過程。github

最小堆維護圖
從上圖能夠直觀的看出,刪除堆頂元素後的 shiftDown操做和添加元素後的 shiftUp的時間上限和堆的高度有關係,二叉樹的高度是 logN的,因此咱們說堆的pop和push的操做時間複雜度是 O(logN)的。

堆的存儲

談到樹結構,你們可能首先想到的是使用鏈表的方式存儲,確實使用引用關聯節點的方式用起來很是靈活。可是對於徹底二叉樹來講,使用數組存儲是一種更加優雅的實現方法。一樣使用數組實現二叉堆是一種很是經典的實現,下面咱們就來談談如何使用數組實現二叉堆的。
先看一幅圖算法

最小堆存儲圖
從圖中淡藍色的索引不難發現,數組中元素是按照二叉樹的廣度優先遍歷順序存儲的,因爲堆是徹底二叉樹,因此存儲數組中元素必定是連續的。
存的問題解決了,那怎麼讀取呢,堆的基本操做須要頻繁的獲取當前節點的父節點或者子節點的。基於鏈表的實現方式很是簡單的就能經過引用獲取,那麼基於數組的實現又如何獲取呢?
咱們仍是重點關注上圖中淡藍色字體標註的樹節點下標,不難發現:

  1. 每一個節點的左子節點下標 = 自身下標 * 2右子節點下標 = 自身下標 * 2 + 1
  2. 每一個節點父節點下標 = 自身下標 / 2,這裏的除是向下取整的。

若是你對這個結果持懷疑態度,可使用數學概括法證實一下,也比較簡單,這裏就不在贅述了
可能你注意到了,數組的起始下標是從1開始的,這也是一種比較經典的實現方式(能夠減小計算次數),數組的起始下標從0開始,也有類似的性質。api

解決了存取問題,咱們不妨看一下核心代碼的實現數組

堆的實現

我用Java代碼實現了一個二叉堆,詳細的代碼能夠在個人GitHub上能夠看到,下面是一些核心代碼。數據結構

  1. 下面的代碼展現了,從下標i開始作shiftUp操做
private void shiftUp(final int i) {
    for (int c = i; ; ) {
        // 取父節點下標p
        int p = c >>> 1;
        // 若是p<1表示遍歷到了堆頂,
        // compare(c, p) >= 0表示當前元素大於等於父節點
        // 這兩種狀況都表示堆性質已經恢復,須要跳出循環
        if (p < 1 || 
            compare(c, p) >= 0) {
            break;
        }
        // 交換兩個元素
        swap(c, p);
        c = p;
    }
}
複製代碼
  1. 下面的代碼展現了,從下標i開始作shiftDown操做
private void shiftDown(final int i) {
    for (int c = i; ; ) {
        // 獲取左右子節點下標
        int l = c << 1, r = l + 1;
        // 右子節點存在,就和兩個子節點中較小的比較
        if (r <= size) {
            int ch = compare(l, r) < 0 ? l : r;
            if (compare(c, ch) <= 0) {
                break;
            }
            swap(c, ch);
            c = ch;
        } else if (l <= size &&
                   compare(c, l) > 0) {
            swap(c, l);
            c = l;
        } else {
            // 循環到底的狀況
            break;
        } 
    }
}
複製代碼

堆的heapfiy

從一個數組構建堆的操做,咱們姑且稱之爲heapify。一種很容易想到的方法是,直接遍歷數組push到一個空堆中,這種作法也有着不錯的時間複雜度(O(NlogN))。不過還有更優雅的作法,這種作法的時間複雜度是O(N)的。下面heapify代碼很是簡單,就是從數組中間向前遍歷,依次作shiftDown操做,爲何要從size ÷ 2開始遍歷呢?由於從這個下標開始往前的節點纔有孩子節點,此時作shiftDown是有意義的。這個heapify操做的時間複雜度上界也是O(NlogN)的,不過漸漸時間複雜度是O(N)的,具體證實過程,你們能夠參照《算法導論》。字體

private void heapify() {
    int lastParent = size >>> 1;
    for (int i = lastParent; i >= 1; i--) {
        shiftDown(i);
    }
}
複製代碼

堆的應用

堆是一種應用比較普遍的數據結構,在不少地方都有應用。下面咱們就舉兩個Java中的例子。spa

  1. 優先級隊列,普通的隊列是先進先出,而優先級隊列的出隊老是優先級最高的元素,這很符合堆的特色。Java中提供的java.util.PriorityQueuejava.util.concurrent.PriorityBlockingQueue這兩種優先級隊列都是用堆實現的。
  2. Java定時任務線程池ScheduledThreadPoolExecutor中的工做隊列DelayedWorkQueue其實也是一個堆的實現。

原創不易,轉載請註明出處!www.yangxf.top/線程

相關文章
相關標籤/搜索