前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >算法与数据结构系列之探秘堆结构

算法与数据结构系列之探秘堆结构

作者头像
范蠡
发布2018-07-25 16:18:35
5930
发布2018-07-25 16:18:35
举报

一、概述

此处所说的堆为数据结构中的堆,而非内存分区中的堆。堆通常可以被看做是树结构,满足两个性质: 1)堆中任意节点的值总是不大于(不小于)其子节点的值; 2)堆是一棵完全树。正是由于这样的性质,堆又被称为优先队列。根据性质一,将任意节点不大于其子节点的堆称为最小堆或最小优先队列,反之称为最大堆或最大优先队列。优先队列在操作系统作业调度的设计中有着举足轻重的作用。之前写了一篇优先队列的文章,详见算法导论第六章优先队列。

常见的堆结构,有二叉堆、左倾堆、斜堆、二项堆、斐波那契堆等。斐波那契堆在前文算法导论第十九章 斐波那契堆已经作了讲述。本文主要对其余几种堆结构做宏观上的概述,并说明它们的异同点。

二、二叉堆

二叉堆是最简单的堆结构,本质是一棵完全的二叉树,一般由数组实现,其父节点和左右子节点之间存在着这样的关系: 索引为i(i从0开始)的左孩子的索引是 (2i+1); 右孩子的索引是 (2i+2); 父结点的索引是 floor((i-1)/2)。详细请看我之前写的一篇文章:算法导论第六章堆排序(一)。众所周知,二叉堆在排序算法中应用甚广,特别是涉及到大数据的处理中,如Topk算法。

三、二项堆

某些应用中需要用到合并两个堆的操作,这个时候二叉堆的性能就不是很好,最坏情况下时间复杂度为O(n)。为了改善这种操作,算法研究者就提出了性能更好的可合并堆,如二项堆和斐波那契堆,当然还有左倾堆,斜堆等。二项堆对于该操作能在最坏情况Θ(lgn)下完成,而斐波那契堆则更进一步,能在平摊时间为Θ(1)下完成(注意是平摊时间),见下表。除了Union操作,二叉堆各操作的性能都挺好,而二项堆对此也仅仅是改善了Union操作,对于Minimum操作甚至还不如二叉堆,我想这大概是《算法导论》第三版没有把二项堆再作为一章的原因之一吧。而斐波那契堆各个操作都有极好的性能,除了Extract_min和Delete操作

说回到二项堆,二项堆是由一组的二项树组成,二项树B_k是一种递归定义的有序树,其具有以下的性质: 1)共有2^k个节点; 2)树的高度为k; 3)在深度 i 处恰有C^i_k个节点,因为C^i_k为二项系数,正因如此,才称其为二项树; 4)根的度数为k(即孩子节点个数),它大于任何其他节点的度数。 二项树B_0只包含一个节点,二项树B_k由两棵二项树B_k-1连接而成,其中一棵树的根是另一棵树的根的最左孩子。如下图:

二项堆H由一组满足下面二项堆性质的二项树组成: 1)H中的每个二项树满足最小堆性质(说明二叉堆中最小节点在二项树的根中); 2)对任意非负整数k,H中至多有一棵二项树的根具有度数k(说明在包含n个节点的二项堆中,至多有floor(lgn)+1棵二项树)。 不同于斐波那契堆采用双循环链表来连接根节点和孩子节点,二项堆中采用的是单链表,每个节点有指向父节点的指针,孩子节点指针和兄弟节点指针,如:

代码语言:javascript
复制
1struct BinomialHeapNode {
2    BinomialHeapNode    *parent;
3    BinomialHeapNode    *child;
4    BinomialHeapNode    *sibling;
5    unsigned int        degree;
6    KeyType        key;
7};

下面列上自己写的动态集合操作的代码:

