安琪拉教魯班學堆排序

《安琪拉與面試官二三事》系列文章
一個HashMap能跟面試官扯上半個小時
一個synchronized跟面試官扯了半個小時

《安琪拉教魯班學算法》系列文章

安琪拉教魯班學算法之動態規劃

安琪拉教魯班學算法之BFS和DFS

安琪拉教魯班學算法之堆排序
《安琪拉教妲己學分佈式》系列文章
安琪拉教妲己分佈式限流

堆排序屬於算法中比較常見的一種,本文希望通過使用王者峽谷二位脆皮英雄對話的方式講解動態規劃,讓大家在輕鬆愉快的氛圍中搞懂堆排序。

魯班: 安琪拉,你知道堆嗎?

安琪拉:知道啊!堆在計算機裏是一種數據結構,是一個近似完全二叉樹,當然咯,在JVM內存模型中堆也是存放運行時數據的區域,這裏說的堆指的是數據結構中的堆結構。

魯班: 那堆排序又是怎麼一回事?

安琪拉: 因爲堆有一個非常🐂的特性,堆中選任一節點A,它的左右子節點的值都大於A的值(小根堆),或者左右子節點的值都大於A的值(大根堆),以小根堆爲例,如下圖所示:

image-20200406132133378

安琪拉:因爲堆的這個特性,因此可以用於做排序,以及解決一些類型top K的問題。

魯班:你給我具體講講堆排序怎麼實現的唄。

安琪拉:好的。首先說一下堆排序元素的存儲方式:

  • 構建節點類,節點類中包含左節點和右節點的指針/引用,通過根節點遍歷;

  • 使用數組維護堆,其中 當前節點的下標爲 i , 左節點下標爲 i * 2 + 1, 右節點下標爲 i * 2 + 2, 父節點下標爲

    (i - 1) / 2.

    因此👆這張圖在數組中是 int[] arr = [3, 7, 16, 10, 21, 23, 37, 15], 將下標都標註上去之後,如下圖所示:

    image-20200406134531448

那麼我們要正式開始了,堆排序主要分三步:

  • 第一步:構造堆結構
  • 第二步:保存根節點,調整剩下堆,迭代進行

開始第一步,來看一下給定一個數組,如何將數組調整成符合堆特性的數組(子節點都比當前節點大)。

例如:現在數組數據爲 : 9, 3, 7, 6, 5, 1, 10, 2 剛開始的樹型結構如下圖所示:

image-20200406140158291

我們需要把這顆樹構造成最小堆,需要做些調整,使得滿足最小堆的特性:任一節點A的左右子節點都比A大。

思考第一個問題:從哪裏開始調整? 如何調整。

既然要滿足任一節點A的左右子節點都比A大,那首先節點需要有子節點纔行,那我們從最後一個不是葉子節點的元素開始,(葉子節點沒有左右子節點),最後一個不爲葉子節點的元素是 6 這個數,然後讓它跟左右節點比較,與左右節點中小的交換,這樣局部滿足最小堆特性了,如下圖:

image-20200406141805477

然後往上走,開始調整 元素 7,讓7 與左右子節點比較,如下圖:

image-20200406142035797

然後是元素3, 最後是根節點9,如下圖:

image-20200406142656229

image-20200406142711989

另外很重要的一點,局部調整完成之後,需要遞歸子節點是否同意滿足最小堆特性,如上圖,1 和 9交換之後,9與子節點不滿足最小堆特性,也要做調整,最後結果如下:

image-20200406153649409

這個最小堆的構建過程通過代碼編寫,如下:

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次可以通過將根節點和數組最後一個節點進行替換,把根節點存儲起來,把如下圖:

image-20200406160023873

現在數組最後一個元素是最小值,我們對堆的前 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: 安琪拉的博客 就能跟我一樣經常可以學知識了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章