[數據結構]——堆(Heap)、堆排序和TopK


堆(heap),是一種特殊的數據結構。之所以特殊,因為堆的形象化是一個棵完全二叉樹,並且滿足任意節點始終不大於(或者不小於)左右子節點(有別於二叉搜索樹Binary Search Tree)。其中,前者稱為小頂堆(最小堆,堆頂為最小值),后者為大頂堆(最大堆,堆頂為最大值)。然而更加特殊的是,通常使用數組去存儲堆,而不是二叉樹。關於完全二叉樹,可以參見另一篇博文http://www.cnblogs.com/eudiwffe/p/6207196.html

// Heap is a sepcial complete binary tree(CBT)
/*    Heap sketch is a CBT, but stored in Array
 *        9 ---> maxtop         7                 7
 *       / \                   / \               / \
 *      /   \                 /   \             /   \
 *     7     8               4     8           4     5
 *    / \   / \             / \               /     /
 *   /   \ /   \           /   \             /     /
 *  5    3 2    4         3     5           3     6
 *
 *      (1)                  (2)                (3)
 *  maxtop heap       not maxtop(mintop)    not heap(CBT)
 * */

具體而言,對於長度為N的數組A中的任意一個元素i(0<=i<N/2),其左右子節點為i*2+1和i*2+2。以大頂堆為例,該堆始終滿足:

A[i]>=A[i*2+1] && A[i]>=A[i*2+2]。(下文不做特殊說明均以大頂堆為例

如何創建一個堆呢?對於給定的一個數組arr[]和長度n,一般使用在數組上就地堆化。堆化的過程實際是調整堆的過程。有自上到下和自下到上兩種堆化方法。

1)自上到下構建堆

// Method 1
// Create (Initialize) Heap, from top to bottom
void heap_create(int arr[], int n)
{
	int i;		// from top to bottom
	for(i=1; i<n; heap_adjust(arr,i++));
}

自上到下很好理解,首先假設當前數組arr的前i個元素已經滿足堆性質(arr[0]只有一個元素肯定滿足);然后每次在數組之后添加一個元素A[i],使得新的數組A[0~i]滿足堆化性質,其中heap_adjust可以調整當前節點i使其滿足堆化;直到i為n時,調整完畢,即堆化完畢。其中heap_adjust如下:

void heap_adjust(int arr[], int c)
{	// c - children, p - parent
	int p = (c-1)>>1, temp;
	// heap adjust from maxtop, from bottom to top
	for(; arr[p]<arr[c]; c=p, p=(c-1)>>1){
		temp = arr[p];
		arr[p] = arr[c];
		arr[c] = temp;
	}
}   // Time O(logn)

調整代碼也很好理解,首先找到當前節點c的父節點p,如果arr[p]<arr[c],則交換,然后繼續尋找p的父節點進行調整;否則,調整完畢(因為前文已經假設,數組的前i-1已經滿足堆化,新添一個元素i進行調整)。

很有意思,構建堆時使用自上到下,那么調整堆就必須自下到上。

2)自下到上構建堆

// Method 2
// Create (Initialize) Heap, from bottom to top
void heap_create(int arr[], int n)
{
	int i;		// from bottom to top
	for(i=(n>>1)-1; i>-1; heap_adjust(arr,i--,n));
}

此處自下到上的“下”,並不是數組最后一個元素,而是最后一個父節點n/2-1。也就是以父節點為線索,逐漸調整該節點的子節點。因此,此處heap_adjust是自上到下的調整,如下

void heap_adjust(int arr[], int p, int n)
{	// c - children, p - parent
	int maxid=p, temp;
	// heap_adjust for maxtop, from top to bottom
	for(; p<(n>>1); p=maxid){
		if ((p<<1)+1<n && arr[(p<<1)+1]>arr[maxid])
			maxid = (p<<1)+1;
		if ((p<<1)+2<n && arr[(p<<1)+2]>arr[maxid])
			maxid = (p<<1)+2;
		if (maxid == p) break;
		// swap arr[maxid] and arr[p]
		temp = arr[maxid];
		arr[maxid] = arr[p];
		arr[p] = temp;
	}
}   // Time O(logn)

