一、首先,什么是区间操作?以及各种数据结构性能对比
区间操作就是对一个序列的某个区间的所有元素进行的操作。比如,对区间所有元素增加一个值,翻转区间元素等。
对区间操作,最普通的方法就是数组。比如:对一个长为N 的序列的 [L,R]区间执行每个元素加上k 的操作,可以使用数组来保存序列,然后使用循环对[L,R]区间每个元素加k 。
代码是这样的:
//int A[],L,R,k;for i = L to RA[i] += k;
这样做的效率是 O(N),对于一个排序来说这是很好的性能,但对于一个操作来说,这并不是理想,很多二叉树的操作都能达到O( log N) 级别。
对于最典型的查找操作,普通二叉树的操作能达到O( log N) ,但是树在最坏情况下性能会退化到O(N)(比如順边的情况下) 。
平衡树能对树高度进行控制,最坏性能控制在O(log N),但是,平衡树只是控制了树的高度,而不能保证访问频率高的节点离跟越近,因此,平衡树的访问效率与节点的分布有关,如果访问频率高的节点离根很远,那么平衡树的性能就会有所降低。
因此,便有了伸展树。伸展树的特点就是:每次查找或插入节点,都会把该节点旋转到树根位置,随着操作次数增加,高频节点就会聚集在根周围,从而达到较好的性能。
二、伸展树介绍
伸展树的特点就是:每次查找或插入节点,都会把该节点旋转到树根位置,随着操作次数增加,高频节点就会聚集在根周围,从而达到较好的性能。
伸展树首先是一棵树,可以定义如下:
typedef struct Node
{int key; struct Node *lch,*rch,*parent;
}* Node ,* Tree;
伸展树的高层操作有:
高层操作 | 实现 |
---|---|
insert( t , x ) | 将x插入t中。找到x在t中的位置,插入x,然后再将x旋转到树根 |
delete( t ,x ) | 删除x。找到x,将x调到根部,将x的左子树和右子树删除后合并。 |
spilt( t , x) | 以x为界分离t。找到x,将x旋转到树根,然后将x的左子树和剩余部分分离 |
find( t , x ) | 找到x。找到x,然后将x旋转到树根。 |
join( t1 , t2 ) | 合并子树 t1和t2,要求t1所有元素小于t2的任一元素。找到t1的最大元素,并调整到根部,将t2作为根的右子树插入 |
可见,树的高层操作都依赖与旋转等基本操作:
基本操作 | 实现 |
---|---|
splay( t ,x ) | 将x调至根部。循环调用zig_zag_manager( t , x) 方法,直到x == t |
zig_zag_manager( t , x) | 旋转管理者。 找到x,分析x与父亲节点,父亲节点与祖父节点的关系,选择恰当的旋转方式。 |
下面几个函数中,设x 的父节点为 p, p的父节点为g 。 | |
zig( t , x ) | 右旋。当p是根节点,x是p的左孩子,将x右旋 |
zag( t , x ) | 左旋。当p是根节点,x是p的右孩子,将x左旋 |
zig_zig( t , x ) | 右双旋。x是p的左节点,当p是g的左节点,先将p右旋,再将x右旋 |
zag_zag( t , x ) | 左双旋。x是p的右节点,当p是g的右节点,先将p左旋,再将x左旋 |
zig_zag( t , x ) | 右旋再左旋。x是p的左节点,当p是g的右节点,先将x右旋,再将x左旋 |
zag_zig( t , x ) | 左旋再右旋。x是p的右节点,当p是g的左节点,先将x左旋,再将x右旋 |
其实上述左旋和右旋很有规律,当一个节点是左节点时,应当右旋,当一个节点是右节点时,应当左旋。
伸展树的区间操作
伸展树操作区间的思路是:
第一步,分离区间。分离长度为N的序列里的区间 [L ,R ],首先找到第L大的元素,然后以其为界进行分离操作,得到t1 = [0,L-1] 和 temp = [L ,N]。
然后,再找到 temp 中第 R-L+1 大元素,并以该元素为界进行分离操作, 得到 t2 = [L ,R] 和 t3 = [R+1, N]。这样,就成功分离出了目标区间t2 。
第二步,对目标区间进行操作。
第三步,合并区间。合并t1,t2,t3。