Heap(堆)
Heap學前知識
堆的概念:
N個元素序列[k1, k2, k3, k4, k5, k6...kn]當且僅當滿足以下關係時才會被稱為堆。
當資料下標為1時: ki <= k2i, ki <= k2i+1 或者 ki >= k2i, ki >= k2i + 1
當資料下標為0時: ki <= k2i + 1, ki <= k2i + 2 或者 ki >= k2i + 1, ki >= k2i + 2
複製程式碼
堆(heap)的實現通常是通過構造二叉堆, 因為應用較為普遍, 當不加限定時, 堆通常指的就是二叉堆。
二叉堆:
- 二叉堆是一棵完全二叉樹(參考圖1-1)
- 堆中的節點值總是不大於其父親節點的值, 這種我們一般稱為最大堆。反之亦然我們稱為最小堆。(參考圖1-2)
- 利用陣列實現二叉堆(參考圖1-3)
- 使用下標0的公式:
parent(i) = i / 2 left child (i) = 2 * i right child (i) = 2 * i + 1 複製程式碼
- 使用下標1的公式:
parent(i) = (i - 1) / 2 left child (i) = 2 * i + 1 right child (i) = 2 * i + 2 複製程式碼
- 使用下標0的公式:
圖1-1
圖1-2
圖1-3
最大堆的設計實現
初步結構
由於堆中的元素是需要進行比較的, 所以插入進來的元素都是需要帶有可比較性的。這裡我們繼承Comparable即可。 這裡我們用的是java自帶的動態陣列, 這樣避免空間不充足問題。
為了方便查詢元素的父節點以及左右孩子節點, 我們將其封裝成方法, 這樣無論是從陣列下標0或者下標1開始對於我們來說 都是不關的, 我們只關心返回正確的節點索引位置資料。
public class MaxHeap<E extends Comparable<E>> {
private ArrayList<E> data;
public MaxHeap(int capacity) {
this.data = new ArrayList<>(capacity);
}
public MaxHeap() {
this.data = new ArrayList<>();
}
public int size() {
return data.size();
}
public boolean isEmpty() {
return data.size() == 0;
}
// 返回父元素在二叉堆中陣列的索引位置
private int parent(int index) {
if (index == 0)
throw new IllegalArgumentException("該索引沒有父節點");
return (index - 1) / 2 ;
}
// 返回左孩子索引
private int leftChild(int index) {
return index * 2 + 1;
}
// 返回孩子索引
private int rightChild(int index) {
return index * 2 + 2;
}
}
複製程式碼
新增元素及上濾
現在我們需要向陣列中新增元素, 但是新增進入的元素是否滿足堆的特性呢? 如果不滿足我們又要如何去處理呢?
當我們向陣列中新增一個元素且不滿足堆的特性時候, 我們需要進行一個上濾的過程(有些稱為上浮), 用來達到並滿足堆的特性。
如下圖所示, 向陣列中新增一個元素並且進行上濾的過程:
通過上面的圖例過程, 我們清楚在插入的時候需要一直和自己父節點進行比較, 直到滿足 堆的特性才算插入成功。
// 新增元素
public void add(E e) {
data.add(e);
// 將新加入的索引進行上濾, 新增是從最後新增的所以取最後元素索引位置
siftUp(data.size() - 1);
}
// 上濾過程
private void siftUp(int index) {
// 如果當前index為0表示為根節點, 根節點是沒有父元素的所以沒法比較, 並且父節點是小於子節點的
while (index > 0 && data.get(parent(index)).compareTo(data.get(index)) < 0) {
// 如果子節點大於父節點進行交換
swap(parent(index), index);
// 繼續判斷, 是否還大於祖先節點, 直到滿足堆的特性
index = parent(index);
}
}
// 位置交換
private void swap(int p, int c) {
E t = data.get(p);
data.set(p, data.get(c));
data.set(c, t);
}
複製程式碼
取出元素及下濾
由於堆是優先佇列的結構, 所以只能從堆頂刪除元素。移除堆頂元素之後, 用堆的最後一個節點填補取走的堆頂元素, 並將堆的實際元素個數減1。但用最後一個元素取代堆頂元素可能會破壞堆的特性, 因此需要將堆自頂向下進行調整(這個過程一般稱為下浮或者下濾)使其滿足最大堆或者最小堆。
下圖是取出元素並進行下濾流程圖:
// 查詢出最大元素
public E findMax() {
if (data.size() == 0)
throw new IllegalArgumentException("當前陣列為空");
return data.get(0);
}
// 取出堆中最大元素
public E extractMax() {
// 1. 找到最大值
E ret = findMax();
// 2. 最後一個元素頂替最大值
swap(0, data.size() - 1);
// 3. 刪除最後節點值
data.remove(data.size() - 1);
// 4. 下濾(下浮)過程
siftDown(0);
return ret ;
}
// 下濾節點
private void siftDown(int index) {
// 如果下濾到葉子節點, 在去獲取當前索引位置左孩子肯定超出整個陣列大小
while (leftChild(index) < data.size()) {
// 1. 獲取到該索引的左右孩子
int k = leftChild(index);
// 需要判斷是否存在右孩子
// k + 1的話相當左孩子索引位置+1得到了右孩子, 如果不大於陣列長度則包含右孩子
if (k + 1 < data.size()) {
// 獲取左右孩子中最大的元素節點
// 這裡的判斷是更新索引位置資料, 如果左孩子大於右孩子則不需要更新索引, 否則更新為右孩子的索引
if (data.get(k).compareTo(data.get(k + 1)) < 0) {
// k = rightChild(index);
++ k; // ++k 等價rightChild(k)
}
}
if (data.get(index).compareTo(data.get(k)) >= 0)
break;
// 進行交換資料
swap(index, k);
// 將下濾後的索引繼續進行判斷
index = k;
}
}
複製程式碼
Replace和Heapify處理
-
Replace 定義: 取出堆中最大的元素, 然後放入一個新的元素。
實現原理:
- 可以直接將堆頂元素替換成新的元素, 然後進行下濾(下沉)操作。
-
Heapify
定義: 將任意陣列轉換成堆。
實現原理:
從最後一個非葉子節點開始計算。如圖1-4,我們這個棵完全二叉樹有5個葉子節點。
相應的倒數第一個非葉子節點就是元素22所在的節點。我們從這個節點開始倒著從後向前進行下濾操作。
如何定位最後一個非葉子節點索引位置呢?
取出陣列最後一個索引位置, 更具最後一個索引位置計算獲取到父節點索引位置。
圖1-4
public E replace(E e) {
// 1. 找到最大元素
E ret = findMax();
// 2. 新插入的值替換堆頂元素
data.set(0, e);
// 3. 進行下沉操作
siftDown(0);
return ret;
}
/**
Heapify操作, 寫成一個建構函式.
*/
public MaxHeap(ArrayList<E> data) {
this.data = data;
// 縮寫, 直接獲取到最後一個非葉子節點索引, 進行遞減。
for (int i = parent(data.size() - 1); i >= 0; i--) {
siftDown(i);
}
// 1. 獲取到最後一個非葉子節點的元素索引位置
// int p = parent(this.data.size() - 1);
// 2. 對p從後往前執行, 依次遞減進行下沉操作
// while (p >= 0) {
// // 2. 進行下沉
// siftDown(p);
// p--;
// }
}
複製程式碼