二叉堆與堆排序


二叉堆是一種優先級隊列(priority queue)。搜索樹維護了全部數據結構的有序性,而在我們不需要得知全局有序,僅僅需要全局的極值時,這樣是一種沒有必要的浪費。根據對象的優先級進行訪問的方式,稱為循優先級訪問(call-by-priority)。優先級隊列本身並不是一個隊列結構,而是根據優先級始終查找並訪問優先級最高數據項的數據結構。

首先,定義優先級隊列基類。需要支持的操作,主要是插入、獲取最大(最小)元素、刪除最大(最小)元素。

1 template<typename T> struct PQ
2 {
3     virtual void insert(T) = 0;
4     virtual T getMax() = 0;
5     virtual T delMax() = 0;
6 };

 

完全二叉堆

最為常用的堆結構,就是完全二叉堆。在邏輯形式上,結構必須完全等同於完全二叉樹,同時,堆頂意外的每個節點都不大於(小於)其父親,即堆有序。

如果堆頂最大,稱為最大二叉堆。反之,稱為最小二叉堆。

因為完全二叉堆等同於完全二叉樹的結構,故高度應當為O(logn)。在前面二叉樹的一篇里,提到了完全二叉樹的層次遍歷結構可以采用向量表示,更加簡潔方便、內存上更加緊湊。回顧一下,一個節點的秩為n,它左孩子的秩為2*n+1,右孩子為2*n+2;當n!=0時,n的父親為[n/2]-1(向上取整)。向量結構對於刪除添加等,分攤復雜度為O(1)。

 1 template<typename T> class PQ_ComplHeap: public PQ<T>, public Vector<T>
 2 {
 3 protected:
 4     Rank percolateDown(Rank n, Rank i);//下濾
 5     Rank percolateUp(Rank i);//上濾
 6     void heapify(Rank n);//Floyd建堆算法
 7 public:
 8     PQ_ComplHeap() {};
 9     PQ_ComplHeap(T* A, Rank n) { copyFrom(A, 0, n); heapify(n); }
10     void insert(T);//允許重復故必然成功
11     T getMax();
12     T delMax();
13 };

 

查詢

優先級隊列最大的優勢所在,始終維持全局最大(最小)值為堆頂。因此只需要返回首元素即可。

1 template<typename T> T PQ_ComplHeap<T>::getMax()
2 {
3     return _elem[0];
4 }

插入

插入操作,首先把新元素加到向量末尾。因為需要維持堆序性,逐層檢查是否有序並做出調整,這個過程稱為上濾。上濾的高度不超過樹高,因此復雜度不超過O(logn)。

 1 template<typename T> void PQ_ComplHeap<T>::insert(T e)
 2 {
 3     Vector<T>::insert(e);//將新詞條接到向量末尾
 4     percolateUp(_size - 1);//對該詞條進行上濾
 5 }
 6 template<typename T> Rank PQ_ComplHeap<T>::percolateUp(Rank i)
 7 {
 8     while (ParentValid(i))
 9     {
10         Rank j = Parent(i);
11         if (lt(_elem[i], _elem[j])) break;
12         swap(_elem[i], _elem[j]); i = j;//繼續向上
13     }
14     return i;//返回上濾最終抵達的位置
15 }

刪除

每次刪除操作,都將刪除堆頂。采用的策略,是把向量末尾的元素補充到堆頂,然后向下逐層比較,維持堆序性,與上濾對應地,稱為下濾。

 1 template<typename T> T PQ_ComplHeap<T>::delMax()
 2 {
 3     T maxElem = _elem[0];
 4     _elem[0] = _elem[--_size];//用末尾的值代替
 5     percolateDown(_size, 0);//下濾
 6     return maxElem;//返回備份的最大詞條
 7 }
 8 template<typename T> Rank PQ_ComplHeap<T>::percolateDown(Rank n, Rank i)//前n個詞條中的第i個進行下濾
 9 {
10     Rank j;
11     while (i!= (j = ProperParent(_elem, n, i)))//i以及兩個孩子中,最大的不是i
12     {
13         swap(_elem[i], _elem[j]); i = j;//交換,並向下
14     }
15     return i;//返回下濾到達的位置
16 }

同樣,刪除操作除下濾外,均只需要常數時間,而下濾復雜度也不超過高度,故同樣為O(logn)。

建堆

二叉堆一個重要的問題就是如何建立堆,通常是由一個向量結構進行建堆。最常用的方法有兩種,一種是逐個元素插入,然后對每個元素都進行上濾。這樣操作的復雜度為O(log1+log2+...+logk)=O(nlogn)。其實,整個過程的復雜度與對全局進行排序相當。

介紹一種更快的建堆方法,Floyd算法。算法核心思想是,把建堆的過程等效於堆的合並操作,即兩個堆H1、H2以及節點p的合並操作,此時,如果H1和H2已經符合堆序性,那么只需要把p作為H1、H2的父親,並對p進行下濾操作即可,最終成為一個完整的堆。

1 template<typename T> void PQ_ComplHeap<T>::heapify(Rank n)//Floyd建堆法
2 {
3     for (int i = LastInternal(n); InHeap(n, i); i--)//自底向上下濾,從最后一個節點的父親開始
4         percolateDown(n, i);
5 }

僅對內部節點進行下濾即可。操作的復雜度為O(n)。對比可以發現,插入后上濾效率低的來源,是對每個節點都進行了上濾,而Floyd方法對內部節點進行下濾,而完全二叉堆中的外部節點數量多,並且深度小的節點遠遠少於高度小的節點。

堆排序

堆排序算法其實與選擇排序法非常相似。其核心思想都是將序列分為前后兩個部分:未排序部分U、已經排序部分S。不同之處僅在於,如何從前半部分選擇極值元素。借助堆結構,堆頂即為最值,因此可以很快的選擇出想要的元素,並與U部分的末尾交換即可,這也正是delMax()所做的工作。因為完全二叉堆的該過程不超過O(logn),所以算法的復雜度為O(nlogn),比選擇排序法的O(n^2)有了很大的提升。


免責聲明!

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



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