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