[C++]資料結構:最大堆MaxHeap的建立與使用
優先佇列是一種非常常見的資料結構,
而最大最小樹又是其中最具代表性的一種優先佇列。
在此詳細的講述一下最大樹的插入、刪除、初始化等基本操作的思路。
在文章最後附上一段Demo原始碼提供測試,使用C++語言實現了最大堆。
首先先介紹一下最大樹的概念。
最大樹是每個節點的值都要大於或等於其子節點的值的樹。
而最大堆就是最大的完全二叉樹。
因為最大堆是完全二叉樹,所以擁有n個元素的堆的高度為[log2(n+1)]。
因此如果可以在O(height)的時間內完成插入和刪除操作,則其複雜度為O(log2n)。
下面是一個最大堆的圖例。這裡的第一層和第二層是標記第幾層子節點,並不是樹的第幾層。
1.最大堆的插入:
先來舉個栗子說明最大堆的插入問題。
這是一個有五個元素的最大堆。
如果要插入一個元素,那麼插入完成後的結構應該是這樣才能保證依舊是完全二叉樹:
如果插入的元素是1,那很簡單,直接作為第二層的元素2的左孩子節點插入即可。
但是如果插入的元素是5,也就是比該插入位置的父節點大的話,就需要做一定的調整了。
應該把元素2下移為左孩子,同時在判斷5能否佔據元素2的位置。
因為5<20,所以5可以插入在當前位置。插入完成:
但是如果想要插入的元素是25呢?
那麼在這一步便會有所不同:
因為25>20,不滿足我們最大堆的要求,所以我們要做的事情和上次一樣,
先將20移下一層:
再將25插入即可。插入完成:
下面來總結一下插入的操作。
插入演算法首先要知道插入完成後的二叉樹結構,然後再把要插入的元素一次調整到正確的位置。
插入策略從根到葉只有單一路徑,每一層的工作耗時O(1),
因此實現插入操作的時間複雜性為O(height)=O(log2n)。
2.最大堆的刪除
從最大堆中刪除一個元素的時候,該元素從堆的根部移出。
以下圖為例:
如果要從中刪除元素21也就是移除堆頂元素。
必須移動最後的一個元素,也就是元素2,才能保證它依舊是一個完全二叉樹。
這樣確保它依舊是完全二叉樹,但是元素2還沒有放入堆中。
而根據堆的結構特性,2是不能直接插入根節點的,
可以先假設把元素二放在根節點:
則需要作一定的調整以便讓它保持最大堆的特性。
因為根應當是堆的所有資料中最大的那個數,
也就是說,根元素應該是元素2,根的左孩子,根的右孩子三者中的最大者。
為什麼呢?
因為本來根的左孩子或右孩子應該是是堆中的第二大元素。
移除根之後必有一個是最大的元素,也就是根元素的合適人選。
現在再來看看栗子。
三者中的最大值是20,所以把它移到了根節點。
此時在20的原位置,也就是按次序編號的3位置形成一個空位,
由於該位置沒有孩子節點,所以元素2可以插入。最後形成了刪除完畢的最大堆:
下面再來舉個栗子,在上面的最大堆裡刪除元素20。
刪除之後的結構應該是這樣的:
所以先把元素10移除。
如果直接把10放在根節點並不能形成最大堆。
所以把根節點的兩個孩子15和2中較大的一個移到根節點。
移動完之後發現10還是不能插入,所以再把14往上移一層:
這樣便會發現元素10可以插入了,
於是最終的最大堆如下圖所示:
下面來總結一下刪除的操作。
刪除演算法需要了解刪除完成後的完全二叉樹結構,
先移除最後一個節點,把要刪除位置的兩個孩子挑選最大的移動到根節點。
如果最後一個節點元素能插入則插入,否則重複上述操作直到能插入為止。
插入策略從根到葉只有單一路徑,每一層的工作耗時O(1),
因此實現插入操作的時間複雜性為O(height)=O(log2n)。
3.最大堆的初始化
很多情況下我們是已經獲取了一個有n個元素的無序陣列,需要進行最大堆的初始化操作。
如果用一次插入的方法,構建非空最大堆,插入操作的總時間為O(log2n)。
下面介紹一下利用不同的策略在O(n)的時間裡完成堆的初始化。
假設開始陣列a[1:10]的關鍵值分別為[20,12,35,15,10,80,30,17,2,1]。
這些數值可以通過逐層輸入的方法構成一棵完全二叉樹:
接下來做的便是從下往上依次調整。
先來說一下大體思路。
為了將完全二叉樹轉化成最大堆,先從第一個具有孩子的節點下手(從下往上從後往前看)。
從圖中來看就是節點10。這個元素在陣列中的位置為i=[n/2]。
如果以此元素為根的子樹已經是最大堆,則不需調整,否則必須調整使其成堆。
隨後依次檢查以i-1,i-2等節點為根的子樹,直到檢測到整個二叉樹的根節點。
下面來看這個栗子。
第一次調整檢驗,i=5的時候:
嗯哼,此時這棵子樹是滿足最大堆的要求的,所以我們不用調整它。
接下來檢查下一個節點,也就是i=4的情況:
因為15<17,所以這棵子樹不滿足最大堆的條件。
為了把它變身成為最大堆,可以把子節點中最大的數與根節點元素交換,
也就是可用15與17交換,得到的樹效果如下:
此時i=4的位置已經是最大堆了。接下來便是i=3的情況,
和前面幾次一樣,將35與其孩子節點中最大的元素交換即可:
如此這般,i=3便也解決了。那麼當i=2的時候,
首先執行一次交換,確保了i=3為根的子樹的前兩層是最大堆:
下一步,將元素12與位置為4的節點的兩個孩子中較大的一個元素進行比較。
由於12<15,所以15被移到了位置4,12暫時移入了位置8。
因為8位置沒有子節點,所以將12插入,完成這一步調整。
最後還剩i=1的情況:
當i=1時,此刻以i=2和i=3為根節點的子樹們均已經是最大堆。
然後20<(max[35,30]),所以把80作為最大根,把80移入1位置:
位置3空出,因為20<(max[35,30]),所以元素35插入位置3,元素20插入元素6。
最終形成的最大堆如圖所示:
總結一下初始化最大堆的過程,
大致就是從下往上依次檢測所有子樹是否為最大堆,
如果不是把他們調整成最大堆,並將子樹的根節點放置到合適的位置不影響下面的子樹的最大堆結構。
下面附上原始碼以供加深理解:
//優先佇列:堆MaxHeap的定義與使用
#include <iostream>
using namespace std;
void OutOfBounds(){
cout<<"Out Of Bounds!"<<endl;
}
void BadInput(){
cout<<"Bad Input!"<<endl;
}
void NoMem(){
cout<<"No Memory!"<<endl;
}
template<class T>
class MaxHeap{
public:
MaxHeap(int MaxHeapSize = 10);
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);
void Output(ostream& out)const;
int CurrentSize;
int MaxSize;
T *heap;//元素陣列
};
//輸出連結串列
template<class T>
void MaxHeap<T>::Output(ostream& out)const{
for (int i= 0;i<CurrentSize;i++)
{
cout<<heap[i+1]<<" ";
}
cout<<endl;
}
//過載操作符
template<class T>
ostream& operator<<(ostream& out,const MaxHeap<T>&x){
x.Output(out);
return out;
}
template<class T>
MaxHeap<T>::MaxHeap(int MaxHeapSize /* = 10 */){
MaxSize = MaxHeapSize;
heap = new T[MaxSize+1];
CurrentSize = 0;
}
//將x插入到最大堆中
template<class T>
MaxHeap<T>& MaxHeap<T>::Insert(const T&x){
if(CurrentSize==MaxSize)
throw NoMem();
//為x尋找插入位置
//i從新的葉結點開始,並沿著樹慢慢上升
int i = ++CurrentSize;
while(i!=1&&x>heap[i/2]){
//不能把x放到heap[i]
heap[i] = heap[i/2];//將元素下移
i/=2;
}
heap[i] = x;
return *this;
}
//將最大的元素放到x並從堆中刪除
template<class T>
MaxHeap<T>& MaxHeap<T>::DeleteMax(T&x){
//檢查堆是否為空
if(CurrentSize==0)
throw OutOfBounds();
x = heap[1]; //取出最大元素並放入x中
T y = heap[CurrentSize]; //y為最後一個元素
CurrentSize--;
//從根開始為y尋找合適的位置
int i = 1; //堆的當前節點
int ci = 2; //i的孩子
while(ci<=CurrentSize){
//heap[ci]應該是較大的孩子
if(ci<CurrentSize&&heap[ci]<heap[ci+1])
ci++;
//能否把y放入heap[i]
if(y>=heap[ci])
break;
heap[i]=heap[ci];
i = ci;
ci = 2*ci;
}
heap[i]=y;
return *this;
}
//把最大堆初始化為陣列a
template<class T>
void MaxHeap<T>::Initialize(T a[],int size,int ArraySize){
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放入heap[c/2]
if(y>=heap[c]) //能把y放入heap[c/2]
break;
//不能把y放入heap[c/2]
heap[c/2]=heap[c]; //將孩子上移
c=2*c; //下移一層
}
heap[c/2] = y;
}
}
template<class T>
void HeapSort(T a[],int n)
{
MaxHeap<T>H;
H.Initialize(a,n,20);
T x;
for (int i=n-1;i>=1;i--)
{
H.DeleteMax(x);
a[i+1]=x;
}
}
int main(){
MaxHeap<int>myHeap;
const int number = 10;
int myArray[number+1] = {-1,6,8,4,2,1,3,0,5,7,9};
myHeap.Initialize(myArray,number,20);
cout<<myHeap<<endl;
HeapSort(myArray,number);
for (int j =1;j<=number;j++)
{
cout<<myArray[j]<<" ";
}
cout<<endl;
return 0;
}