摘要

堆排序需要用到一種資料結構,大頂堆。大頂堆是一種二叉樹結構,本質是父節點的數大於它的左右子節點的數,左右子節點的大小順序不限制,也就是根節點是最大的值。

這裡就是不斷的將大頂堆的根節點的元素和尾部元素交換,交換到大頂堆沒有可以被交換的元素為止。後面再說大頂堆的邏輯。

邏輯

首先將序列通過大頂堆排序。然後不斷的從堆中取出頂部元素放在尾部,直到大頂堆元素為空。

流程

  1. 對序列進行原地建堆操作
  2. 重複下面操作,直到堆元素數量為 1
    1. 交換堆頂元素與尾元素
    2. 堆的元素數量減 1
    3. 對 0 位置進行 1 次 自下而上的下濾

下面在程式碼中解釋原地建堆自下而上的下濾這兩個詞的邏輯。

實現

首先進行原地建堆。原地建堆是先將序列按照大頂堆的排序邏輯處理序列。

大頂堆的序列邏輯是父節點的值大於它的左右子節點的值,可以想象成一個二叉樹。這裡的原地排序用到了siftDown方法,而且在迴圈中只迴圈到序列一半數量,為什麼?這個在下面看siftDown方法時詳細探究一下。


// 原地建堆
// 自下而上的下濾
heapSize = array.length;
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
siftDown(i);
}

交換堆頂和尾部元素,然後將需要比較的序列元素數量減少1,並將要進行比較的序列再使用siftDown方法過濾,保持序列的大頂堆的性質。然後繼續開始的交換,直到可以比較的序列數量為 1 就截止。

while (heapSize > 1) {
// 交換堆頂元素和尾部元素
swap(0, --heapSize); // 對 0 位置進行 siftDown(恢復堆的性質)
siftDown(0);
}

大頂堆的 siftDown 方法

這裡來探究一下siftDown(下濾)。

二叉樹的父節點和子節點的關係符合這樣的公式

  • leftChilder = partner * 2 + 1
  • rightChilder = parnter * 2 + 1 + 1
  • half (葉子)節點的數量是總節點數量的 1/2

siftDown 方法主要是將 index 位置上的元素放在合適的位置上。那麼什麼位置是合適的位置呢

依據大頂堆的父節點值大於左右子節點的值的性質來看,只要是保證 index 位置的元素大於它的左右子節點就好。

看下面程式碼,如果 index < half 才進行迴圈比較,那麼就有一個問題,index >= half 為什麼不用比較

這就要提到很巧妙的點,首先看大頂堆的性質,左右子節點沒有具體順序的要求,其次子節點的值小於父節點。那麼就可以依據二叉樹的葉子節點性質,如果index的位置是在葉子節點位置,那麼就本來比它的父節點要小,就不用比較(這個是建立在序列本來符合大頂堆的順序,出現一個位置的元素有變化時進行的過濾處理)。

這也是上面的原地排序中,只從一半的位置開始,是因為從這個位置開始,肯定會給它的子節點比較,過濾出大的,並放在合適位置。

程式碼中有三個巧妙的點

  1. 迴圈從序列的一半位置開始比較,如果位置不在前半部分,就不進行比較,這個在上面分析過
  2. 在比較的時候,獲取到它左右子節點中最大的節點比較。在獲取右子節點的時候看右子節點是否存在rightIndex<heapSize。因為大頂堆是符合完全二叉樹的(儘量往左子樹安排元素)。
  3. 說是二叉樹,但是沒有實際的節點,還是一個線性序列,通過公式來獲取左右子樹的位置,這個就是心中有樹,沒有樹也是樹

/*
* 讓 index 位置的元素下濾
*/
private void siftDown(int index) {
E element = array[index]; int half = heapSize >> 1; // 取出非葉子節點
// 第一個葉子結點的索引 == 非葉子節點的數量
// 必須保證 index 是非葉子節點
while (index < half) {
// index 的節點有2種情況
// 1、只有左子節點
// 2、同時有左右子節點 // 預設左子節點跟它進行比較
int childIndex = (index << 1) + 1;
E child = array[childIndex];
// 右子節點
int rightIndex = childIndex + 1;
if (rightIndex < heapSize && cmp(array[rightIndex], child) > 0) {
child = array[ childIndex = rightIndex];
} if (cmp(child, element) < 0) break; // 將子節點存放到index位置
array[index] = child;
// 重新設定 index
index = childIndex;
}
array[index] = element;
}

時間和空間複雜度

  • 最好、平均時間複雜度:O(nlogn)
  • 最壞時間複雜度:O((nlogn)
  • 空間複雜度:O(1)
  • 屬於不穩定排序

題外話

這次的排序用到了二叉樹大頂堆的一些知識,可能看下來有諸多疑問,這裡就先請諸位看官有個印象,後續我會分享二叉樹的知識,然後在回過頭來看堆排序,會讓你思路大開。