排序演算法大雜燴之堆排序
阿新 • • 發佈:2018-11-17
堆排序
#include <iostream>
#include <vector>
/*
非降序排序
時間複雜度:o(nlgn)
空間複雜度:o(nlgn)
建堆:o(n)
維護堆:o(lgn)
不穩定
*/
using namespace std;
void swap(int &a,int &b){int c = a;a = b;b = c;}
int Max(const vector<int> &a,int n,int i){
if(2*i + 2 > n) return i;//該節點左孩子不存在
if(2*i + 3 > n) {
//該節點右孩子不存在
if(a[i] >= a[2*i + 1]) return i;
else return 2*i + 1;
}
if(a[i] >= a[2*i + 1] && a[i] >= a[2*i + 2]) return i;
if(a[2*i + 1] >= a[i] && a[2*i + 1] >= a[2*i + 2]) return 2*i + 1;
return 2*i + 2;
}
void heapify(vector<int>&a,int n,int i)//lgn,即堆高度
{
//最大堆
//n 為堆大小
//i 是指下標
int cur = i;
while(true){
int tmp = Max(a,n,cur);
if(tmp == cur) break;
swap(a[cur],a[tmp]);
cur = tmp;
}
}
//建堆過程
void buildHeap(vector<int> &a) //n
{
for(int i = a.size()/2 - 1;i >= 0;i --){
heapify(a,a.size(),i);
}
}
//堆排序過程
void heapSort(vector<int> &a)
{
buildHeap(a);
for(auto c : a) cout << c << " ";
cout << endl;
for(int i = a.size() - 1;i > 0;i --){
swap(a[i],a[0]);
heapify(a,i,0);
for(auto c : a) cout << c << " ";
cout << endl;
}
}
int main()
{
vector<int> a = {4,1,3,2,16,9,10,14,8,7};
heapSort(a);
for(auto c : a) cout << c << " ";
return 0;
}
- 基本概念
- 最大堆:對於任何一個結點的鍵值總大於孩子結點的鍵值
- 最小堆:對於任何一個結點的鍵值總小於孩子節點的鍵值
- 思維引擎
堆排序顧名思義,利用這種資料結構達到排序的目的。這裡主要運用了三個主要操作:建堆,維護堆,堆排序。- 問題描述:對序列 進行非降序排序。
- 建堆:將原序列生成最大(小)堆的過程。
從 到 依次維護以該節點為根節點的子樹的最大(小)堆性質。 - 維護堆:
由於僅有根節點破壞了堆的性質,調整方案如下:- 將子樹的根節點作為起始節點,
- (以最大堆為例)若 小於孩子結點,則與孩子結點交換,並將 用此孩子更新。
- 重複操作 直至 大於孩子結點,那麼此時形成的堆就為最大堆。
- 堆排序:
1. 將原始序列 建成最大堆。
2. 將 與 中的元素交換,那麼 儲存了序列的最大值,而 則成了僅 不滿足最大堆性質的堆,對堆 進行堆維護。
3. 在依次將 與 … 交換,重複操作 ,使得 全部排序。 - 值得注意的是在對維護的過程中,發現以 為根節點的子堆中僅有 不滿足最大(小)堆性質
- 時間複雜度
堆排序的時間複雜度是一個十分有趣的問題的,裡面的一些數學背景我會在後面一一指出,並給出相關的證明。整體的思路來源於演算法導論第三版相應內容。- 維護堆:這裡很容易可以觀察到維護堆的時間複雜度與子樹的高度有關(子樹根節點與孩子結點置換時最多隻能置換子樹高度次,所以推測時間複雜度
,下面是數學證明:
這邊我們要指出為什麼偏偏是 不是其他的呢,這裡我們首先假設最後一層半滿,那麼我就可以得出最後一層(記為第 層)的節點個數為 ,那麼根節點的左子樹的的節點總個數就是 ,並且在維護堆的過程中我們只能堆根節點的左子樹( 個節點)或者右子樹( 個節點)進行操作,顯然上述不等式成立。 - 建堆
這個時間複雜度是很坑的,當直接從程式碼本身去觀察,對於 個節點,每一個節點都需要 左右的複雜度,所以是 然而啪啪打臉。
- 維護堆:這裡很容易可以觀察到維護堆的時間複雜度與子樹的高度有關(子樹根節點與孩子結點置換時最多隻能置換子樹高度次,所以推測時間複雜度
,下面是數學證明: