1. 程式人生 > >12、【堆】二項堆

12、【堆】二項堆

不能 並且 on() 連接 允許 暫時 其它 == 更新

一、二項樹的介紹

二項樹的定義

二項堆是二項樹的集合。在了解二項堆之前,先對二項樹進行介紹。

二項樹是一種遞歸定義的有序樹。它的遞歸定義如下:
  (1) 二項樹B0只有一個結點;
  (2) 二項樹Bk由兩棵二項樹B(k-1)組成的,其中一棵樹是另一棵樹根的最左孩子。

如下圖所示:

技術分享圖片

上圖的B0、B1、B2、B3、B4都是二項樹。對比前面提到的二項樹的定義:B0只有一個節點,B1由兩個B0所組成,B2由兩個B1所組成,B3由兩個B2所組成,B4由兩個B3所組成;而且,當兩顆相同的二項樹組成另一棵樹時,其中一棵樹是另一棵樹的最左孩子。

二項樹的性質

二項樹有以下性質:
[性質一] Bk共有2k個節點。
[性質二] Bk

的高度為k。
[性質三] 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)

=(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中深度為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、【堆】二項堆