資料結構——優先佇列與堆
什麼是優先佇列?
普通佇列:先進先出,後進後出 優先佇列:出隊順序和入隊順序無關;和優先順序相關,如醫院中,病重先安排病床
優先佇列的現例項子:
①cpu任務排程,動態選擇優先順序最高的任務執行 ②王者榮耀中,攻擊鍵優先打敵人,無敵人時候優先打最近的小兵
關於優先佇列的實現:
普通的線性結構和堆都可以作為優先佇列的底層實現。
堆實現優先佇列,在最壞的情況下,入隊與出隊的操作,時間複雜都為O(logn)
二叉堆
①堆是一棵被完全二叉樹
②堆中某個節點的值總是不大於其父節點的值,圖示為最大堆
補充:在堆中,所有節點的值都大於他們的孩子節點,那麼層次比較低的節點一定大於層次比較高的節點呢?
不一定,如16、13和19。
可以使用一個數組表示而不需要使用鏈,陣列如圖所示:
父子索引之間的關係
parent(i) = i / 2;
left child(i) = 2 * i;
right child(i) = 2 * i + 1;
索引也可以從0開始,但公式有所變化
父子索引之間的關係
parent(i) = (i - 1 ) / 2;
left child(i) = 2 * i + 1;
right child(i) = 2 * i + 2;
大根堆中
向堆中新增一個元素(Sift Up上浮)
先新增,再上浮【滿足父節點一定大於新新增的節點,否則(while)兩者交換位置,並更新索引值】
/** * 向堆中新增元素 */ public void add(T t) { data.addLast(t); siftUp(data.getSize() - 1); } //上浮 private void siftUp(int i) { while (i > 0 && data.get(i).compareTo(data.get(parent(i))) > 0) { //交換位置 data.swap(i, parent(i)); //更新i值 i = parent(i); } }
堆中移除最大一個元素後(Sift Up下沉)
移除最大元素後,實際上變為了52和30為根節點的子樹,為了簡化操作,將索引值最大的元素size-1複製到索引0的位置,再刪除size-1的元素,這樣總的元素個數就保證了。
那麼造成一個問題,16元素作為父節點並不大於它的兩個子節點,所以需要有Sift Up下沉的動作,直到滿足父節點元素值大於子節點元素值。
每次我們需要下沉的元素都和子孩子比較,選擇兩個孩子中最大的元素,如果最大的元素比父節點更大的話,則子節點與父節點交換位置,此時父節點52就一定比左右節點都大了。
此時再將16與他的最大子節點進行比較,如果仍不滿足父節點大於子節點的話,繼續交換位置。
此時再次比較16所處位置的子節點,滿足父節點大於子節點,不需要交換位置,下沉操作結束Sift down。
/**
* 檢視堆中最大元素
*/
public T findMax() {
if (data.getSize() == 0)
throw new IllegalArgumentException("堆中沒有元素");
return data.get(0);
}
/**
* 取出堆中最大元素
*/
public T extractMax() {
T t = findMax();
/**
* 將堆中最大的元素刪除
* 1、將最後一個元素與第一個元素交換位置
* 2、刪除最後一個元素
* 3、對索引0元素執行siftDown過程
*/
data.swap(0, data.getSize() - 1);
data.removeLast();
siftDown(0);
return t;
}
/**
* 元素下沉操作(大值上浮,小值下沉)
* 0、當該節點下是否具有左子節點:
* 1、如果該節點存在具備右子節點,且右子節點大於左子節點,記錄下右子節點索引
* 2、如果不存在右子節點或右子節點小於左子節點,記錄下左子節點索引
* 3、此時記錄下的索引值一定是最大值,與其父節點做比較,如果父節點小於該值,交換值並更新索引index
*/
private void siftDown(int index) {
//0
while (leftChild(index) < data.getSize()) {
int tag = leftChild(index);
//1
if (tag + 1 < data.getSize() && data.get(tag + 1).compareTo(data.get(tag)) > 0) {
tag ++;
}
//2、3 tag
if(data.get(tag).compareTo(data.get(index)) > 0){
data.swap(tag,index);
index = tag;
}
}
}
add和extractMax時間複雜度都是O(logn),同二叉樹的高度。因為堆是一個完全二叉樹,永遠不會退化成一個連結串列。
replace
取出最大的元素後,再放入一個新的元素。
實現思路:①先執行一次extractMax的操作,再執行一次add操作,實際上是兩次O(logn)的操作
②可以將堆頂的元素替換為新增元素,再呼叫一次siftDown,實際上是一次O(logn)操作
/**
* 先取出堆中最大元素,再新增新元素
* 將堆頂的元素替換為新增元素,再呼叫一次siftDown()
*/
public T replace(T t){
T t1 = findMax();
data.add(0,t);
siftDown(0);
return t1;
}
heapify
將任意陣列整理成堆的形狀
①將給定的陣列看成一個完全二叉樹
②從最後一個非葉子節點開始下沉siftDown(),即從索引4位置不斷向前執行siftDown()操作
plus:最後一個非葉子節點的索引為:獲得最後一個索引,計算出其父節點索引。
對於索引從0開始的完全二叉樹,其父節點索引為(data.getSize() - 1 ) / 2,從1開始為data.getSize() / 2
③索引4上的22小於其子節點(索引為9的62),交換位置,下沉操作結束。再看索引為3所在節點,其左右節點最大值為41大於13,則41與13交換位置,下沉操作結束。再看索引為2所在節點,其左右節點最大值為28大於19,則28與19交換位置,下沉結束
④對於索引為1的節點17,它的最大子節點為62大於17,則62與17交換位置,此時17來到了索引為4的位置,由於17下還有子節點22且大於17,17與22交換位置,此時17來到了索引為9的位置。
⑤索引為0的節點15,其最大子節點為62大於15,則15與62交換位置,此時15來到了索引為1的位置,由於其下面還有子節點,索引為1下的子節點最大值為41大於15,則41與15交換位置,此時15來到了索引為3的位置,由於其下面還有子節點,接著下沉索引為3下的子節點最大值為30大於15,30與15交換位置,此時15來到了索引為7的位置,由於索引為7不存在子節點,下沉結束。
自此位置,heapify過程完成了,陣列變成了一個最大堆。我們在heapfiy之前就排除了所有葉子節點,相當於一次拋棄了一半的節點,再進行siftDown操作,相比從一個空堆開始一個個新增元素要快很多(因為對每一個元素都要執行O(logn)的操作,相當於O(nlogn)級別的操作,而heapify的過程,時間複雜度為0(n)級別)
//Array類中
public Array(E[] arr){
data = (E[]) new Object[arr.length];
for(int i = 0 ; i < arr.length ; i ++)
data[i] = arr[i];
size = arr.length;
}
public MaxHeap(T[] arr){
//構建一個完全二叉樹
data = new Array<>(arr);
//從最後一個非葉子節點開始向前siftDown
for(int i = parent(arr.length - 1) ; i >= 0 ; i--)
siftDown(i);
}