二叉堆是一種優先級隊列(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)有了很大的提升。