1. 程式人生 > 實用技巧 >快排與堆排

快排與堆排

本文複習一下快速排序和堆排序 2 種排序演算法(為了多快好省地刷 leetcode )。

快排

主要思想:

通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。

時間複雜度 \(O(n \log n)\) ,空間複雜度取決於是否使用遞迴實現。

程式碼實現:

int partition(vector<int> &v, int p, int r)
{
    int x = v[r];
    int i = p - 1;
    for (int j = p; j < r; j++)
    {
        if (v[j] < x)
            i = i + 1, swap(v[i], v[j]);
    }
    swap(v[i + 1], v[r]);
    return i + 1;
}
void quickSort(vector<int> &v, int p, int r)
{
    if (p < r)
    {
        int q = partition(v, p, r);
        quickSort(v, p, q - 1);
        quickSort(v, q + 1, r);
    }
}

partition 函式的圖解(圖源為《演算法導論》):

在這裡的實現,我們預設區間的最右側 v[r] = 4 為主元,將上面所示的陣列分為 2 部分,左側小於等於 4,右側大於 4 。下面是函式中幾個臨時變數所表示的含義:

堆排

堆的性質:

子結點的鍵值總是小於(或者大於)它的父節點。

三個步驟:

  • 建立大頂堆
  • 把最大的的數字放在堆的最末尾 ,即 j = n-1, ..., 1
  • 調整堆,使其滿足大頂堆的性質

程式碼實現:

class Heap
{
private:
    int heapSize;
    // index start at 0
    inline int getLeft(int x) { return 2 * x + 1; }
    inline int getRight(int x) { return 2 * x + 2; }
    void heapify(vector<int> &v, int idx)
    {
        int l = getLeft(idx), r = getRight(idx);
        int largest = idx;
        if (l < heapSize && v[l] > v[largest]) largest = l;
        if (r < heapSize && v[r] > v[largest]) largest = r;
        // largest 這個位置被影響,所以需要遞迴調整
        // Of course, 也可以通過迭代實現
        if (largest != idx)
            swap(v[idx], v[largest]), heapify(v, largest);
    }
    void buildHeap(vector<int> &v)
    {
        heapSize = v.size();
        for (int i = v.size() / 2; i >= 0; i--)
            heapify(v, i);
    }

public:
    void heapSort(vector<int> &v)
    {
        buildHeap(v);
        for (int i = v.size() - 1; i >= 1; i--)
        {
            swap(v[i], v[0]);
            heapSize--;
            heapify(v, 0);
        }
    }
};

heapify

heapify 函式圖解如下圖所示。需要注意的是,圖中陣列下標是從 1 開始的,而上面的程式碼實現是從 0 開始的。

時間複雜度 \(O(\log n)\) .

buildHeap

在表示堆的陣列中,範圍 $\lfloor n/2 \rfloor $ 到 \(n-1\) 是葉子節點(下標從 0 開始),對於葉子節點,自然而然會滿足堆的性質,對葉子節點呼叫 heapify 絲毫沒有影響,因此不需要調整。這就是為什麼 for 迴圈的範圍是 size/2 -> 0

時間複雜度為 \(O(n)\) .

heapSort

呼叫 buildHeap 後的陣列,是一個大頂堆,所以 v[0]

是最大的數字,我們把它交換到陣列的最末尾處。然後對 [0, heapSize) 範圍內的數字進行 heapify ,因為影響的只有位置 0 ,所以只需要呼叫一次 heapify(v, 0) 就能使得陣列滿足堆的性質。

時間複雜度 \(O(n\log n)\) .

heapSort 的圖解如下: