堆(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