1. 程式人生 > >C++ 堆排序演算法的實現與改進(含筆試面試題)

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)時間內找到中數。