《安琪拉與面試官二三事》系列文章
一個HashMap能跟面試官扯上半個小時
一個synchronized跟面試官扯了半個小時《安琪拉教魯班學算法》系列文章
安琪拉教魯班學算法之堆排序
《安琪拉教妲己學分佈式》系列文章
安琪拉教妲己分佈式限流
堆排序屬於算法中比較常見的一種,本文希望通過使用王者峽谷二位脆皮英雄對話的方式講解動態規劃,讓大家在輕鬆愉快的氛圍中搞懂堆排序。
魯班: 安琪拉,你知道堆嗎?
安琪拉:知道啊!堆在計算機裏是一種數據結構,是一個近似完全二叉樹,當然咯,在JVM內存模型中堆也是存放運行時數據的區域,這裏說的堆指的是數據結構中的堆結構。
魯班: 那堆排序又是怎麼一回事?
安琪拉: 因爲堆有一個非常🐂的特性,堆中選任一節點A,它的左右子節點的值都大於A的值(小根堆),或者左右子節點的值都大於A的值(大根堆),以小根堆爲例,如下圖所示:
安琪拉:因爲堆的這個特性,因此可以用於做排序,以及解決一些類型top K的問題。
魯班:你給我具體講講堆排序怎麼實現的唄。
安琪拉:好的。首先說一下堆排序元素的存儲方式:
-
構建節點類,節點類中包含左節點和右節點的指針/引用,通過根節點遍歷;
-
使用數組維護堆,其中 當前節點的下標爲 i , 左節點下標爲
i * 2 + 1
, 右節點下標爲i * 2 + 2
, 父節點下標爲(i - 1) / 2
.因此👆這張圖在數組中是
int[] arr = [3, 7, 16, 10, 21, 23, 37, 15]
, 將下標都標註上去之後,如下圖所示:
那麼我們要正式開始了,堆排序主要分三步:
- 第一步:構造堆結構
- 第二步:保存根節點,調整剩下堆,迭代進行
開始第一步,來看一下給定一個數組,如何將數組調整成符合堆特性的數組(子節點都比當前節點大)。
例如:現在數組數據爲 : 9, 3, 7, 6, 5, 1, 10, 2 剛開始的樹型結構如下圖所示:
我們需要把這顆樹構造成最小堆,需要做些調整,使得滿足最小堆的特性:任一節點A的左右子節點都比A大。
思考第一個問題:從哪裏開始調整? 如何調整。
既然要滿足任一節點A的左右子節點都比A大,那首先節點需要有子節點纔行,那我們從最後一個不是葉子節點的元素開始,(葉子節點沒有左右子節點),最後一個不爲葉子節點的元素是 6 這個數,然後讓它跟左右節點比較,與左右節點中小的交換,這樣局部滿足最小堆特性了,如下圖:
然後往上走,開始調整 元素 7,讓7 與左右子節點比較,如下圖:
然後是元素3, 最後是根節點9,如下圖:
另外很重要的一點,局部調整完成之後,需要遞歸子節點是否同意滿足最小堆特性,如上圖,1 和 9交換之後,9與子節點不滿足最小堆特性,也要做調整,最後結果如下:
這個最小堆的構建過程通過代碼編寫,如下:
private void buildMinHeap(int[] arr, int len) {
//因此前面說從最後一個不爲葉子節點的元素開始,這裏((len - 1) -1) / 2 就是最後一個不爲葉子節點的元素的下標
//因爲最後一個節點下標爲len -1,最後一個葉子節點的父節點就是最後一個有子節點的元素 ,父節點下標爲(len -1 -1) 很多程序直接從 (len -1) /2 開始也是可以的,不影響最終結果,因爲很有可能(len -1) /2 是個葉子節點
for (int i = ((len - 1) -1) / 2; i >= 0; i--) {
heapify(arr, i, len);
}
}
private void heapify(int[] arr, int i, int len) {
if (i >= len) return;
int min = i;
//求左子節點下標 c1
int c1 = 2 * i + 1;
//求右子節點下標 c2
int c2 = 2 * i + 2;
//取最小者 和 節點替換
if (c1 < len && arr[c1] < arr[min]) {
min = c1;
}
if (c2 < len && arr[c2] < arr[min]) {
min = c2;
}
//如果當前節點子節點中有比自己小的,替換,然後調整子樹。
if (min != i) {
swap(arr, i, min);
heapify(arr, min, len);
}
}
上面構建的過程說完了,後面排序的部分就很簡單了。構建完成的堆,根節點是最小值,我們第k = 1次可以通過將根節點和數組最後一個節點進行替換,把根節點存儲起來,把如下圖:
現在數組最後一個元素是最小值,我們對堆的前 n -k 個元素做調整,讓它滿足最小堆,然後不斷把根節點和數組倒數第n-k個元素交換,最後數組就成了一個倒敘排列的數組,如果需要順序排列,就按照大根堆構建,然後不斷調整就可以了,實現完整代碼如下:
public int[] dumpSort(int[] arr){
int len = arr.length;
//構建最小堆
buildMinHeap(arr, len);
//將根節點保存到數組最後,調整堆
for (int i = len - 1; i >= 0; i--) {
swap(arr, 0, i);
heapify(arr, 0, --len);
}
return arr;
}
private void buildMinHeap(int[] arr, int len) {
/**
* 因此前面說從最後一個不爲葉子節點的元素開始,這裏((len - 1) -1) / 2 就是最後一個不爲葉子節點的元素的下標
* 因爲最後一個節點下標爲len -1,最後一個葉子節點的父節點就是最後一個有子節點的元素 ,父節點下標爲(len -1 -1)
* 很多程序直接從 (len -1) /2 開始也是可以的,不影響最終結果,因爲很有可能(len -1) /2 是個葉子節點
*/
for (int i = ((len - 1) -1) / 2; i >= 0; i--) {
heapify(arr, i, len);
}
}
private void heapify(int[] arr, int i, int len) {
if (i >= len) return;
int min = i;
//求左子節點下標 c1
int c1 = 2 * i + 1;
//求右子節點下標 c2
int c2 = 2 * i + 2;
//取最小者 和 節點替換
if (c1 < len && arr[c1] < arr[min]) {
min = c1;
}
if (c2 < len && arr[c2] < arr[min]) {
min = c2;
}
//如果當前節點子節點中有比自己小的,替換,然後調整子樹。
if (min != i) {
swap(arr, i, min);
heapify(arr, min, len);
}
}
private void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
魯班:明白了,大家關注Wx: 安琪拉的博客 就能跟我一樣經常可以學知識了。