参考 百度词条 树结构
参考书目 算法导论
传送门 请在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函数