原創文章,轉載請私信.關注公衆號 tastejava 學習加思考,品味java之美
什麼是小頂堆
小頂堆是一種經過排序的完全二叉樹, 其滿足如下性質:
- 小頂堆中的任意父節點都比其兩個孩子結點小
由上方性質又可以推導出如下性質:
- 小頂堆的根節點爲整個堆元素中最小的元素
將小頂堆裝入數組
我們當然可以用面向對象的方式描述一顆二叉樹, 但是有沒有不浪費一丁點空間. 即除了元素本身開銷外, 儘量不額外佔用內存空間的描述方式呢?
有的, 我們可以把小頂堆裝入數組中. 爲了把小頂堆裝入數組中, 我們需要給出一個數組中的元素, 就能計算出其對應的父結點, 以及其兩個子節點對應的位置 (這樣我們就能定位到小頂堆中所有元素的位置, 可以操作任意一個元素, 這樣就達到用數組描述小頂堆的目的啦).
裝入數組的小頂堆元素位置滿足如下性質:
- 假設有結點m在數組中下標爲n, 那麼其左孩子結點下標爲2n+1, 右孩子結點下標爲2n+2
三條性質, 條條有用
通過上面的介紹, 我們現在知道一個裝在數組中的小頂堆要滿足如下三個性質:
- 小頂堆中的任意父節點都比其兩個孩子結點小
- 小頂堆的根節點爲整個堆元素中最小的元素
- 假設有結點m在數組中下標爲n, 那麼其左孩子結點下標爲2n+1, 右孩子結點下標爲2n+2
這三個性質第一點是元素構成小頂堆的基本性質, 衍生出的第二點性質是利用小頂堆對無序元素進行堆排序的關鍵, 第三點性質是爲了將小頂堆裝入到數組中.
來一根數組, 以及一打元素下酒
假設我們有一個長度爲10的無序數組:
[5, 2, 1, 9, 6, 7, 3, 4, 0, 8]
按照性質3我們可以將無序數組等價還原成一顆完全二叉樹:
5
/ \
2 1
/ \ / \
9 6 7 3
/ \ /
4 0 8
我們的目標是將其元素排序, 構造成符合性質1和2的完全二叉樹, 即小頂堆(同一個完全二叉樹排序後小頂堆有多種情況, 下方展示其中一種).
排序後構造成小頂堆的數組:
[0, 2, 1, 4, 6, 7, 3, 5, 9, 8]
也就是下方所示的小頂堆:
0
/ \
2 1
/ \ / \
4 6 7 3
/ \ /
5 9 3
排序! 每個元素都有屬於自己的位置
以下方的完全二叉樹爲例, 我們分析一下, 如何調整元素順序, 來將其構造成小頂堆呢.
5
/ \
2 1
/ \ / \
9 6 7 3
/ \ /
4 0 8
根據性質1, 我們要讓所有的父節點都比其直接子節點小, 也就是說我們要把二叉樹中每一個子樹的父節點與其子節點比較, 如果父節點比子節點大, 那麼將父節點與其交換, 使得子樹滿足最小堆的性質.
那麼父節點要與孩子結點比較幾次呢, 是存在幾個孩子就比較幾次嗎, 其實比較一次就可以, 我們先讓兩個孩子結點比較, 找到較大的一個, 然後讓父節點與較大的比較, 如果父節點比較大的子節點大, 那麼它肯定也比較小的子節點大, 將父節點與較大的子節點交換, 那麼此時子樹就滿足最小堆性質.
將整顆二叉樹, 最後一個擁有子節點的父節點進行上述調整, 當從後往前處理完所有節點後, 整顆二叉樹都滿足最小堆性質, 那麼就完成了最小堆的構建. 整個過程看起來像是元素在下沉, 比如根節點5, 最終下沉到了4的位置.
上代碼
接下來的代碼展示瞭如何構建小頂堆, 依賴了lombok與junit, 放在IDE裏調試運行可以理解的更清晰.
/**
* 最小堆的定義是父節點一定比其兩個直接子節點要大
* 根節點一定是所有元素中最小的, 依據這個性質可以從前往後不斷構建小頂堆, 最終整個數組元素升序排列, 這就是堆排序
*
* @author Gaozl
* @date 2020/6/10 17:00
*/
@Slf4j
public class MinHeap {
/**
* 元素數組, 需要先了解下如何用數組表示二叉樹
* 數組表示的二叉樹有如下特性
* 假設有結點m下標爲n, 那麼2n+1是結點m的左孩子, 2n+2是結點m的右孩子
*/
private Integer[] elements = {5, 2, 1, 9, 6, 7, 3, 4, 0, 8};
/**
* 嘗試將給定元素放到指定下標, 如果不符合小頂堆特性將調整結點位置直到符合
* @param index 元素嘗試放入的下標
* @param element 給定元素
* @param length 用數組前幾個元素構建小頂堆
*/
public void siftDown(int index, Integer element, int length) {
int half = length >>> 1;
while (index < half) {
int leftChild = 2 * index + 1;
int rightChild = 2 * index + 2;
// 比左右兩個孩子中較小的孩子大才需要下沉, 因爲比較小孩子小時肯定也比較大的孩子小, 此時比兩個孩子都小無需下沉
int compareChild = (rightChild < length) // 存在rightChild
&& elements[rightChild] < elements[leftChild] ? rightChild : leftChild;
// 當前元素比孩子大, 需要下沉
if (element > elements[compareChild]) {
elements[index] = elements[compareChild];
index = compareChild;
} else { // 否則無需下沉, 停止處理
break;
}
}
elements[index] = element;
}
/**
* 將數組構建成小頂堆
*/
public void heapify(int length) {
for (int i = (length >>> 1) - 1; i >= 0; i--) { // 遍歷所有非葉子結點
siftDown(i, elements[i], length); // 將每個非葉子結點調整到符合小頂堆性質後, 整個數組自然構建成了小頂堆
}
}
/**
* 用數組構建小頂堆
*/
@Test
public void testHeapify() {
log.info("{}", toString());
heapify(elements.length);
log.info("{}", toString());
}
@Override
public String toString() {
return Arrays.toString(elements);
}
}
堆排序
既然我們都瞭解瞭如何構建小頂堆, 那麼可以進一步瞭解一下很有意思的排序方法, 堆排序. 堆排序依賴堆的第2條性質. 直接上代碼吧:)
/**
* 堆排序, 降序是用小頂堆, 小頂堆每次找到最小的元素並下沉.
* 升序是用大頂堆, 大頂堆每次找到最大元素並下沉
*/
@Test
public void testDescendSort() {
log.info("{}", toString());
// 先將數組所有元素構建小頂堆, 然後將堆頂最小元素與小頂堆最後一個元素交換
// 排除此時最後一個最小的元素, 前面元素繼續構建小頂堆, 找到最小元素繼續交換到此時小頂堆最後一個元素
// 經過構建length次小頂堆, 此時數組裏所有元素已經是降序排列
for (int i = 0; i < elements.length; i++) {
int length = elements.length - i;
heapify(length);
int temp = elements[length - 1];
elements[length - 1] = elements[0];
elements[0] = temp;
}
log.info("{}", toString());
}
拓展
本篇文章的構建小頂堆代碼並不是我憑空想象出來的, 是JDK1.8中優先隊列PriorityQueue類中的代碼片段改造註釋而來. 既然聰明的你已經讀到了這裏, 那麼可以順便去看看PriorityQueue源代碼實現啦, 如果還是看不懂也沒關係, 請參考如下開源項目, 超級方便哦, 一個致力於幫大家節約閱讀JDK源碼時間的開源項目
https://github.com/gaozhilai/open-jdk1.8-analysis