快排與堆排
阿新 • • 發佈:2020-10-21
本文複習一下快速排序和堆排序 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
的圖解如下: