12、【堆】二項堆
一、二項樹的介紹
二項樹的定義
二項堆是二項樹的集合。在了解二項堆之前,先對二項樹進行介紹。
二項樹是一種遞歸定義的有序樹。它的遞歸定義如下:
(1) 二項樹B0只有一個結點;
(2) 二項樹Bk由兩棵二項樹B(k-1)組成的,其中一棵樹是另一棵樹根的最左孩子。
如下圖所示:
上圖的B0、B1、B2、B3、B4都是二項樹。對比前面提到的二項樹的定義:B0只有一個節點,B1由兩個B0所組成,B2由兩個B1所組成,B3由兩個B2所組成,B4由兩個B3所組成;而且,當兩顆相同的二項樹組成另一棵樹時,其中一棵樹是另一棵樹的最左孩子。
二項樹的性質
二項樹有以下性質:
[性質一] Bk共有2k個節點。
[性質二] Bk
[性質三] Bk在深度i處恰好有C(k,i)個節點,其中i=0,1,2,...,k。
[性質四] 根的度數為k,它大於任何其它節點的度數。
註意:樹的高度和深度是相同的。
下面對這幾個性質進行簡單說明:
[性質一] Bk共有2k個節點。
如上圖所示,B0有20=1節點,B1有21=2個節點,B2有22=4個節點,...
[性質二] Bk的高度為k。
如上圖所示,B0的高度為0,B1的高度為1,B2的高度為2,...
[性質三] Bk在深度i處恰好有C(k,i)個節點,其中i=0,1,2,...,k。
C(k,i)是高中數學中階乘元素,例如,C(10,3)
B4中深度為0的節點C(4,0)=1
B4中深度為1的節點C(4,1)= 4 / 1 = 4
B4中深度為2的節點C(4,2)= (4*3) / (2*1) = 6
B4中深度為3的節點C(4,3)= (4*3*2) / (3*2*1) = 4
B4中深度為4的節點C(4,4)= (4*3*2*1) / (4*3*2*1) = 1
合計得到B4的節點分布是(1,4,6,4,1)。
[性質四] 根的度數為k,它大於任何其它節點的度數。
節點的度數是該結點擁有的子樹的數目。
二、二項堆的介紹
二項堆通常被用來實現優先隊列,它堆是指滿足以下性質的二項樹的集合:
(1) 每棵二項樹都滿足最小堆性質。即,父節點的關鍵字 <= 它的孩子的關鍵字。
(2) 不能有兩棵或以上的二項樹具有相同的度數(包括度數為0)。換句話說,具有度數k的二項樹有0個或1個。
上圖就是一棵二項堆,它由二項樹B0、B2和B3組成。對比二項堆的定義:(01)二項樹B0、B2、B3都是最小堆;(02)二項堆不包含相同度數的二項樹。
二項堆的第(1)個性質保證了二項堆的最小節點是某一棵二項樹的根節點,第(2)個性質則說明結點數為n的二項堆最多只有log{n} + 1棵二項樹。實際上,將包含n個節點的二項堆,表示成若幹個2的指數和(或者轉換成二進制),則每一個2個指數都對應一棵二項樹。例如,13(二進制是1101)的2個指數和為13=23 + 22 + 20, 因此具有13個節點的二項堆由度數為3, 2, 0的三棵二項樹組成。
三、二項堆的基本操作
二項堆是可合並堆,它的合並操作的復雜度是O(log n)。
1. 基本定義
1 template <class T> 2 class BinomialNode { 3 public: 4 T key; // 關鍵字(鍵值) 5 int degree; // 度數 6 BinomialNode<T> *child; // 左孩子 7 BinomialNode<T> *parent; // 父節點 8 BinomialNode<T> *next; // 兄弟節點 9 10 BinomialNode(T value):key(value), degree(0), 11 child(NULL),parent(NULL),next(NULL) {} 12 };
BinomialNode是二項堆的節點。它包括了關鍵字(key),用於比較節點大小;度數(degree),用來表示當前節點的度數;左孩子(child)、父節點(parent)以及兄弟節點(next)。
1 template <class T> 2 class BinomialHeap { 3 private: 4 BinomialNode<T> *mRoot; // 根結點 5 6 public: 7 BinomialHeap(); 8 ~BinomialHeap(); 9 10 // 新建key對應的節點,並將其插入到二項堆中 11 void insert(T key); 12 // 將二項堆中鍵值oldkey更新為newkey 13 void update(T oldkey, T newkey); 14 // 刪除鍵值為key的節點 15 void remove(T key); 16 // 移除二項堆中的最小節點 17 void extractMinimum(); 18 19 // 將other的二項堆合並到當前二項堆中 20 void combine(BinomialHeap<T>* other); 21 22 // 獲取二項堆中的最小節點的鍵值 23 T minimum(); 24 // 二項堆中是否包含鍵值key 25 bool contains(T key); 26 // 打印二項堆 27 void print(); 28 private: 29 30 // 合並兩個二項堆:將child合並到root中 31 void link(BinomialNode<T>* child, BinomialNode<T>* root); 32 // 將h1, h2中的根表合並成一個按度數遞增的鏈表,返回合並後的根節點 33 BinomialNode<T>* merge(BinomialNode<T>* h1, BinomialNode<T>* h2); 34 // 合並二項堆:將h1, h2合並成一個堆,並返回合並後的堆 35 BinomialNode<T>* combine(BinomialNode<T>* h1, BinomialNode<T>* h2); 36 // 反轉二項堆root,並返回反轉後的根節點 37 BinomialNode<T>* reverse(BinomialNode<T>* root); 38 // 移除二項堆root中的最小節點,並返回刪除節點後的二項樹 39 BinomialNode<T>* extractMinimum(BinomialNode<T>* root); 40 // 刪除節點:刪除鍵值為key的節點,並返回刪除節點後的二項樹 41 BinomialNode<T>* remove(BinomialNode<T> *root, T key); 42 // 在二項樹root中查找鍵值為key的節點 43 BinomialNode<T>* search(BinomialNode<T>* root, T key); 44 45 // 增加關鍵字的值:將二項堆中的節點node的鍵值增加為key。 46 void increaseKey(BinomialNode<T>* node, T key); 47 // 減少關鍵字的值:將二項堆中的節點node的鍵值減小為key 48 void decreaseKey(BinomialNode<T>* node, T key); 49 // 更新關鍵字的值:更新二項堆的節點node的鍵值為key 50 void updateKey(BinomialNode<T>* node, T key); 51 52 // 獲取二項堆中的最小根節點 53 void minimum(BinomialNode<T>* root, BinomialNode<T> *&prev_y, BinomialNode<T> *&y); 54 // 打印二項堆 55 void print(BinomialNode<T>* node, BinomialNode<T>* prev, int direction); 56 };
BinomialHeap是二項堆對應的類,它包括了二項堆的根節點mRoot以及二項堆的基本操作的定義。
下面是一棵二項堆的樹形圖和它對應的內存結構關系圖。
2. 合並操作
合並操作是二項堆的重點,它的添加操作也是基於合並操作來實現的。合並兩個二項堆,需要的步驟概括起來如下:
(1) 將兩個二項堆的根鏈表合並成一個鏈表。合並後的新鏈表按照"節點的度數"單調遞增排列。
(2) 將新鏈表中"根節點度數相同的二項樹"連接起來,直到所有根節點度數都不相同。
下面,先看看合並操作的代碼;然後再通過示意圖對合並操作進行說明。
merge()代碼(C++)
1 /* 2 * 將h1, h2中的根表合並成一個按度數遞增的鏈表,返回合並後的根節點 3 */ 4 template <class T> 5 BinomialNode<T>* BinomialHeap<T>::merge(BinomialNode<T>* h1, BinomialNode<T>* h2) 6 { 7 BinomialNode<T>* root = NULL; //heap為指向新堆根結點 8 BinomialNode<T>** pos = &root; 9 10 while (h1 && h2) 11 { 12 if (h1->degree < h2->degree) 13 { 14 *pos = h1; 15 h1 = h1->next; 16 } 17 else 18 { 19 *pos = h2; 20 h2 = h2->next; 21 } 22 pos = &(*pos)->next; 23 } 24 if (h1) 25 *pos = h1; 26 else 27 *pos = h2; 28 29 return root; 30 }
link()代碼(C++)
1 /* 2 * 合並兩個二項堆:將child合並到root中 3 */ 4 template <class T> 5 void BinomialHeap<T>::link(BinomialNode<T>* child, BinomialNode<T>* root) 6 { 7 child->parent = root; 8 child->next = root->child; 9 root->child = child; 10 root->degree++; 11 }
合並操作代碼(C++)
/* * 合並二項堆:將h1, h2合並成一個堆,並返回合並後的堆 */ template <class T> BinomialNode<T>* BinomialHeap<T>::combine(BinomialNode<T>* h1, BinomialNode<T>* h2) { BinomialNode<T> *root; BinomialNode<T> *prev_x, *x, *next_x; // 將h1, h2中的根表合並成一個按度數遞增的鏈表root root = merge(h1, h2); if (root == NULL) return NULL; prev_x = NULL; x = root; next_x = x->next; while (next_x != NULL) { if ( (x->degree != next_x->degree) || ((next_x->next != NULL) && (next_x->degree == next_x->next->degree))) { // Case 1: x->degree != next_x->degree // Case 2: x->degree == next_x->degree == next_x->next->degree prev_x = x; x = next_x; } else if (x->key <= next_x->key) { // Case 3: x->degree == next_x->degree != next_x->next->degree // && x->key <= next_x->key x->next = next_x->next; link(next_x, x); } else { // Case 4: x->degree == next_x->degree != next_x->next->degree // && x->key > next_x->key if (prev_x == NULL) { root = next_x; } else { prev_x->next = next_x; } link(x, next_x); x = next_x; } next_x = x->next; } return root; } /* * 將二項堆other合並到當前堆中 */ template <class T> void BinomialHeap<T>::combine(BinomialHeap<T> *other) { if (other!=NULL && other->mRoot!=NULL) mRoot = combine(mRoot, other->mRoot); }
合並函數combine(h1, h2)的作用是將h1和h2合並,並返回合並後的二項堆。在combine(h1, h2)中,涉及到了兩個函數merge(h1, h2)和link(child, root)。
merge(h1, h2)就是我們前面所說的"兩個二項堆的根鏈表合並成一個鏈表,合並後的新鏈表按照‘節點的度數‘單調遞增排序"。
link(child, root)則是為了合並操作的輔助函數,它的作用是將"二項堆child的根節點"設為"二項堆root的左孩子",從而將child整合到root中去。
在combine(h1, h2)中對h1和h2進行合並時;首先通過 merge(h1, h2) 將h1和h2的根鏈表合並成一個"按節點的度數單調遞增"的鏈表;然後進入while循環,對合並得到的新鏈表進行遍歷,將新鏈表中"根節點度數相同的二項樹"連接起來,直到所有根節點度數都不相同為止。在將新聯表中"根節點度數相同的二項樹"連接起來時,可以將被連接的情況概括為4種。
x是根鏈表的當前節點,next_x是x的下一個(兄弟)節點。
Case 1: x->degree != next_x->degree
即,"當前節點的度數"與"下一個節點的度數"相等時。此時,不需要執行任何操作,繼續查看後面的節點。
Case 2: x->degree == next_x->degree == next_x->next->degree
即,"當前節點的度數"、"下一個節點的度數"和"下下一個節點的度數"都相等時。此時,暫時不執行任何操作,還是繼續查看後面的節點。實際上,這裏是將"下一個節點"和"下下一個節點"等到後面再進行整合連接。
Case 3: x->degree == next_x->degree != next_x->next->degree
&& x->key <= next_x->key
即,"當前節點的度數"與"下一個節點的度數"相等,並且"當前節點的鍵值"<="下一個節點的度數"。此時,將"下一個節點(對應的二項樹)"作為"當前節點(對應的二項樹)的左孩子"。
Case 4: x->degree == next_x->degree != next_x->next->degree
&& x->key > next_x->key
即,"當前節點的度數"與"下一個節點的度數"相等,並且"當前節點的鍵值">"下一個節點的度數"。此時,將"當前節點(對應的二項樹)"作為"下一個節點(對應的二項樹)的左孩子"。
下面通過示意圖來對合並操作進行說明。
第1步:將兩個二項堆的根鏈表合並成一個鏈表
執行完第1步之後,得到的新鏈表中有許多度數相同的二項樹。實際上,此時得到的是對應"Case
4"的情況,"樹41"(根節點為41的二項樹)和"樹13"的度數相同,且"樹41"的鍵值 >
"樹13"的鍵值。此時,將"樹41"作為"樹13"的左孩子。
第2步:合並"樹41"和"樹13"
執行完第2步之後,得到的是對應"Case 3"的情況,"樹13"和"樹28"的度數相同,且"樹13"的鍵值 < "樹28"的鍵值。此時,將"樹28"作為"樹13"的左孩子。
第3步:合並"樹13"和"樹28"
執行完第3步之後,得到的是對應"Case 2"的情況,"樹13"、"樹28"和"樹7"這3棵樹的度數都相同。此時,將x設為下一個節點。
第4步:將x和next_x往後移
執行完第4步之後,得到的是對應"Case 3"的情況,"樹7"和"樹11"的度數相同,且"樹7"的鍵值 < "樹11"的鍵值。此時,將"樹11"作為"樹7"的左孩子。
第5步:合並"樹7"和"樹11"
執行完第5步之後,得到的是對應"Case 4"的情況,"樹7"和"樹6"的度數相同,且"樹7"的鍵值 > "樹6"的鍵值。此時,將"樹7"作為"樹6"的左孩子。
第6步:合並"樹7"和"樹6"
此時,合並操作完成!
PS. 合並操作的圖文解析過程與"二項堆的測試程序(Main.cpp)中的testUnion()函數"是對應的!
3. 插入操作
理解了"合並"操作之後,插入操作就相當簡單了。插入操作可以看作是將"要插入的節點"和當前已有的堆進行合並。
插入操作代碼(C++)
1 /* 2 * 新建key對應的節點,並將其插入到二項堆中。 3 */ 4 template <class T> 5 void BinomialHeap<T>::insert(T key) 6 { 7 BinomialNode<T>* node; 8 9 // 禁止插入相同的鍵值 10 if (contains(key)) 11 { 12 cout << "Insert Error: the key (" << key << ") is existed already!" << endl; 13 return ; 14 } 15 16 node = new BinomialNode<T>(key); 17 if (node==NULL) 18 return ; 19 20 mRoot = combine(mRoot, node); 21 }
在插入時,首先通過contains(key)查找鍵值為key的節點。存在的話,則直接返回;不存在的話,則新建BinomialNode對象node,然後將node和heap進行合並。
註意:我這裏實現的二項堆是"進制插入相同節點的"!若你想允許插入相同鍵值的節點,則屏蔽掉插入操作中的contains(key)部分代碼即可。
4. 刪除操作
刪除二項堆中的某個節點,需要的步驟概括起來如下:
(01) 將"該節點"交換到"它所在二項樹"的根節點位置。方法是,從"該節點"不斷向上(即向樹根方向)"遍歷,不斷交換父節點和子節點的數據,直到被刪除的鍵值到達樹根位置。
(02) 將"該節點所在的二項樹"從二項堆中移除;將該二項堆記為heap。
(03) 將"該節點所在的二項樹"進行反轉。反轉的意思,就是將根的所有孩子獨立出來,並將這些孩子整合成二項堆,將該二項堆記為child。
(04) 將child和heap進行合並操作。
下面,先看看刪除操作的代碼;再進行圖文說明。
reverse()代碼(C++)
1 /* 2 * 反轉二項堆root,並返回反轉後的根節點 3 */ 4 template <class T> 5 BinomialNode<T>* BinomialHeap<T>::reverse(BinomialNode<T>* root) 6 { 7 BinomialNode<T>* next; 8 BinomialNode<T>* tail = NULL; 9 10 if (!root) 11 return root; 12 13 root->parent = NULL; 14 while (root->next) 15 { 16 next = root->next; 17 root->next = tail; 18 tail = root; 19 root = next; 20 root->parent = NULL; 21 } 22 root->next = tail; 23 24 return root; 25 }
刪除操作代碼(C++)
1 /* 2 * 刪除節點:刪除鍵值為key的節點 3 */ 4 template <class T> 5 BinomialNode<T>* BinomialHeap<T>::remove(BinomialNode<T>* root, T key) 6 { 7 BinomialNode<T> *node; 8 BinomialNode<T> *parent, *prev, *pos; 9 10 if (root==NULL) 11 return root; 12 13 // 查找鍵值為key的節點 14 if ((node = search(root, key)) == NULL) 15 return root; 16 17 // 將被刪除的節點的數據數據上移到它所在的二項樹的根節點 18 parent = node->parent; 19 while (parent != NULL) 20 { 21 // 交換數據 22 swap(node->key, parent->key); 23 // 下一個父節點 24 node = parent; 25 parent = node->parent; 26 } 27 28 // 找到node的前一個根節點(prev) 29 prev = NULL; 30 pos = root; 31 while (pos != node) 32 { 33 prev = pos; 34 pos = pos->next; 35 } 36 // 移除node節點 37 if (prev) 38 prev->next = node->next; 39 else 40 root = node->next; 41 42 root = combine(root, reverse(node->child)); 43 44 delete node; 45 46 return root; 47 } 48 49 template <class T> 50 void BinomialHeap<T>::remove(T key) 51 { 52 mRoot = remove(mRoot, key); 53 }
remove(key)的作用是刪除二項堆中鍵值為key的節點,並返回刪除節點後的二項堆。
reverse(root)的作用是反轉二項堆root,並返回反轉之後的根節點。
下面通過示意圖來對刪除操作進行說明(刪除二項堆中的節點20)。
總的思想,就是將被"刪除節點"從它所在的二項樹中孤立出來,然後再對二項樹進行相應的處理。
PS. 刪除操作的圖文解析過程與"二項堆的測試程序(Main.cpp)中的testDelete()函數"是對應的!
5. 更新操作
更新二項堆中的某個節點,就是修改節點的值,它包括兩部分分:"減少節點的值" 和 "增加節點的值" 。
更新操作代碼(C++)
1 /* 2 * 更新二項堆的節點node的鍵值為key 3 */ 4 template <class T> 5 void BinomialHeap<T>::updateKey(BinomialNode<T>* node, T key) 6 { 7 if (node == NULL) 8 return ; 9 10 if(key < node->key) 11 decreaseKey(node, key); 12 else if(key > node->key) 13 increaseKey(node, key); 14 else 15 cout <<"No need to update!!!" <<endl; 16 } 17 18 /* 19 * 將二項堆中鍵值oldkey更新為newkey 20 */ 21 template <class T> 22 void BinomialHeap<T>::update(T oldkey, T newkey) 23 { 24 BinomialNode<T> *node; 25 26 node = search(mRoot, oldkey); 27 if (node != NULL) 28 updateKey(node, newkey); 29 }
5.1 減少節點的值
減少節點值的操作很簡單:該節點一定位於一棵二項樹中,減小"二項樹"中某個節點的值後要保證"該二項樹仍然是一個最小堆";因此,就需要我們不斷的將該節點上調。
減少操作代碼(C++)
1 /* 2 * 減少關鍵字的值:將二項堆中的節點node的鍵值減小為key。 3 */ 4 template <class T> 5 void BinomialHeap<T>::decreaseKey(BinomialNode<T>* node, T key) 6 { 7 if(key>=node->key || contains(key)) 8 { 9 cout << "decrease failed: the new key(" << key <<") is existed already, " 10 << "or is no smaller than current key(" << node->key <<")" << endl; 11 return ; 12 } 13 node->key = key; 14 15 BinomialNode<T> *child, *parent; 16 child = node; 17 parent = node->parent; 18 while(parent != NULL && child->key < parent->key) 19 { 20 swap(parent->key, child->key); 21 child = parent; 22 parent = child->parent; 23 } 24 }
下面是減少操作的示意圖(20->2)
減少操作的思想很簡單,就是"保持被減節點所在二項樹的最小堆性質"。
PS. 減少操作的圖文解析過程與"測試程序(Main.cpp)中的testDecrease()函數"是對應的!
5.2 增加節點的值
增加節點值的操作也很簡單。上面說過減少要將被減少的節點不斷上調,從而保證"被減少節點所在的二項樹"的最小堆性質;而增加操作則是將被增加節點不斷的下調,從而保證"被增加節點所在的二項樹"的最小堆性質。
增加操作代碼(C++)
1 /* 2 * 增加關鍵字的值:將二項堆中的節點node的鍵值增加為key。 3 */ 4 template <class T> 5 void BinomialHeap<T>::increaseKey(BinomialNode<T>* node, T key) 6 { 7 if(key<=node->key || contains(key)) 8 { 9 cout << "decrease failed: the new key(" << key <<") is existed already, " 10 << "or is no greater than current key(" << node->key <<")" << endl; 11 return ; 12 } 13 14 node->key = key; 15 16 BinomialNode<T> *cur, *child, *least; 17 cur = node; 18 child = cur->child; 19 while (child != NULL) 20 { 21 if(cur->key > child->key) 22 { 23 // 如果"當前節點" < "它的左孩子", 24 // 則在"它的孩子中(左孩子 和 左孩子的兄弟)"中,找出最小的節點; 25 // 然後將"最小節點的值" 和 "當前節點的值"進行互換 26 least = child; 27 while(child->next != NULL) 28 { 29 if (least->key > child->next->key) 30 { 31 least = child->next; 32 } 33 child = child->next; 34 } 35 // 交換最小節點和當前節點的值 36 swap(least->key, cur->key); 37 38 // 交換數據之後,再對"原最小節點"進行調整,使它滿足最小堆的性質:父節點 <= 子節點 39 cur = least; 40 child = cur->child; 41 } 42 else 43 { 44 child = child->next; 45 } 46 } 47 }
下面是增加操作的示意圖(6->60)
增加操作的思想很簡單,"保持被增加點所在二項樹的最小堆性質"。
PS. 增加操作的圖文解析過程與"測試程序(Main.cpp)中的testIncrease()函數"是對應的!
12、【堆】二項堆