代码语言:javascript
复制
  1#ifndef _BINOMIAL_HEAP_
  2#define _BINOMIAL_HEAP_
  3//#define NOT_IN_HEAP    UINT_MAX
  4// the node of binomial tree
  5// struct heap_node {
  6//     struct heap_node*    parent;
  7//     struct heap_node*    child;
  8//     struct heap_node*    sibling;
  9// 
 10//     unsigned int    degree;
 11//     int                key;
 12//     const void*        value;
 13// };
 14template<typename KeyType>
 15class BinomialHeap {
 16public:
 17    struct BinomialHeapNode {
 18        BinomialHeapNode    *parent;
 19        BinomialHeapNode    *child;
 20        BinomialHeapNode    *sibling;
 21        unsigned int        degree;
 22        KeyType                key;
 23    };
 24
 25public:
 26    BinomialHeap() {
 27        _head_list = NULL;
 28    }
 29
 30    ~BinomialHeap() {
 31        BinomialHeapNode *tmp = _head_list;
 32        while (tmp) {
 33            BinomialHeapNode *next = tmp->sibling;
 34            _DeleteTree(tmp);
 35            tmp = next;
 36        }
 37    }
 38
 39public:
 40    //在二项堆中插入一个新节点
 41    void BinomialInsert(KeyType new_key){
 42        BinomialHeap new_heap;
 43        new_heap._head_list = new BinomialHeapNode();
 44        new_heap._head_list->parent = new_heap._head_list->child = new_heap._head_list->sibling = NULL;
 45        new_heap._head_list->degree = 0;
 46        new_heap._head_list->key = new_key;
 47        this->BinomialUnion(new_heap);
 48    }
 49
 50    //获取二项堆中最小元素的值
 51    //用一个列表存根链表的值,然后找最小值,O(lgn),或者用一个指针指向最小值,O(1)
 52    KeyType Minimum() const{
 53        vector<KeyType> values_in_head_list;
 54        BinomialHeapNode *node = _head_list;
 55        while (node != NULL) {
 56            values_in_head_list.push_back(node->key);
 57            node = node->sibling;
 58        }
 59        return *min_element(values_in_head_list.begin(), values_in_head_list.end());
 60    }
 61   
 62    bool CompVector( BinomialHeapNode *left, BinomialHeapNode *right ) {
 63        return left->key < right->key;
 64    }
 65
 66    //弹出二项堆中的最小元素,并获取该最小元素的值
 67    KeyType ExtractMinimum(){
 68        vector<BinomialHeapNode *> head_nodes;
 69        BinomialHeapNode *hl = _head_list;
 70        while ( hl ) {
 71            head_nodes.push_back( hl );
 72            hl = hl->sibling;
 73        }
 74        //auto min_ele = min_element(head_nodes.begin(), head_nodes.end(), [](BinomialHeapNode *left, BinomialHeapNode *right)
 75        //{
 76        //return left->key < right->key;
 77        //});
 78        vector<BinomialHeapNode *>::iterator min_ele = min_element(head_nodes.begin(), head_nodes.end()/*, CompVector*/);
 79        int min_index = min_ele - head_nodes.begin();
 80        KeyType min_value = ( *min_ele )->key;
 81        BinomialHeapNode *min_node = head_nodes[min_index];
 82        //根链表上去掉最小结点,更新兄弟节点的值,注意头尾的处理
 83        if ( min_index == 0 ) {
 84            if ( head_nodes.size() > 1 )
 85                _head_list = head_nodes[1];
 86            else
 87                _head_list = NULL; //根链表上只有一个元素
 88        }
 89        else if ( min_index == head_nodes.size() - 1 )
 90            head_nodes[min_index - 1]->sibling = NULL;
 91        else
 92            head_nodes[min_index - 1]->sibling = head_nodes[min_index + 1];
 93        //处理最小结点的孩子节点
 94        BinomialHeap new_head;
 95        new_head._head_list = min_node->child;
 96        BinomialHeapNode *x = new_head._head_list;
 97        //更新所有孩子节点上的兄弟节点
 98        while ( x ) {
 99            x->parent = NULL;
100            x = x->sibling;
101        }
102        //把独立出来的节点合并到原链表上
103        this->BinomialUnion( new_head );
104        delete min_node;
105        min_node = NULL;
106        return min_value;
107    }
108
109    //减小一个节点的值到某个值
110    void Decrease( BinomialHeapNode *x, KeyType k ){
111        if ( k > x->key )
112            throw exception("只能减少不能增大");
113        x->key = k;
114        BinomialHeapNode *y = x;
115        BinomialHeapNode *z = y->parent;
116        while ( z && y->key < z->key ) {
117            swap(y->key, z->key);
118            y = z;
119            z = y->parent;
120        }
121    }
122
123    //删除一个结点
124    void Delete( BinomialHeapNode *node ){
125        if ( node ) {
126            //将要删除的结点减小到最小值,然后在删除
127            Decrease( node, numeric_limits<KeyType>::min() );
128            ExtractMinimum();
129        }
130    }
131   
132    //查找一个值为key的结点
133    //所有的堆堆查找操作的支持都很差,时间复杂度为O(n)
134    BinomialHeapNode* Search( KeyType key ) const{
135        BinomialHeapNode *tree = _head_list;
136        //遍历根链
137        while ( tree ) {
138            BinomialHeapNode *node = _SearchInTree( tree, key );
139            if ( node )
140                return node;
141            tree = tree->sibling;
142        }
143        return NULL;
144    }
145   
146    //联合另外一个二项堆,当联合操作完成后,other的二项堆中的数据将无效
147    void BinomialUnion( BinomialHeap &other ){
148        vector<BinomialHeapNode *> nodes;
149        BinomialHeapNode *l = _head_list;
150        BinomialHeapNode *r = other._head_list;
151        while ( l ) {
152            nodes.push_back( l );
153            l = l->sibling;
154        }
155        while ( r ) {
156            nodes.push_back( r );
157            r = r->sibling;
158        }
159        if ( nodes.empty() )
160            return;
161        //排序并合并两个二项堆
162        sort( nodes.begin(), nodes.end());
163        for ( size_t i = 0; i < nodes.size() - 1; ++ i )
164            nodes[i]->sibling = nodes[i + 1];
165        nodes[nodes.size()-1]->sibling = NULL;
166        //重置根链表
167        this->_head_list = nodes[0];
168        //销毁待合并的链表
169        other._head_list = NULL;
170        if ( this->_head_list == NULL )
171            return;
172        //将具有相同度的二项树进行合并
173        BinomialHeapNode *prev_x = NULL;
174        BinomialHeapNode *cur_x = _head_list;
175        BinomialHeapNode *next_x = cur_x->sibling;
176        while ( next_x ) {
177            if ( cur_x->degree != next_x->degree || ( next_x->sibling != NULL && next_x->sibling->degree == cur_x->degree ) ) {
178                prev_x = cur_x;
179                cur_x = next_x;
180            }
181            else if ( cur_x->key < next_x->key ) {
182                cur_x->sibling = next_x->sibling;
183                _Link( next_x, cur_x );
184            }
185            else {
186                if ( prev_x == NULL ) 
187                    _head_list = next_x;
188                else 
189                    prev_x->sibling = next_x;
190                _Link( cur_x, next_x );
191                cur_x = next_x;
192            }
193            next_x = cur_x->sibling;
194        }
195    }
196
197    //判断二项堆当前状态是否为空
198    bool IsEmpty() const {
199        return _head_list == NULL;
200    }
201   
202    //得到二项堆的根链表
203    BinomialHeapNode *GetHeadList() {
204        return _head_list;
205    }
206
207    //显示二项堆
208    void Display() const {
209        //stringstream ss;
210        cout << "binomial_heap:" << "{" << endl;
211        BinomialHeapNode *node = _head_list;
212        if ( node )
213            cout << "rootlist->" << node->key << ";" << endl;
214        while ( node ) {
215            _DisplayTree( node );
216            if ( node->sibling )
217                cout << " " << node->key << "->" << node->sibling->key << ";" << endl;
218            node = node->sibling;
219        }
220        cout << "}" << endl;
221    }
222
223private:
224    //删除一棵二项树
225    void _DeleteTree(BinomialHeapNode *tree){
226        if (tree && tree->child) {
227            BinomialHeapNode *node = tree->child;
228            while (node) {
229                BinomialHeapNode *next = node->sibling;
230                _DeleteTree(next);
231                node = next;
232            }
233            delete tree;
234            tree = NULL;
235        }
236    }
237
238    //将D(k-1)度的y结点连接到D(k-1)度的z结点上去,使得z成为一个D(k)度的结点
239    void _Link(BinomialHeapNode *y, BinomialHeapNode *z) {
240        y->parent = z;
241        y->sibling = z->child;
242        z->child = y;
243        ++(z->degree);
244    }
245
246    //在二项树中搜索某个结点
247    BinomialHeapNode * _SearchInTree( BinomialHeapNode *tree, KeyType key ) const{
248        if ( tree->key == key )
249            return tree;
250        BinomialHeapNode *node = tree->child;
251        while( node ) {
252            BinomialHeapNode *n = _SearchInTree( node, key );
253            if ( n )
254                return n;
255            node = node->sibling;
256        }
257        return NULL;
258    }
259  
260    //画一棵二项树
261    void _DisplayTree( BinomialHeapNode *tree ) const {
262        if ( tree ) {
263            BinomialHeapNode *child = tree->child;
264            if ( child ) {
265                vector<BinomialHeapNode *> childs;
266                while ( child ) {
267                    childs.push_back( child );
268                    child = child->sibling;
269                }
270               //for_each( childs.begin(), childs.end(), [&]( BinomialHeapNode *c ){
271               //ss << " " << c->key << "->" << tree->key << ";" << endl;
272               //_DisplayTree( ss, c )
273               //} );
274                for ( vector<BinomialHeapNode *>::iterator it = childs.begin(); it != childs.end(); ++ it ) {
275                    cout << " " << (*it)->key << "->" << tree->key << ";" << endl;
276                    _DisplayTree( *it );
277                }
278            }
279        }
280    }
281
282private:
283    BinomialHeapNode *_head_list; //根链表
284};
285#endif//_BINOMIAL_HEAP_
286二项堆C++实现

