h5 Video開啟本地攝像頭和離開頁面關閉攝像頭
什麼是堆
瞭解什麼是堆之前,我們知道佇列的概念,佇列的特點是先進先出,但是有一種特殊的佇列,取出元素的順序是按照元素的優先權(關鍵字)大小,而不是元素進入佇列的先後順序,這就是優先佇列(Priority Queue)。
若採用陣列或者連結串列實現優先佇列,總會有插入、刪除或者查詢中的一項操作的複雜度是$O(N)$ 的。
若採用二叉搜尋樹實現,那麼插入和刪除都跟樹的高度有關,也就是$O(log_2N)$ 的複雜度,但是刪除的時候,由於每次都要刪除最大的或者最小的,這樣操作幾次後,會造成搜尋樹失去平衡,所以不能簡單的使用二叉搜尋樹。
如果採用二叉樹結構,我們更關注的應該是刪除的操作,那麼我們把最大的值放到根結點,左右兩邊也是最大值作為左右子樹的根結點,每次刪除只需要刪除根結點。同時,為了保證樹的平衡性,可以考慮使用完全二叉樹來實現優先佇列。
優先佇列使用完全二叉樹表示如上圖所示,陣列的第 0 個元素空著,後面的按照層序遍歷的順序存放到陣列中。使用完全二叉實現的優先佇列,也可以稱之為堆,堆的特性如下:
- 結構性:用陣列表示的完全二叉樹。
- 有序性:任一結點的關鍵字是其子樹所有結點的最大值(或最小值)
- "最大堆",也稱 "大頂堆":堆頂元素是整個樹的最大值
- "最小堆",也稱"小頂堆":堆頂元素是整個樹的最小值
如下圖所示的幾個二叉樹,不是堆。
第一和第二棵二叉樹雖然滿足有序性,但是不是完全二叉樹。第三和第四棵二叉樹是完全二叉樹,但是不滿足有序性的特點。
注意:堆從根結點到任意結點路徑上的結點順序都是有序的!
最大堆的建立
堆的資料結構包括儲存完全二叉樹的陣列 data,堆中當前元素個數 size,堆的最大容量 capacity。
陣列的元素從1開始,0的位置定義為哨兵,方便以後更快操作。
public abstract class Heap { // 堆的型別定義 protected int[] data; //儲存元素的陣列 protected int size;//堆中當前元素個數 protected int capacity; //堆的最大容量 public Heap() { this.size = 0; this.capacity = 0; } public Heap(int[] data, int capacity) { this.data = data; this.size = 0; this.capacity = capacity; this.data[0] = Integer.MAX_VALUE; } public Heap(int maxSize) { this.data = new int[maxSize + 1];//最大元素從1開始 this.size = 0; this.capacity = maxSize; this.data[0] = Integer.MAX_VALUE;// 定義哨兵,為大於最大堆中所有可能元素的值 } public boolean isFull() { return this.size == this.capacity; } public boolean isEmpty() { return this.size == 0; } public abstract boolean insert(int element); }
最大堆的插入
插入元素時,插入到陣列的最後一個位置,這裡插入的結點值為20,檢查插入後仍然符合堆的兩個特性,插入完成。
當插入的值為35的時候,當前堆的有序性被破壞了,將35和31的位置調換後就可以了。
當插入的值為58的時候,58 > 31,跟31對調位置,58 > 44 繼續跟根結點調換位置。調整後保證了有序性,同時,從58 -> 44 -> 31這條線也是按照從大到小的順序。
public boolean insert(int element) {
// 將元素X插入最大堆H,其中H->Data[0]已經定義為哨兵
int i;
if (isFull()) {
System.out.println("最大堆已滿");
return false;
}
i = ++this.size; // i指向插入後堆中的最後一個元素的位置
for (; this.data[i / 2] < element; i /= 2) {
data[i] = data[i / 2]; // 向下過濾結點,對調父結點的位置
}
data[i] = element; // 將X插入
return true;
}
由於我們將陣列的第 0 個元素設定為哨兵,哨兵的值為一個非常大的整數值。如果沒有哨兵結點,我們在迴圈中還需要判斷 i > 1 這個條件,有了哨兵之後,迴圈在 i = 0 的時候就會停下來,可以少寫一個條件,提高程式效率。
最大堆的刪除
最大堆的刪除過程就是取出根結點(最大值)元素,同時刪除堆的一個結點。
刪除下圖的這個堆的最大值:
- 把 31 移至根
- 找出 31 的較大的孩子
時間複雜度為: $T(N)=O(logN)$
public int deleteMax() {
// 從最大堆中取出鍵值為最大的元素,並刪除一個結點
int parent, child;
int maxItem, temp;//maxItem-堆頂元素,temp-臨時變數
if (isEmpty()) {
System.out.println("最大堆已經為空");
return -1;
}
maxItem = this.data[1];//取出根結點最大值
// 用最大堆中的最後一個元素從根結點開始向上過濾下層結點
temp = this.data[this.size--];
for (parent = 1; parent * 2 < this.size; parent = child) {
child = parent * 2; // 左兒子的位置
if (child != this.size && this.data[child] < this.data[child + 1]) {
child++; //child 指向左右結點的較大者
}
if (temp > this.data[child]) {//找到位置了
break;
} else {//將子結點與父節點對換
this.data[parent] = this.data[child];
}
}
this.data[parent] = temp;
return maxItem;
}
最大堆的建立
建立最大堆是將已經存在的N個元素按最大堆的要求存放在一個一維陣列中。
建堆的過程可以從樹的從最後一個結點的父節點開始,到根結點1,將最後一個結點的父節點所在的小堆調整為最大堆,然後向左尋找有兒子的結點,每次調整一個最大堆,直到根結點。
public void buildHeap() {
//* 調整Data[]中的元素,使滿足最大堆的有序性 *//*
//* 這裡假設所有Size個元素已經存在Data[]中 *//*
int i;
//* 從最後一個結點的父節點開始,到根結點1 *//*
for (i = this.size / 2; i > 0; i--) {
preDown(i);
}
}
private void preDown(int p) {
//* 下濾:將H中以Data[p]為根的子堆調整為最大堆 *//*
int parent, child;
int temp;
temp = data[p]; //* 取出根結點存放的值 *//*
for (parent = p; parent * 2 <= size; parent = child) { //這個過程與刪除的過程一樣
child = parent * 2;
if ((child != size) && (data[child] < data[child + 1]))
child++; //* Child指向左右子結點的較大者 *//*
if (temp >= data[child]) break; //* 找到了合適位置 *//*
else //* 下濾X *//*
data[parent] = data[child];
}
data[parent] = temp;
}
總結
從堆的幾種操作可以發現,刪除和建堆的過程,就是從上往下調整堆的有序性的過程,插入元素的過程是從下往上調整堆的有序性的過程。
參考
【1】資料結構-浙江大學