優先級隊列的特征在於刪除最大值和插入操作。
初級實現
- 數組實現(無序):惰性方法,僅在必要的時候找出最大元素;
- 數組實現(有序):積極方法:在插入時就保持列表有序,使后續操作更高效;
- 鏈表表示法
數據結構 | 插入元素 | 刪除最大元素 |
---|---|---|
有序數組 | N | 1 |
無序數組 | 1 | N |
堆 | logN | logN |
理想情況 | 1 | 1 |
在上述優先隊列的初級實現中,刪除最大元素和插入元素這兩個操作之一在最壞情況下需要線性時間來完成。
堆的定義
1. 完全二叉樹(從上到下,從左到右)
2. 一棵二叉樹的每個結點都大於等於它的兩個子結點時,它被稱為堆有序
二叉堆表示法
如果使用指針來表示堆有序的二叉樹,那么每個元素都需要三個指針來找到它的上下結點。
但使用完全二叉樹,只需要數組而不需要指針就可以表示,十分方便。
具體方法是將二叉樹的結點按照層級順序放入數組中。(下圖_根節點從1開始)
位置 k | 父結點位置 | 兩子結點位置 |
---|---|---|
根節點從1開始 | k/2 | 2k,2k+1 |
根節點從0開始 | (k-1)/2 | 2k+2,2k+1 |
高性能的原因:利用在數組中無需指針即可沿樹上下移動的便利。
堆的算法
我們在長度為 n + 1 的私有數組 pq []中表示大小為 N 的堆,其中pq [0]未使用且堆在pq [1]到pq [n]中。我們只通過函數less() 和exch()來訪問元素。
堆有序化
打破堆的狀態,然后再遍歷堆並按照要求將堆的狀態恢復。
由下至上的堆序列化(上浮)
private void swim(int n) {
// 第一層 父節點小於子節點
while (n > 1 && less(n / 2, n)) {
exch(n / 2, 2);
//n=父節點
n = n / 2;
}
}
由上至下的堆序列化(下沉)
private void sink(int k){
//子節點小於等於最后一個
while(2 * k <= N){
int j = 2 * k;
if(j <N && less(j,j + 1))j ++;//如果第二個子節點大,就取大的
if(!less(k,j))break;//跟父節點比對,父節點大就break;
exch(k,j);//交換后 k = j 繼續下沉
k = j;
}
}
插入元素時,將新元素加到數組末尾,增加堆的大小並讓這個新元素上浮到合適的位置。
刪除最大元素時,從數組頂端刪去最大的元素並將數組的最后一個元素放到頂端,減小堆的大小並讓這個元素下沉到合適的位置。
api&實現
public class MaxPQ<T extends Comparable<T>> {
private boolean less(int i, int j) {
return pq[i].compareTo(pq[j]) < 0;
}
private void exch(int i, int j) {
T t = pq[i];
pq[i] = pq[j];
pq[j] = t;
}
private T[] pq;
private int N = 0; //存放到[1-N]中 0沒使用
public MaxPQ(int maxN) {
pq = (T[]) new Comparable[maxN + 1];
}
public boolean isEmpty() {
return N == 0;
}
public int size() {
return N;
}
public T delMax() {
T max = pq[1];
exch(1, N--);
pq[N + 1] = null;//防止對象游離態
//下沉
sink(1);
return max;
}
private void sink(int i) {
while (2 * i <= N) {
int j = 2 * i;
if (j < N && less(j, j + 1)) {
j++;
}
if (!less(i, j)) {
break;
}
exch(i, j);
i = j;
}
}
public void insert(T n) {
pq[++N] = n;
//上浮
swim(N);
}
private void swim(int n) {
while (n > 1 && less(n / 2, n)) {
exch(n / 2, 2);
n = n / 2;
}
}
}
特點
在某些數據處理的場合,總數據量太大(可以認為輸入是無限的),
無法排序(甚至無法全部裝進內存)。如果將每個新的輸入和已知的 M 個最大(或最小)元素比較,除非 M 較小,
否則這種比較的代價會非常高昂。如果有了優先隊列,就只用一個能存儲 M 個元素的隊列即可。
利用在數組中無需指針即可沿樹上下移動的便利。
堆排序
為了方便書寫根節點從0開始
public static void sort(Comparable[] a) {
int N = a.length - 1;//一共有N個 元素
/**
* 初始化堆
*/
for (int i = (N - 1) / 2; i >= 0; i--) {
sink(a, i, N);
}
/**
* 現在堆有序狀態
*/
while (N >= 0) {
exch(a, 0, N--);
sink(a, 0, N);
}
}
public static void sink(Comparable[] a, int k, int n) {
while (2 * k + 1 <= n) {
int j = 2 * k + 1;
if (j < n && less(a, j, j + 1)) {
j++;
}
if (!less(a, k, j)) {
break;
}
exch(a, j, k);
k = j;
}
}
public static boolean less(Comparable[] a, int j, int i) {
return a[j].compareTo(a[i]) < 0;
}
這里我們將堆中的最大元素刪除,然后放入堆縮小后數組中空出的位置。
這個過程和選擇排序有些類似(按照降序而非升序取出所有的元素),但所需的比較要少很多,因為堆提供了一種從未排序部分找到最大元素的有效方法。