参考 百度词条 树结构
参考书目 算法导论
传送门 请在heap.h中找到完整的源码
树结构
树是一种重要的非线性数据结构,直观地看,它是数据元素(在树中称为结点)按分支关系组织起来的结构,很象自然界中的树那样。树结构在客观世界中广泛存在,如人类社会的族谱和各种社会组织机构都可用树形象表示。
定义
一棵树(tree)是由n(n>0)个元素组成的有限集合,其中:
(1)每个元素称为结点(node);
(2)有一个特定的结点,称为根结点或根(root);
(3)除根结点外,其余结点被分成m(m>=0)个互不相交的有限集合,而每个子集又都是一棵树(称为原树的子树)
一棵树可以直观的表示为
1
/ \
2 3
/ \
4 5
树有一些重要的概念, 如:
度 树的度——也即是宽度,简单地说,就是结点的分支数。以组成该树各结点中最大的度作为该树的度,如上图的树,其度为3;树中度为零的结点称为叶结点或终端结点。树中度不为零的结点称为分枝结点或非终端结点。除根结点外的分枝结点统称为内部结点。
树的深度 ——组成该树各结点的最大层次,如上图,其深度为3;
层次 根结点的层次为1,其他结点的层次等于它的父结点的层次数加1.
完全二叉树 除最后一层外,每一层上的节点数均达到最大值;在最后一层上只缺少右边的若干结点。
二叉堆
堆(英语:heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
(1)堆中某个节点的值总是不大于或不小于其父节点的值;
(2)堆总是一棵完全树。
其中, 符合堆性质的完全二叉树称为二叉堆.根据定义, 二叉堆可以分为大根堆和小根堆.例如下图是一个小根堆:
1
/ \
3 7
/ \ /
12 15 14
不难发现, 堆可以用来高速的查询最大值(或最小值). 因此, 弗罗依德(Floyd)同他人一起发明了基于堆的堆排序算法. 其中, 堆的主要操作有三个--push(), pop(), top()
代码与实现
1, 堆的数据结构(代码来自TOCL)
因为堆始终是一棵完全二叉树, 可以直接用数组存储, 即按照编号存储. 例如上一个图中的堆可以表示为:
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函数