優先隊列是一種抽象的數據類型,表示一組值和對這些值的操作,其中最重要的操作就是刪除最大元素和插入元素。
二叉堆數組中,每個元素都要保證大于等于另兩個特定位置的元素,相應這些位置的元素又至少要大于等于數組中的另兩個的元素。
如果將所有元素弄成一個二叉樹,靈魂畫手就覺得此事很簡單:
這是一個二叉樹,其中當它每一個節點都大于等于它的兩個子節點時,稱之為堆有序。
比如C D一定小于B B,E一定小于A。但E和B的大小比較不一定,可能E比D還小也說不定。唯一可確定的是,樹的根節點,A一定是最大的元素。
二叉堆表示法:
如果我們用指針來表示堆有序的二叉樹,那么每個元素都需要三個指針來找到它的上下結點,雖然可能不會全用上。如果我們采用完全二叉樹complete tree
(完全二叉樹從根結點到倒數第二層滿足完美二叉樹(完美就是都是有兩個子結點,而且高度填充一樣),最后一層可以不完全填充,其葉子結點都靠左對齊。)
甚至可以用數組直接表達,具體是層級表示,比如根節點在序號1,子節點則在序號2和3,以此類推(并不用序號0)
二叉堆就是一組堆有序的完全二叉樹排序的元素,在數組中按照層次存儲。一棵大小為N的完全二叉樹的高度為log2N下取整(只有一個父結點高度認為為0)
這些元素在數組中用層次存儲是有規律的,二叉堆中,位置k的結點的父結點的位置在(k/2下取整),它的兩個子結點的位置為2k和2k+1。
上浮實現堆有序化
如果堆有序狀態因為某個結點變得比它的父結點更大而被打破,這樣就要不斷交換它和它的父結點來修復有序狀態。這種就好像是小弟立了功就被人提拔上去的感覺。
private void swim(int k){
while(k>1&&(k/2)<k){
exchange(a,k,k/2);
k=k/2;
}
}
下沉實現堆有序化
父結點比子結點的數小,所以父結點要和它的子結點交換,直到它沒有子結點或者比子結點大為止。差不多就是上司犯錯然后被降級的感覺
private void sink(int k) {
while(2*k<=n) {
int j=2*k;
if(j<n&&a[j]<a[j+1])j++; //找到兩個子結點中較大的那個
if(a[k]>=a[j])break;
exchange(k, j);
k=j;
}
}
根據上面的sink和swim就可以實現對一個堆有序序列進行元素的增刪,并維護它的順序
public class MaxPQ {
private int n=0;
private int[] a;
public MaxPQ(int maxlength) {
a=new int[maxlength];
}
public int size() {
return n;
}
public void insert(int v) {
a[++n]=v;
swim(n);
}
public int delmax() { //這個很帥,將最大的拿出來讓最底層的結點放上面來然后下沉排序
int max=a[1];
exchange(1, n--);
sink(1);
return max;
}
private void swim(int k){
while(k>1&&(k/2)<k){
exchange(k,k/2);
k=k/2;
}
}
private void sink(int k) {
while(2*k<=n) {
int j=2*k;
if(j<n&&a[j]<a[j+1])j++;
if(a[k]>=a[j])break;
exchange(k, j);
k=j;
}
}
private void exchange(int i,int j) {
int temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
對于這個有幾點改進
- 這個可以用不定容數組代替定容數組
- 可以通過模擬多叉樹來實現多叉堆,比如三叉堆就是位置k的元素有3k-1、3k、3k+1三個結點。同理修改d叉樹也不是什么難事,我們需要在樹高和d個子結點找到折中。
- 添加索引,方便從大到小遍歷元素
堆排序
堆排序分成兩個階段,構造堆和下沉排序階段。
堆的構造分成兩種:一個是通過數組的從左往右遍歷數組調用swim,一種是從右往左遍歷數組調用sink方法。聽說好像后面那種更加聰明。
有一種堆排序的實現方式如下,開始時掃描數組一半的元素用sink方法,因為我們可以跳過大小為一的子堆,因為n/2-n這些元素都是沒有子結點的結點。
這里sink方法改了一下方法參數,但內容是一樣的
public static void sort(int [] a){
int n=a.length();
for(int k=n/2;k>=1;k--){
sink(a,k,n);
}
//上面方法結束后保證堆有序了
//下沉排序
while(n>1){
exch(a,1,n--); //交換位置且縮小堆
sink(a,1,n);
}
}
下沉排序主要工作是在第二階段完成的。這里我們將堆中最大元素刪了,放在縮小堆后空出來的位置(其實是交換了元素位置再縮小堆)。然后再sink
堆排序是唯一一個能同時最優利用時間和空間的排序。在最壞的情況下也能保證2Nlogn的比較和恒定額外的空間。但是它很少利用緩存(啥意思?)