在數據結構中,堆其實就是一棵完全二叉樹。我們知道內存中也有一塊叫做堆的存儲區域,但是這與數據結構中的堆是完全不同的概念。在數據結構中,堆分為大根堆和小根堆,大根堆就是根結點的關鍵字大於等於任一個子節點的關鍵字,而它的左右子樹又分別都是大根堆;小根堆與大根堆恰好相反。在C++的STL中優先隊列priority_queue結構就是實現的堆結構。下來自己動手現實一個堆結構,包括heap_init,heap_insert,heap_top等操作。
1、堆的實現
因為堆是一棵完全二叉樹,所以我們可以用順序表來實現,而且堆也只能用順序表。我們用vector。
(1) 堆的初始化
對堆的初始化基本思想:首先初始數組是一個雜亂無章的序列,但是如果堆中只有一個元素heap[0],那么heap[0]本身是一個堆,然后加入heap[1]調整堆;繼續加入heap[2].....直到完成所有元素的調整。
void sift_up(vector<int> &heap,int index){ while((index+1)/2-1 >= 0){ if(heap[(index+1)/2-1] < heap[index]){ swap(&heap[(index+1)/2-1],&heap[index]); index = (index+1)/2-1; }else break; } } void heap_init(vector<int> &heap){ if(heap.empty()) return ; for(int i=1; i<heap.size(); i++){ sift_up(heap,i); } }
(2) 向堆中插入元素
把插入的元素放入堆的末尾,然后向上調整堆。
void heap_insert(vector<int> &heap,int element){ heap.push_back(element); sift_up(heap,heap.size()-1); }
(3) 取出堆頂的元素
取出一個元素后,用最后一個元素填補第一個元素的位置,然后向下依次調整堆。
void sift_down(vector<int> &heap,int index){ while(index*2+2 < heap.size()){ if(heap[index*2+1]>=heap[index*2+2] && heap[index]<heap[index*2+1]){ swap(&heap[index],&heap[index*2+1]); index = index*2+1; }else if(heap[index*2+1]<heap[index*2+2] && heap[index]<heap[index*2+2]){ swap(&heap[index],&heap[index*2+2]); index = index*2+2; }else break; } } bool heap_top(vector<int> &heap,int *res){ if(heap.empty()) return false; *res = heap[0]; heap[0] = heap[heap.size()-1]; heap.erase(heap.end()-1); sift_down(heap,0); return true; }
2、堆排序
首先初始化堆,然后依次取出堆頂的值。這里為大根堆,所以是從大到小排序。
void heap_sort(vector<int> &vec){ heap_init(vec); int len = vec.size(); while(len--){ int num; heap_top(vec,&num); printf("%d ",num); } }
堆排序的時間復雜度為O(nlog2n),從上面排序的步驟可以看出它是不穩定的排序。但是它與選擇排序,歸並排序一樣時間復雜度不隨序列的分布變化而變化。而對於插入排序和冒泡排序來說,當輸入序列有序或者基本有序時,它們的復雜度會遞減為O(n),而快速排序則會退化成O(n2)。
所以在具體應用中,要根據輸入序列來選擇哪種排序方法,具體問題具體分析。由於堆排序特殊的排序結構和優良的性能,所以在很多時候下都可以采用堆排序。
3、堆排序的應用
在一個n個數的序列中取其中最大的k個數(Top k問題)。
這是一個很常見的排序算法題。
方法一:直接對這這n個數進行排序,然后取k個數。時間復雜度最少為O(nlog2n)。
方法二:借鑒快排的思路,並不需要完整地實現快排,只需要實現快排的一部分即可得到最大的k個數。復雜度為O(nlog2k)。
方法三:可以采用哈希排序,先把n中開始的k個數放入hash表中,然后依次從剩下的的n-k個數中取出一個,與hash表中的k個數比較,每次淘汰最小的那個數。時間復雜度為O((n-k)*k)。
方法四:取出n中開始的k個數,建立一個小根堆,然后從剩下的n-k個數中,每次取出一個數插入小根堆中,然后刪除堆頂的那個元素(堆中的最小值)。時間復雜度為O(*(n-k)*lg2k)。
不可否認,采用堆來求最大的k個數性能是最好的,但是好處還不止這么一點點!!我們試想一下,如果輸入的序列很大,也就是n值很大,以致於無法全部存放在內存中,那么這時候,方法一和方法二就不管用了,當然方法一采用歸並排序可以達到目的,但是這時候需要多少次IO??。如果選擇方法四,最多只需要(n-k)次IO,當然方法三也是如此,只是每次需要比較k次。
完整代碼詳見:https://github.com/whc2uestc/DataStructure-Algorithm/tree/master/heap
版權所有,歡迎轉載,轉載請注明出處。
