C++ 堆排序演算法的實現與改進(含筆試面試題)
堆排序(Heap sort)是指利用堆這種資料結構所設計的一種排序演算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。堆排序可以用到上一次的排序結果,所以不像其他一般的排序方法一樣,每次都要進行n-1次的比較,複雜度為O(nlogn)。
這裡先說明以下幾個基本概念:
完全二叉樹:假設一個二叉樹有n層,那麼如果第1到n-1層的每個節點都達到最大的個數:2,且第n層的排列是從左往右依次排開的,那麼就稱其為完全二叉樹
堆:本身就是一個完全二叉樹,但是需要滿足一定條件,當二叉樹的每個節點都大於等於它的子節點的時候,稱為大頂堆,當二叉樹的每個節點都小於它的子節點的時候,稱為小頂堆,上圖即為小頂堆。
關鍵的性質:
將堆的內容填入一個一位陣列,這樣通過下標就能計算出每個結點的父子節點,編號順序從0開始,從左往右,從上至下層次遍歷。a[10] = {2,8,5,10,9,12,7,14,15,13}
若一個結點的下標為k,那麼它的父結點為(k-1)/2,其子節點為2k+1和2k+2
例:數字為10的節點的下標為3,父結點為1號:8,子節點為7號和8號:14,15
演算法步驟:
1)利用給定陣列建立一個堆H[0..n-1](我們這裡使用最小堆),輸出堆頂元素
2)以最後一個元素代替堆頂,調整成堆,輸出堆頂元素
3)把堆的尺寸縮小1
4) 重複步驟2,直到堆的尺寸為1
實現程式碼:
/*************************************************************************** * @file main.cpp * @author MISAYAONE * @date 25 March 2017 * @remark 25 March 2017 * @theme Heap Sort ***************************************************************************/ #include <iostream> #include <vector> #include <time.h> #include <Windows.h> using namespace std; //輔助交換函式 void Swap(int &a, int &b) { int temp = a; a = b; b = temp; } //堆排序的核心是建堆,傳入引數為陣列,根節點位置,陣列長度 void Heap_build(int a[],int root,int length) { int lchild = root*2+1;//根節點的左子結點下標 if (lchild < length)//左子結點下標不能超出陣列的長度 { int flag = lchild;//flag儲存左右節點中最大值的下標 int rchild = lchild+1;//根節點的右子結點下標 if (rchild < length)//右子結點下標不能超出陣列的長度(如果有的話) { if (a[rchild] > a[flag])//找出左右子結點中的最大值 { flag = rchild; } } if (a[root] < a[flag]) { //交換父結點和比父結點大的最大子節點 Swap(a[root],a[flag]); //從此次最大子節點的那個位置開始遞迴建堆 Heap_build(a,flag,length); } } } void Heap_sort(int a[],int len) { for (int i = len/2; i >= 0; --i)//從最後一個非葉子節點的父結點開始建堆 { Heap_build(a,i,len); } for (int j = len-1; j > 0; --j)//j表示陣列此時的長度,因為len長度已經建過了,從len-1開始 { Swap(a[0],a[j]);//交換首尾元素,將最大值交換到陣列的最後位置儲存 Heap_build(a,0,j);//去除最後位置的元素重新建堆,此處j表示陣列的長度,最後一個位置下標變為len-2 } } int main(int argc, char **argv) { clock_t Start_time = clock(); int a[10] = {12,45,748,12,56,3,89,4,48,2}; Heap_sort(a,10); for (size_t i = 0; i != 10; ++i) { cout<<a[i]<<" "; } clock_t End_time = clock(); cout<<endl; cout<<"Total running time is: "<<static_cast<double>(End_time-Start_time)/CLOCKS_PER_SEC*1000<<" ms"<<endl; cin.get(); return 0; }
建堆的過程,堆調整的過程,這些過程的時間複雜度,空間複雜度,以及如何應用在海量資料Top K問題中等等,都是需要重點掌握的。
複雜度分析:
最差時間複雜度O(n log n)
最優時間複雜度O(n log n)
平均時間複雜度O(n log n)
最差空間複雜度O(n)
注意:此排序方法不適用於個數少的序列,因為初始構建堆需要時間;
特點分析:不穩定演算法(unstable sort)、In-place sort。
例1:編寫演算法,從10億個浮點數當中,選出其中最大的10000個。
典型的Top K問題,用堆是最典型的思路。建10000個數的小頂堆,然後將10億個數依次讀取,大於堆頂,則替換堆頂,做一次堆調整。結束之後,小頂堆中存放的數即為所求。為了方便,直接使用STL容器
理論上有兩種方法,
一是資料全部排序,時間複雜度一般為O(n * log(n) ),但是考慮到10億這種數量級不適合全部載入記憶體,所以實際的演算法估計只能採用外排序。這是個演算法效率太低,不太實用。
另外一種是用一個有序容器儲存10000個數,然後其他的數字依次和容器中的最小數字比較,如果大於容器已有的,就插入容器並刪除原先最小的那個,而容器仍舊保持有序。
這個演算法的時間複雜度是O(n),理論上可能已經沒有更好的演算法了。
#include "stdafx.h"
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional> // for greater<>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
vector<float> bigs(10000,0);
vector<float>::iterator it;
// Init vector data
for (it = bigs.begin(); it != bigs.end(); it++)
{
*it = (float)rand()/7; // random values;
}
cout << bigs.size() << endl;
make_heap(bigs.begin(),bigs.end(), greater<float>()); // The first one is the smallest one!
float ff;
for (int i = 0; i < 1000000000; i++)
{
ff = (float) rand() / 7;//十億個數都以隨機數代替
if (ff > bigs.front()) // replace the first one ?
{
// set the smallest one to the end!
pop_heap(bigs.begin(), bigs.end(), greater<float>());
// remove the last/smallest one
bigs.pop_back();
// add to the last one
bigs.push_back(ff);
// mask heap again, the first one is still the smallest one
push_heap(bigs.begin(),bigs.end(),greater<float>());
}
}
// sort by ascent
sort_heap(bigs.begin(), bigs.end(), greater<float>());
return 0;
}
例題2:設計一個數據結構,其中包含兩個函式,1.插入一個數字,2.獲得中數。並估計時間複雜度。
使用大頂堆和小頂堆儲存。
使用大頂堆儲存較小的一半數字,使用小頂堆儲存較大的一半數字。
插入數字時,在O(logn)時間內將該數字插入到對應的堆當中,並適當移動根節點以保持兩個堆數字相等(或相差1)。
獲取中數時,在O(1)時間內找到中數。