1. 程式人生 > 實用技巧 >排序 | 堆排序

排序 | 堆排序

一、什麼是堆?

堆是一種資料結構,是一種特殊的二叉樹。堆安排在一個連續的陣列中,這個陣列的下標從 1 開始,節點 i 的子節點下標可以用 i * 2, i * 2 + 1 計算得到。

例如,3 的子節點就是 3 * 2 = 6, 3 * 2 + 1 = 7。

此外,節點直接還滿足這樣一個性質:父節點總是【大於/小於】它的兩個子節點。父節點較大的稱為大根堆,反之為小根堆。子節點之間沒有大小關係的要求。

二、如何建堆?

對於只有一個元素的陣列,很顯然它不需要任何操作,就是一個堆。

如果一個數組有更多的元素,我們可以試圖想想:如果二叉樹根節點的左子樹和右子樹都是一個合法的堆,我們要如何調整根節點,使整個陣列變為一個堆?

以大根堆為例,現在兩邊的子樹都是一個合法的大根堆了,如何調整 4 的位置呢?答案是讓 4 下沉,並讓數值最大的子節點上浮。

但是這樣的話,左邊這個堆的性質又被破壞了。我們如法炮製,繼續讓 4 下沉。

這樣,我們就得到了一個合法的堆。

現在的問題就是如何把這個下沉操作應用到整個陣列,使其成為堆。

解決方法如下:

1. 對於葉子節點,顯然它是一個合法的堆。

2. 我們從最後一個非葉節點開始,對這些節點進行下沉操作。這相當於在包含非葉節點的倒數第一層開始。

3. 由於是包含非葉節點的倒數第一層,我們可以確保我們操作的節點全部具備了左右子樹均為合法堆的性質。

4. 接著我們會前往包含非葉節點的倒數第二層。當我們在這一層的時候,底下的這一層已經全部是調整好的節點了。因此,我們可以逐個對它們進行下沉。

5. 這樣子,我們就能得到一個完全的堆。

void heapify(vector<int> &a, int index) {
    // leftindex = (index + 1) * 2 - 1 = index * 2 + 1
    // rightindex 同理
    int leftIndex = index * 2 + 1;
    int rightIndex = index * 2 + 2;

    // 如果是葉節點,下沉結束。
    
// 由堆的性質,只需要檢查左節點是否為空。 if (leftIndex >= a.size()) { return; } // 我們確保了 index 是非葉節點, // 因此至多隻需要檢查右節點是不是空。 if (rightIndex >= a.size()) { if (a[index] < a[leftIndex]) { swap(a[index], a[leftIndex]); heapify(a, leftIndex); } } else { int maxIndex = index; if (a[leftIndex] > a[maxIndex]) { maxIndex = leftIndex; } if (a[rightIndex] > a[maxIndex]) { maxIndex = rightIndex; } if (maxIndex != index) { swap(a[index], a[maxIndex]); heapify(a, maxIndex); } } } void makeheap(vector<int>& a) { int n = a.size(); // 因為陣列的下標以 0 開始計,所以 n / 2 還要再 - 1 才是正確的下標。 for (int i = n / 2 - 1; i >= 0; i--) { heapify(a, i); } }

三、如何堆排序

當我們構建了大根堆之後,我們就可以提取堆的根,知道整個陣列的最大值。

而取了這個節點之後,誰來當根?當了之後是怎麼維護堆的性質的?

考慮到整個堆除了沒有根之外,都是合法大根堆根節點,我們只需要把陣列的最後一位拿出來放在堆頂。此時我們的堆就變成了“除了根節點,其它的節點都是合法”。

只需要經過提取 -> 調換 -> 維護 -> 提取 -> ... 這樣的迴圈,我們就可以得到一個排序好的陣列。

然而這樣的話我們會有額外的 O(n) 空間開銷,因此比起把提取的根節點放到一個新的數組裡,我們不如直接置換到堆的末尾。

這樣我們就完成了整個堆排序的演算法。

// 注意:為了完成堆排序,heapify 函式額外添加了一個當前堆長度的引數。
void heapify(vector<int> &a, int index, int maxlen) {
    // leftindex = (index + 1) * 2 - 1 = index * 2 + 1
    // rightindex 同理
    int leftIndex = index * 2 + 1;
    int rightIndex = index * 2 + 2;

    // 如果是葉節點,下沉結束。
    // 由堆的性質,只需要檢查左節點是否為空。
    if (leftIndex >= maxlen) {
        return;
    }
    
    // 我們確保了 index 是非葉節點,
    // 因此至多隻需要檢查右節點是不是空。
    if (rightIndex >= maxlen) {
        if (a[index] < a[leftIndex]) {
            swap(a[index], a[leftIndex]);
            heapify(a, leftIndex, maxlen);
        }
    }
    else {
        int maxIndex = index;
        if (a[leftIndex] > a[maxIndex]) {
            maxIndex = leftIndex;
        }
        if (a[rightIndex] > a[maxIndex]) {
            maxIndex = rightIndex;
        }
        if (maxIndex != index) {
            swap(a[index], a[maxIndex]);
            heapify(a, maxIndex, maxlen);
        }
    }
}

void makeheap(vector<int>& a) {
    int n = a.size();
    // 因為陣列的下標以 0 開始計,所以 n / 2 還要再 - 1 才是正確的下標。
    for (int i = n / 2 - 1; i >= 0; i--) {
        heapify(a, i, a.size());
    }
}

void heapsort(vector<int>& heap) {
    for (int i = heap.size() - 1; i >= 1; i--) {
        swap(heap[0], heap[i]);
        heapify(heap, 0, i);
    }
}