首先保證當前p節點是作為父節點,然后在找到其子節點p*2+1和p*2+2,在三者中選擇最大的一個maxid,然后交換;否則調整結束。

兩種構建堆的方法各有利弊,方法1)是逐漸增加新節點,堆的節點增加方法數組尾部;方法2)是逐漸刪除堆頂節點,然后在剩下的節點中尋找最大的放在堆頂(一般會將數組尾元素與堆頂交換,以保證其符合完全二叉樹結構)。堆的調整時間復雜度均為O(logn),堆的創建時間復雜度均為O(nlogn)。

3)堆排序

堆的常見應用是堆排序。堆排序方法十分巧妙,無須額外空間,直接在原數組中進行堆排序。對於給定的數組arr[]以及其長度n,首先進行原地堆化,上面兩種方法均可,推薦第二種;然后每次將堆頂元素與數組尾元素交換,即arr[0]與arr[n-1]交換;將數組arr[]以及其長度n-1進行堆調整,此調整使用2)中的調整方法;反復迭代,直到調整數組的長度為1為止,排序完畢。

以非降序排序為例,每次刪除堆頂的元素放入數組尾部,所以需要使用大頂堆。

// Heap Sort - ascending order
void heap_sort(int arr[], int n)
{
	int i, temp;
	// init maxtop heap, using method 2 (from bottom to top)
	for (i=(n>>1)-1; i>-1; heap_adjust(arr,i--,n));
	for (i=n-1; i>0; heap_adjust(arr,0,i--)){
		// mv heap top to end (heap top is max)
		temp = arr[0];
		arr[0] = arr[i];
		arr[i] = temp;
	}
}   // Time O(nlogn)

每次調整堆,只需將堆頂調整即可。堆化時間復雜度為O(nlogn),排序時間復雜度為O(nlogn),總的時間復雜度為O(nlogn)。因為調整堆必須使用自上到下的方法調整heap_adjust,所以使用方法2)進行堆化和調整,十分巧妙。

4)TopK問題

TopK問題描述:在N個無序元素中,找到最大的K個(或最小的K)。

如果使用排序類似的算法,其時間復雜度為O(NlogN)+O(K)。當N遠大於K時,例如N為1e9,而K為10時,這種方法顯然太慢。使用堆化和堆調整則可以快速解決。以下以尋找最小的K個元素為例

設有一個K長度的最大堆,如果在數組中有一個元素小於該堆頂,則該元素有可能為尋找的最小K元素之一。則將該元素替換堆頂,然后進行堆調整。反復迭代,直到遍歷了數組中的所有元素。此時,該長度為K的最大堆就是待尋找的TopK。

// TopK problem : find max k (or min k) elements from unordered set
// eg. find min k elements from arr[], stored in res[]
void topk(int arr[], int n, int res[], int k)
{
	int i;		// copy and k elements to res
	for (i=0; i<k; res[i]=arr[i],++i);
	// make maxtop heap for res[]
	for(i=(k>>1)-1; i>-1; heap_adjust(res,i--,k));
	for(i=k; i<n; ++i){
		if (res[0] <= arr[i]) continue;
		// now arr[i] < heap top
		res[0] = arr[i];
		heap_adjust(res,0,k);
	}
}   // Time O(nlogk)

其中arr[]為原始無序數據,res[]為尋找結果。堆調整使用2)中的調整方法。首先任意選擇無序數組arr[]中的K個元素,對其進行堆化;然后從K開始遍歷無序數組arr[],每次將比堆頂小的放入堆頂,然后堆調整;最后得到堆res[]為TopK結果。其時間復雜度:創建K個元素堆O(KlogK),尋找最小K元素O((N-K)logK),總時間復雜度為O(NlogK),(當N遠大於K時)。

對於尋找最大K個元素,則需要構建最小堆,以及最小堆的堆調整,不再贅述。

注:本文涉及的源碼:https://git.oschina.net/eudiwffe/codingstudy/blob/master/src/heap/heap.c


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM