優先隊列
我們經常會碰到下面這種情況,並不需要將所有數據排序,只需要取出數據中最大(或最小)的幾個元素,如排行榜。
那么這種情況下就可以使用優先隊列,優先隊列是一個抽象數據類型,最重要的操作就是刪除最大元素和插入元素,插入元素的時候就順便將該元素排序(其實是堆有序,后面介紹)了。
二叉堆
二叉堆其實是優先隊列的一種實現,下面主要講的是用數組實現二叉堆。
先上一個實例:
如有一個數組A{9,7,8,3,0,6,5,1,2}
用二叉樹來表示數組更直觀:

從這張圖我們可以總結一些規律:
- 當一個二叉樹的每個結點都大於等於它的兩個子節點時,稱為堆有序
- 根節點是堆有序的二叉樹中的最大結點
- 在數組中,位置為K的結點的父節點,位置為K/2,它的兩個子節點位置分別為:2K和2K+1(下標從1開始,A[0]不使用)
上面這三點應該非常好理解
下面就引出一個問題,怎樣讓一個數組變成堆有序呢?
首先,需要介紹兩個操作:
- 由下至上的堆有序化(上浮)
當插入一個結點,或改變一個結點的值時,上浮指的是交換它和它的父節點以達到堆有序
在上面的堆有序的圖中,如果我們把0換成10,那么上浮的操作具體為:
(1)10比它的父節點7大,所以交換
(2)交換后,10比它的父節點9還要打,交換
之后得到的二叉樹如下圖:

代碼如下(需要注意,下標是從1開始,A[0]保留不用,以下所有代碼相同):
//index based on 1 public void swim(Integer[] a,Integer key) { while(key > 1 && a[key/2] < a[key]) { change(a,key/2,key); key /= 2; } }
2. 由上至下的堆有序化(下沉)
由上浮可以很容易得出下沉的概念:
當插入一個結點,或改變一個結點的值時,下沉指的是交換它和它的較大子節點以達到堆有序。
在原來的二叉樹中,如果將根節點9換成4,操作如下:
(1)4與它的最大子節點8交換位置
(2)4與它的最大子節點6交換位置
交換后的二叉樹如下圖:

代碼如下:
//index based on 1 public void sink(Integer[] a,Integer key) { Integer max = key*2; while(key*2 < a.length - 1) { if(a[key*2] < a[key*2 + 1]) { max = key*2 + 1; } else { max = key*2; } if(a[key] > a[max]) break; change(a,key,max); key = max; } }
那么將一個數組構造成有序堆,相應的也有兩種方法:使用上浮以及使用下沉:
初始數組如下:
Integer[] a = {null,2,1,5,9,0,6,8,7,3};
上浮構造有序堆:
從數組左邊到右邊依次使用上浮,因為根節點A[1]沒有父節點,所以從A[2]開始:
public void buildBinaryHeapWithSwim(Integer[] a) { for(int k=2;k<a.length;k++) { swim(a,k); } }
結果如下:
a: [null ,9 ,7 ,8 ,5 ,0 ,2 ,6 ,1 ,3] 讀者有興趣可以自己畫一下二叉樹,看是否有序
下沉構造有序堆:
代碼: public void buildBinaryHeapWithSink(Integer[] a) { //index based on 1 for(int k=a.length/2;k>=1;k--) { sink(a,k); } }
為什么使用下沉只需要遍歷數組左半邊呢?
因為對於一個數組,每一個元素都已經是一個子堆的根節點了,sink()對於這些自對也適用。如果一個結點的兩個子節點都已經是有序堆了,那么在該結點上調用sink(),可以讓整個數組變成有序堆,這個過程會遞歸的建立起有序堆的秩序。我們只需要掃描數組中一半的元素,跳過葉子節點。
a: [null ,9 ,7 ,8 ,3 ,0 ,6 ,5 ,1 ,2]
可以看到使用下沉和上浮構造出來的有序堆並不相同,那么用哪一個更好呢?
答案是使用下沉構造有序堆更好,構造一個有N個元素的有序堆,只需少於2N次比較以及少於N次交換。
證明過程就略過了。
堆排序
前面說了那么多,終於要說到堆排序了,其實前面的優先隊列和二叉堆都是為了堆排序做准備。
現在我們知道如果將一個數組構造成有序堆的話,那么數組中最大的元素就是有序堆的根節點。
那么很容易想到一個排序的思路:
第一種:將數組構造成有序堆,將根節點拿出來,即將A[1]拿出(因為A[0]不用,當然也可以使用,讀者可以自己編程實現),對剩下的數組再構造有序堆……
不過第一種思路只能降序排列,並且需要構造一個數組用來存放取出的最大元素,以及最大的弊端是取出最大元素后,數組剩下的其它所有元素需要左移。
那么第二種辦法就可以避免以上的問題:
第二種:先看圖:
先來解釋下這幅圖:
- 一開始先將數組構造成一個有序二叉堆,如圖1
- 因為有序二叉堆的最大元素就是根節點,將根節點和最后一個元素交換。
- 從index=1到index=a.lenth-1開始調用sink方法重新構造有序二叉堆。(即第二步交換過的最大元素不參與這次的構造)
- 經過第三步后,得到數組中第二大的元素即為根節點。
-
再次交換根節點和倒數第二個元素
…….
這樣循環下去,即得到按升序排序的數組
代碼:
public void heapSort(Integer[] a) { for(int k=a.length/2;k>=1;k--) { sink(a,k); } Integer n = a.length - 1; while(n > 0) { change(a,1,n--); //去除最后一個元素,即前一個有序堆的最大元素 sink(a,1,n); } }
注意在while循環中,sink()方法多了一個參數,這個參數的目的是去掉上一個有序堆的最大元素。
全部代碼如下:
public class HeapSort extends SortBase { /* (non-Javadoc) * @see Sort.SortBase#sort(java.lang.Integer[]) */ @Override public Integer[] sort(Integer[] a) { // TODO Auto-generated method stub print("init",a); heapSort(a); print("result",a); return null; } public void buildBinaryHeapWithSink(Integer[] a) { //index based on 1 for(int k=a.length/2;k>=1;k--) { sink(a,k); } } public void buildBinaryHeapWithSwim(Integer[] a) { for(int k=2;k<a.length;k++) { swim(a,k); } } public void heapSort(Integer[] a) { for(int k=a.length/2;k>=1;k--) { sink(a,k); } Integer n = a.length - 1; while(n > 0) { change(a,1,n--); //去除最后一個元素,即前一個有序堆的最大元素 sink(a,1,n); } } //index based on 1 public void swim(Integer[] a,Integer key) { while(key > 1 && a[key/2] < a[key]) { change(a,key/2,key); key /= 2; } } //index based on 1 public void sink(Integer[] a,Integer key) { Integer max = key*2; while(key*2 < a.length - 1) { if(a[key*2] < a[key*2 + 1]) { max = key*2 + 1; } else { max = key*2; } if(a[key] > a[max]) break; change(a,key,max); key = max; } } public void sink(Integer[] a,Integer key,Integer n) { Integer max = key*2; while(key*2 < n) { if(a[key*2] < a[key*2 + 1]) { max = key*2 + 1; } else { max = key*2; } if(a[key] > a[max]) break; change(a,key,max); key = max; } } public static void main(String[] args) { Integer[] a = {null,2,1,5,9,0,6,8,7,3}; //(new HeapSort()).sort(a); (new HeapSort()).buildBinaryHeapWithSink(a); print("a",a); } }
堆排序的平均時間復雜度為NlogN
