1. 程式人生 > >二項堆

二項堆

直接 gif amp 結構 one 執行 圖文解析 swap 說過

二項樹的介紹

二項樹的定義

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

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

技術分享圖片

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

二項樹的性質

二項樹有以下性質:
[性質一] 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,它大於任何其它節點的度數。
節點的度數是該結點擁有的子樹的數目。
註意:樹的高度和深度是相同的。關於樹的高度的概念,《算法導論》中只有一個節點的樹的高度是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

二項堆