四、左倾堆

左倾堆(leftist tree 或 leftist heap),又称为左偏树。这种结构《算法导论》上没有提及,大概是因为太简单了吧。因为其本质是一棵二叉树,不像二项堆和斐波那契堆一样,具有复杂的结构。我想历史的演进过程大概是先提出左倾堆以及接下来要说的斜堆,然后才提出的二项堆和斐波那契堆这种复杂的结构,由易到难嘛,又或者同时,相反,anyway。 二叉堆是非常平衡的树结构,左倾堆,从名字上来看,这种堆结构应该是整体往左倾,不平衡,那是什么导致它往左倾,孩子节点个数?不可能,比如下面这棵树:

如果只从孩子节点个数来评判它往哪倾,则从整体上看,根节点的右孩子节点要多于左孩子节点,但是这棵树是左倾堆。那到底通过什么来评判一棵树为左倾堆?要引入一个属性:零距离(英文名NPL,Null Path Length)——表示的是从一个节点到一个“最近的不满节点”的路径长度(不满节点:两个子节点至少有一个为NULL)。一个叶节点的NPL为0,一个NULL节点的NPL为-1。因此,左倾的意思主要是看每个节点的NPL是否满足左孩子的NPL >= 右孩子的NPL,这也是左倾堆的性质一。当然啦,左倾堆既然叫堆,自然满足堆的性质:即每个节点的优先级大于子节点的优先级,这是性质二。还有性质三就是:节点的NPL = 它的右孩子的NPL + 1。如上图:每个节点旁边的标号即为它们的NPL值。 如前所述,这些堆结构的提出,主要是解决简单二叉堆中Union操作的低性能的。左倾堆的Union操作相对上述两种比较简单,基本思想如下: 1)如果一个空左倾堆与一个非空左倾堆合并,返回非空左倾堆; 2)如果两个左倾堆都非空,那么比较两个根节点,取较小堆的根节点为新的根节点。然后将“较小堆的根节点的右孩子”和“较大堆”进行合并; 3)如果新堆的右孩子的NPL > 左孩子的NPL,则交换左右孩子; 4)设置新堆的根节点的NPL = 右子堆NPL + 1。 上面的Union算法递归调用了自身,由于我们沿着右侧路径递归,所以时间复杂度为lgn量级。至于其他操作,比如insert,delete_min都可以在Union的基础上实现,如insert:将新增节点与一个已有的左倾堆做Union;delete_min删除根节点,将剩余的左右子堆合并。具体的就不再详述,如有疑问,可以参考这篇图文并茂的博客:左倾堆(一)之 图文解析 。(站在别人的肩膀上学习,避免重复造轮子^-^)

