簡單探尋堆排序
堆排序
堆排序
堆排序是排序演算法中的一種,演算法時間複雜度是 O ( n l o g ( n ) ) O(n log(n)) O(nlog(n))。
堆
堆是電腦科學中一類特殊的資料結構的統稱,堆通常可以被看做是一棵完全二叉樹的陣列物件。
性質
- 如果一個節點的位置為
k
k
k ,則它的父節點的位置為
k
2
\cfrac{k}{2}
2k ,而它的兩個子節點的位置則分別為
2
×
k
2 \times k
2×k 和
2
×
k
+
1
2 \times k + 1
- 每個節點的值都大於或等於其左右孩子節點的值,稱為大頂堆
- 每個節點的值都小於或等於其左右孩子節點的值,稱為小頂堆
堆排序
基本思想
- 將待排序序列構造成一個堆。
- 進行堆調整使其變為大根堆,整個序列的最大值就是堆頂的根節點。
- 將其與末尾元素進行交換,此時末尾就為最大值。
- 然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n-1個元素的次小值。如此反覆執行,便能得到一個有序序列了。(剪枝)
程式碼實現
C程式碼
#include <stdio.h> #include <stdlib.h> void swap(int a[],int i,int j){ int temp = a[i]; a[i] = a[j]; a[j] = temp; } void heapify(int tree[],int n ,int i ){ if(i >= n) //遞迴出口 return; int c1 = 2 * i + 1; int c2 = 2 * i + 2; int max = i; if(c1 < n && tree[c1] > tree[max] ) max = c1; if(c2 < n && tree[c2] > tree[max]) max = c2; if(max != i){ swap(tree,max,i); //將最大值移動至父節點 heapify(tree,n,max); //遞迴維護下面的節點 } } void build_heap(int tree[],int n){ //構造堆 int last_node = n - 1; int parent = (last_node - 1) / 2; //求出最後一個父節點 for(int i = parent;i >= 0;i--){ //向上維護節點 heapify(tree, n , i); } } void heap_sort(int tree[],int n){ build_heap(tree,n);f for(int i = n - 1;i >= 0;i--){ //每次取出根節點後,堆減少一個節點 swap(tree,i,0); heapify(tree,i,0); } } int main(){ int tree[] = {2, 5, 3, 1, 10, 4}; int n = 6; heap_sort(tree,n); for(int i = 0;i < n;i++){ printf("%d ",tree[i]); } return 0; }
思路
一、heapify:堆調整
- 針對節點 i,將其兩個子節點找出來,此三個節點構成一個最小單位的完全二叉樹(越界的忽略)
- 找到這個最小單位的完全二叉樹 的最大值,並將其交換至父節點的位置
- 遞迴呼叫,維護交換後 子節點與其子節點被破壞的堆關係,遞迴出口為葉節點
堆調整是自頂向下進行調整,無需考慮下標i小於0的情況
二、 build_heap:構造堆
-
用一維陣列表示:堆
a [ i ] a[ i ] a[i] 的父節點為 a [ i − 1 2 ] a[\cfrac{i-1}{2}] a[2i−1],兩個子節點為 a [ 2 × i + 1 ] a[2 \times i + 1]
-
先找到最後一個父節點,再從最後一個父節點開始向上呼叫 heapify維護節點。
三、heap_sort:利用堆進行堆排序
儘管我們可以構造出大根堆,但是我們也發現,大根堆並沒有排好序
大根堆是根節點的值最大,這時我們可以將根節點移走,即剪去根節點,並將剩餘的再次構造一個新堆,迴圈往復,就進行了排序操作。
那麼,拿走根節點後,根節點的數值又應該由誰取代?
結合剛剛的分析,我們知道,該節點需滿足:移動到根節點對堆的破壞最小。那麼,結果便很明瞭了,最後一個節點是最好的選擇。
我們可以用最後一個節點替換根節點,此時根節點和第二層的兩個節點構成一個最小單位的最小二叉樹,此時就可以直接呼叫 heapify函式,形成一個新的堆。但是我們並沒有必要使用 build_heap函式,因為此時最大值就在第二層,根節點會因為遞迴維護被替換到最後一層,其他節點也會因為遞迴維護而得到調整。
最後,經過堆排序,數值便從小到大排序,同時也構成了小根堆。
後記
C語言需要我們自行手寫出程式碼。而實際上,許多程式語言並非需要我們手寫堆排序。
在這裡,我們可以引入一個名為優先佇列的資料結構。
優先佇列
優先佇列可以完成以下操作:
- 插入一個數值
- 取出最小的數值(獲得數值,並且刪除)
經過剛剛堆排序的知識,我們很容易就會發現優先佇列與堆排序的相似性。可以說,優先佇列進行了大根堆的堆排序,在輸出時按從大到小的順序輸出。
在C++中,STL裡的priority_queue就是其中之一。
#include <queue>
#include <cstdio>
using namespace std;
priority_queue<int> p;
int main{
//插入元素
p.push(3);
p.push(5);
p.push(1);
//不斷迴圈直至空為止
while(!p.empty()){
//獲取並刪除最大值
printf("%d\n",p.top());
p.pop();
}
return 0;
}