树和树结构(1) : 二叉堆和堆排序

参考 百度词条 树结构
参考书目 算法导论
传送门 请在heap.h中找到完整的源码

树结构
树是一种重要的非线性数据结构,直观地看,它是数据元素(在树中称为结点)按分支关系组织起来的结构,很象自然界中的树那样。树结构在客观世界中广泛存在,如人类社会的族谱和各种社会组织机构都可用树形象表示。

定义
一棵树(tree)是由n(n>0)个元素组成的有限集合,其中:
(1)每个元素称为结点(node);
(2)有一个特定的结点,称为根结点或根(root);
(3)除根结点外,其余结点被分成m(m>=0)个互不相交的有限集合,而每个子集又都是一棵树(称为原树的子树)
一棵树可以直观的表示为

  
  
  1. 1
  2. / \
  3. 2 3
  4. / \
  5. 4 5

树有一些重要的概念, 如:

 树的度——也即是宽度,简单地说,就是结点的分支数。以组成该树各结点中最大的度作为该树的度,如上图的树,其度为3;树中度为零的结点称为叶结点或终端结点。树中度不为零的结点称为分枝结点或非终端结点。除根结点外的分枝结点统称为内部结点。

树的深度 ——组成该树各结点的最大层次,如上图,其深度为3;

层次 根结点的层次为1,其他结点的层次等于它的父结点的层次数加1.

完全二叉树 除最后一层外,每一层上的节点数均达到最大值;在最后一层上只缺少右边的若干结点。

二叉堆
堆(英语:heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
(1)堆中某个节点的值总是不大于或不小于其父节点的值;
(2)堆总是一棵完全树。
其中, 符合堆性质的完全二叉树称为二叉堆.根据定义, 二叉堆可以分为大根堆和小根堆.例如下图是一个小根堆:

  
  
  1. 1
  2. / \
  3. 3 7
  4. / \ /
  5. 12 15 14

不难发现, 堆可以用来高速的查询最大值(或最小值). 因此, 弗罗依德(Floyd)同他人一起发明了基于堆的堆排序算法. 其中, 堆的主要操作有三个--push(), pop(), top()

代码与实现

1, 堆的数据结构(代码来自TOCL)
因为堆始终是一棵完全二叉树, 可以直接用数组存储, 即按照编号存储. 例如上一个图中的堆可以表示为:

  
  
  1. 1 3 7 12 15 14

不难发现, 每个节点i的左孩子编号为2i, 右孩子为2i+1. 因此写出堆的数组存储(这里用指针实现)

template <typename T>
//泛型模板, 可以使用heap<int> h; 创建一个int类型的小根堆.
class heap {
        private:
        T *h;
        int heap_size;
        int topp;
        /*堆顶指针*/
        public:
        /*构造/析构函数*/
        heap(int size){
                /*必须指定堆的大小*/
                heap_size = size;
                topp = 0;
                h = new T[size+1];
        }
        ~heap() {
                delete[] h;
                /*删除堆*/
        }
        ...
};


2, 入堆的操作push()

将一个节点插入堆的方法是: 先将节点加入数组, 然后不断的向上比较. 如果已经满足堆的性质(这里指父节点已经比当前节点小), 即退出; 否则将此节点和父节点交换, 并再次向上比较, 直到满足性质或到达堆顶.

void push (T data) 
{
        h[++topp] = data;
        int now = topp;
        /*当前元素指针*/
        int next = half(now);
        /*当前元素的父节点*/
        while (now > 1) {
                next = half (now);
                /*定理 堆中节点i的左右孩子编号为2i,2i+1, 父节点编号为i/2*/
                if (h[now] < h[next]) {
                        swap (h[now], h[next]);
                        now = next;
                /*交换*/
                } else {
                        return;
                        /*否则插入完成,结束*/
                }
        }
}


3, 将优先级最大的节点退出堆 pop()

退出堆的算法是: 将根节点用最末尾的节点代替, 并将堆的大小-1. 再自顶而下的选择较小的子节点. 若当前节点优先级小于其优先级较大的子节点, 则交换之, 并继续向下更新, 知道满足性质或到达堆的末尾.

T pop () 
{
        T res = h[1];
        h[1] = h[topp--];
        int now = 1;
        /*当前元素指针*/
        int next = twice(now);
        /*当前元素的左孩子节点*/
        while (twice(now) <= topp) {
                next = twice (now);
                /*见push解释*/
                if (next < topp && h[next+1] < h[next]) 
                        next ++;
                /*选择较小的孩子*/
                if (h[now] < h[next] || h[now] == h[next])
                        return res;
                swap (h[now], h[next]);
                        /*交换*/
                now = next;
        }
        return res;
        //返回优先级最大的值
}


4, 获取堆顶(可以不写)

很简单, 直接获取数组第一个即为根

T top () 
{
        return h[1];
}


5, 堆排序

也很简单, 通过多次push建一个堆, 再用pop逐个取出.

template <typename T> 
void heap_sort (T array[], int begin, int end)
{
        heap<T> h (end - begin + 1);
        /*建立一个堆*/
        for (int i = begin; i <= end; i++)
                h.push (array[i]);
        /*通过push建小根堆*/
        for (int i = begin; i <= end; i++) 
                array[i] = h.pop();
        /*逐个取出*/
}


6, 堆排序的效率和稳定性分析
堆排序的时间,主要由建立初始堆和出堆这两部分的时间开销构成.建立堆的时间复杂度为O(nlogn), 出堆的复杂度也为O(nlogn), 总效率为O(nlogn). 而且堆排序不会出现快速排序的最坏情况, 是一种高效的排序算法(尤其是在数据量很大的情况下).
它是不稳定的排序方法.

7, 堆的其他用途
由push和pop可以看到, 堆可以作为高效的优先队列.


思考
1, 算法导论中使用了一种更为高效的建堆方法, 可以探究一下.
2, 试着用递归方法改写push和pop函数


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值