五、斜堆

斜堆(skew heap),又称自适应堆,它是左倾堆的一个变种。相比于左倾堆,斜堆的节点没有“NPL”这个属性,合并操作也相对简单了,但同样能实现lgn的量级。具体算法过程的前两步和左倾堆是一样的,只是第三步不像左倾堆要比较左右孩子的NPL大小才交换,而是合并后就直接交换。

六、总结

最常用的堆结构还是要属二叉堆,前面也提到过,如果没有Union操作,二叉堆的性能还是令人满意的。对于一些复杂的问题场景,则相应需要用到复杂的数据结构,此时斐波那契堆是最佳选择,如求最小生成树问题和求单源点最短路径问题的实现,如果基于斐波那契堆,则能得到非常好的性能。但这只是从理论上来说,《算法导论》上也说了,如果从实际应用角度来看,除了某些需要管理大量数据的应用外,对于大多数应用,斐波那契堆的常数因子和编程复杂性使得它比起普通二叉堆并不那么适用。因此,斐波那契堆的研究主要出于理论兴趣。这个时候,如果权衡一下,那就只有左倾堆和斜堆这等堆结构更适用了。是这样吗?不禁陷入了深深的思考中……

本文由aCloudDeveloper投稿,其微信公众号是『aCloudDeveloper』

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-05-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能服务器开发 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、概述
  • 二、二叉堆
  • 三、二项堆
  • 四、左倾堆
  • 五、斜堆
  • 六、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档