二叉堆分爲兩種,最大堆和最小堆,咱們只討論最小堆的性質,最大堆具備相同的原理。
最小堆是一種符合下面兩個特性的樹形結構:java
從性質2能夠得出一個結論,最小堆的堆頂元素,必定是堆中最小的元素。
最小堆能夠支持如下幾種基本操做:git
這其中,peek的時間複雜度是O(1)
的,pop和push的操做是O(logN)
的,union操做是O(N+M)
的,其中N和M是堆元素個數。這裏先有個概念便可,下面會詳細分析這些操做。
由堆的這些性質能夠看出來,堆在處理動態數據的時候有很是大的優點,在不少優先級相關的場景下發揮着重大的做用。
堆刪除和新增元素後的維護操做,咱們分別稱之爲shiftUp
和shiftDown
,下圖展現了一個最小堆的添加和刪除過程。github
shiftDown
操做和添加元素後的
shiftUp
的時間上限和堆的高度有關係,二叉樹的高度是
logN
的,因此咱們說堆的pop和push的操做時間複雜度是
O(logN)
的。
談到樹結構,你們可能首先想到的是使用鏈表的方式存儲,確實使用引用關聯節點的方式用起來很是靈活。可是對於徹底二叉樹來講,使用數組存儲是一種更加優雅的實現方法。一樣使用數組實現二叉堆是一種很是經典的實現,下面咱們就來談談如何使用數組實現二叉堆的。
先看一幅圖算法
左子節點下標 = 自身下標 * 2
,右子節點下標 = 自身下標 * 2 + 1
父節點下標 = 自身下標 / 2
,這裏的除是向下取整的。若是你對這個結果持懷疑態度,可使用數學概括法證實一下,也比較簡單,這裏就不在贅述了
可能你注意到了,數組的起始下標是從1開始的,這也是一種比較經典的實現方式(能夠減小計算次數),數組的起始下標從0開始,也有類似的性質。api
解決了存取問題,咱們不妨看一下核心代碼的實現數組
我用Java代碼實現了一個二叉堆,詳細的代碼能夠在個人GitHub上能夠看到,下面是一些核心代碼。數據結構
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;
}
}
複製代碼
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;
}
}
}
複製代碼
從一個數組構建堆的操做,咱們姑且稱之爲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
java.util.PriorityQueue
和java.util.concurrent.PriorityBlockingQueue
這兩種優先級隊列都是用堆實現的。DelayedWorkQueue
其實也是一個堆的實現。原創不易,轉載請註明出處!www.yangxf.top/線程