定義:【最大樹(最小樹)】每個節點的值都大於(小於)或等於其子節點(如果有的話)值的樹。
最大樹與最小樹的例子如下所示,雖然這些樹都是二叉樹,但最大樹不必須是二叉樹,最大樹或最小樹節點的子節點個數可以大於等於2.
1-1最大樹
1-2 最小樹
定義:【最大堆(最小堆)】最大(最小)的完全二叉樹。由於堆是完全二叉樹,所以1-1b 不是最大堆,1-2b不是最小堆。注意到堆是完全二叉樹(從滿二叉樹中刪除K個元素之後爲完全二叉樹),擁有n個元素的堆其高度爲log2(n+1)(與滿二叉樹有相同的高度),因此可以在O(height)時間內完成插入和刪除操作,則這些操作複雜性爲O(log2n)。
最大堆的插入
圖1-3a給出了一個具有5個元素的最大堆,由於堆是完全二叉樹,當插入一個元素形成6元素時,其結構必如1-3b所示。如果插入的元素值爲1,則插入的該元素成爲了2的左孩子,相反,若元素的值爲5,則該元素不能成爲2的左孩子(否則將改變最大樹的特性),應把2下移爲左孩子,如圖1-3c所示,同時還得決定在最大堆中5是否佔據2原來的位置,由於父節點上的值爲20,大於子節點新插入元素5,因此可以在原2的位置插入新的元素5。假設要插入的元素值不是5而是21,這時,同1-3c一樣,把2下移爲左孩子,由於21大於根節點20,因此不能在2的位置放入新元素21,因此這時需要再次下移根節點20元素,將其移到其右孩子的位置(2的位置),再將新元素21插入根節點如圖1-3d所示。
圖1-3 最大堆的插入
由此可見,插入策略從葉到根只有單一路徑,每一層的工作需要耗時Θ(1),因此實現此策略的時間複雜度O(height)=O(log2n),n爲節點個數。
最大堆的刪除
從最大堆中刪除一個元素時,該元素從根部移出,例如從1-3d的最大堆中進行刪除操作即是移去元素21,因此最大堆只剩下5個元素。此時1-3d的二叉樹需要重新構造,以便仍然爲完全二叉樹。爲此可以移動6中的元素,即2。這樣就得到了正確的結構如圖1-4a中,但此時根節點爲空且元素2不在堆中,如果2直接插入根節點,得到的二叉樹不是最大樹,根節點的元素應該爲大於左右子節點值得元素,這個元素值應爲20,把它移到根節點,3的位置空了,2可以插入,最後形成的最大堆如1-3a所示。
現在假設要刪除20,在刪除之後,堆的二叉樹結構如圖1-4b所示,爲得到這個結構,10從位置5移出,如果10放在根節點,結果並不是最大堆。把根節點的兩個孩子(15和2)中較大一個移到根節點。假設將10插入2的位置,結果仍然不是最大堆,因此將14上移,10插入4位置,最後結果如1-4c所示。
1-4 最大堆的刪除
刪除策略從堆的根到葉節點的單一路徑,每一層的工作需要耗時Θ(1),因此實現此策略的時間複雜度O(height)=O(log2n),n爲節點個數,與插入相同。
最大堆的初始化
假設開始數組a中有n個元素,另有n=10,a[1:10]中元素的關鍵值爲[20,12,35,15,10,80,30,17,2,1],這個數組可以用來表示圖1-5a的完全二叉樹,這棵二叉樹不是最大樹。爲了將其轉化成最大堆,從第一具有孩子的節點開始(即節點10),這個元素在數組中的位置爲i=[n/2],如果以這個元素爲根的子樹已經是最大堆,則此時不需要調整,否則必須調整子樹使其成爲最大堆。隨後繼續檢查以i-1,i-2等節點爲根的子樹,直到檢查到整個二叉樹的根節點(其位置爲1)。
過程如下,最初i=5,由於10大於其子節點的元素值1,所以位置i=5爲根的子樹已經是最大堆。
檢查i=4的子樹,由於15<17,因此不是最大堆,將15與17進行交換得樹如圖1-5b;
檢查i=3的子樹,由於35<80,因此不是最大堆,將35與80進行交換;
檢查i=2的子樹,因爲12<17,17成爲重構子樹的根,下一步將12與位置4的兩個孩子中的較大者進行比較,由於12<15,15被移到4位置,12移到15的位置。形成二叉樹如圖1-5c;
檢查i=1子樹,這時以位置2或者位置3爲根的子樹已是最大堆了,然而,20<max(17,80),80移入根,位置3空出,由於20<max(35,30),較大者35移入作爲子樹根,20代替35的位置。如圖1-5d。
圖1-5最大堆的初始化
代碼實現
以下程序給出了最大堆的類定義。n 是私有成員,代表目前堆中元素的個數; MaxSize是堆的最大容量;heap爲存貯堆元素的數組,省缺堆的大小爲1 0個元素
template<class T>
class MaxHeap {
public:
MaxHeap(int MaxHeapSize = 10);
~MaxHeap() {delete [] heap;}
int Size() const {return CurrentSize;}
T Max() {if (CurrentSize == 0) throw OutOfBounds();
return heap[1];}
MaxHeap<T>& Insert(const T& x);
MaxHeap<T>& DeleteMax(T& x);
void Initialize(T a[], int size, int ArraySize);
private:
int CurrentSize, MaxSize;
T *heap; // 元素數組
} ;
template<class T>
MaxHeap<T>::MaxHeap(int MaxHeapSize){
// 構造函數
MaxSize = MaxHeapSize;
heap = new T[MaxSize+1];
CurrentSize = 0;
}
插入
template<class T>
MaxHeap<T>& MaxHeap<T>::Insert(const T& x){
// 把 x 插入到最大堆中
if (CurrentSize == MaxSize)
throw NoMem(); // 沒有足夠空間
/ /爲 x尋找應插入位置
// i 從新的葉節點開始,並沿着樹上升
int i = ++CurrentSize;
while (i != 1 && x > heap[i/2]) {
// 不能夠把 x 放入 h e a p [ i ]
heap[i] = heap[i/2]; // 將元素下移
i /= 2; // 移向父節點
}
heap[i] = x;
return *this;
}
在插入代碼中,i 從新創建的葉節點位置CurrentSize開始,對從該位置到根的路徑進行遍歷。對於每個位置i,都要檢查是否到達根( i = 1)或在i 處插入新元素不會改變最大樹的性質(x .key≤h e a p [i/ 2 ] . key)。只要這兩個條件中有一個滿足,就可以在 i 處插入x,否則,將執行while 循環體,把位於i / 2處的元素移到i 處並把i 處元素移到父節點(i / 2)。對於一個具有n 個元 素的最大堆(即CurrentSize = n),while 循環的執行次數爲O(height) =O( log2n),且每次執行所需時間爲 ( 1 ),因此Insert 的時間複雜性爲O( log2n)。
刪除
template<class T>
MaxHeap<T>& MaxHeap<T>::DeleteMax(T& x)
{
// 將最大元素放入 x ,並從堆中刪除最大元素
// 檢查堆是否爲空
if (CurrentSize == 0)
throw OutOfBounds(); // 隊列空
x = heap[1]; // 最大元素
// 重構堆
T y = heap[CurrentSize--]; // 最後一個元素
// 從根開始,爲y 尋找合適的位置
int i = 1, // 堆的當前節點
ci = 2; // i的孩子
while (ci <= CurrentSize) {
// heap[ci] 應是 i的較大的孩子
if (ci < CurrentSize && heap[ci] < heap[ci+1]) ci++;
// 能把 y 放入h e a p [ i ]嗎?
if (y >= heap[ci]) break; // 能
// 不能
heap[i] = heap[ci]; // 將孩子上移
i = ci; //下移一層
ci *= 2;
}
heap[i] = y;
return *this;
}
在DeleteMax操作中,堆的根(即最大元素)heap [ 1 ]被保存到變量x中,堆的最後一個元素heap [ CurrentSize ]被保存到變量y中,堆的大小(CurrentSize)被減1。在while 循環中,開始查找一個合適的位置以便重新將 y插入。從根部開始沿堆向下查找,對於具有 n 個元素的堆,while 循環的執行次數爲O ( lg2n),且每次執行所花時間爲 (1),因此,DeleteMax 操作總的時間複雜性爲O( log2n)。注意到即使堆的元素個數爲 0, 代碼也能正確執行,在這種情況下不執行While 循環,對堆的位置1進行賦值是多餘的。
初始化
template<class T>
void MaxHeap<T>::Initialize(T a[], int size, int ArraySize){
// 把最大堆初始化爲數組 a .
delete [] heap;
heap = a;
CurrentSize = size;
MaxSize = ArraySize;
// 產生一個最大堆
for (int i = CurrentSize/2; i >= 1; i--) {
T y = heap[i]; // 子樹的根
// 尋找放置 y的位置
int c = 2*i; // c的父節點是y的目標位置
while (c <= CurrentSize) {
// heap[c] 應是較大的同胞節點
if (c < CurrentSize &&
heap[c] < heap[c+1]) c++;
// 能把 y 放入h e a p [ c / 2 ]嗎?
if (y >= heap[c]) break; // 能
// 不能
heap[c/2] = heap[c]; // 將孩子上移
c *= 2; // 下移一層
}
heap[c/2] = y;
}
}
初始時刪除私有成員heap當前所指的數組,並使 heap指向a [ 0 ]。size爲數組a中的元素個數,ArrarySize是假設從a [1]算起數組a 中所能容納的最大元素個數。程序 的最初4行代碼重新設置了最大堆的私有成員,使數組a 代替數組heap。在for 循環中,從數組heap(即數組a)的二叉樹表示中最後一個具有一個孩子的節點開始進行初始化,直至到達根節點。對於每個位置 i,在while 循環中都保證根節點爲i的子樹已是最大堆。請注意for循環體與DeleteMax代碼的相似性。