《FIFA 22》球員排名公佈 梅西仍然位居第一
堆排序是一種高效率的排序方法,它充分的利用了二叉堆的性質,無需藉助額外的輔助空間,並且擁有O(n*log(n))的時間複雜度。
首先這裡我從二叉堆講起。二叉堆是一種具有一定邏輯關係的完全二叉樹(這裡需要和滿二叉樹做一下區分),它始終滿足:任意節點的值均大於(或小於)其子節點,滿足該條件的二叉堆又叫大根堆(或小根堆)。
再說堆排序,它即是利用了二叉堆的該性質,在每次調整二叉堆結構後取堆頂元素,即該二叉堆內最大(或最小)的元素,取n次後得到的序列即為一個有序序列。
接下來來看一下隊的儲存結構:
int heap[HEAPLENGTH]; int *anotherHeap = new int[HEAPLENGTH];
由於二叉堆滿足“完全二叉樹”的性質,因此二叉堆可以很輕鬆的由一個一維陣列來表示,而祖先節點和兒子節點可以由索引很方便的計算出,來看程式碼:
int getLeftChild(const int &index) {
if (index < 0) return (0);
return (2 * index + 1);
}
int getRightChild(const int &index) {
if (index < 0) return (0);
return (2 * index + 2);
}
需要注意的是這裡計算索引的前提是使用了“0~n - 1”下標的陣列,這樣可以不造成記憶體浪費。
通過這兩個方法可以快速的得到與傳入引數對應的該完全二叉樹的左(右)兒子,方便後續的演算法操作。
接下來就看一下構建堆的過程,首先筆者總結了一個規律:在一個擁有n個節點的完全二叉樹中,最後一個擁有孩子的節點在層次遍歷序列中的位置為(n / 2) - 1。因此,為了節省資源,可以從該位置進行遞歸向下調整,直至整個二叉完全樹滿足堆的條件。下面給出程式碼:
void adjustHeap(int *arr, const int &index, const int &n) { int child = getLeftChild(index); int now = index; while (child < n) { if (getRightChild(now) < n && arr[child] /**/ < /**/ arr[getRightChild(now)]) { child = getRightChild(now); } if (arr[now] /**/ < /**/ arr[child]) { dataSwap(arr[now], arr[child]); } else break; now = child; child = getLeftChild(now); } }
當然,你可能注意到該段程式碼裡的getRightChild完全可以被child + 1替代(因為右兒子肯定緊挨著左兒子),這裡只是為了易讀。
在調整過後,我們可以由堆的性質得知堆頂(最前面那個元素)一定是大根堆(可以通過替換程式碼中有註釋部分的“<”為“>”來將調整過程改為小根堆調整),然後再將第(0)的元素和第(n - 1 - 調整次數)的元素交換,來保證第(n - 1 - 調整次數)的元素到第(n - 1)的元素的序列為有序序列。此時堆頂元素變化,因此可能破壞了堆的性質,則再從堆頂進行遞歸向下調整,最終經過n - 2次調整,即從第(n - 1 - (n - 2) = 1)的位置到第(n - 1)的位置為有序序列,第0的位置為堆,自然整個序列滿足了排序的最終要求,堆排序完成。下面給出堆排序程式碼:
void dataSwap(int &a, int &b) {
if (&a == &b || a == b) return;
a = a + b;
b = a + b;
a = b - a;
b = b - 2 * a;
}
void heapSort(int *arr, const int &n) {
if (arr == NULL || n <= 0) {
return;
}
for (int i = (n / 2) - 1; i >= 0; i--) {
adjustHeap(arr, i, n);
}
for (int i = n - 1; i > 0; i--) {
dataSwap(arr[0], arr[i]);
adjustHeap(arr, 0, i);
}
}
有一個有趣的函式,即“dataSwap”,本質上是一個交換函式,但是不需要藉助中間量。
如有不對敬請指出,感謝閱讀!