平衡树
是一种二叉查找树,其平衡性使得树的深度在$\log n$以内,增加、删除等操作可以做到 $O(\log n)$.
平衡树的实现有多种,本文主要介绍 AVL,Treap,FHQ Treap 与 Splay .
AVL
介绍
AVL 是这些算法中时间复杂度最优秀的,也是码量最大的.
其原因在于:AVL 是维护绝对平衡,即左子树高度与右子树高度之差 $\leq 1$
所以每一次维护造就了优秀的时间复杂度 与超大的码量 .
平衡维护
可以说,平衡维护是铸造二叉平衡树最关键的一步,也是最难理解的一步.
如何维护?
1.先判断左子树与右子树高度之差.
2.再判断较高的那一棵子树是它的左子树高还是右子树高.
3.最后再进行旋转操作使其平衡.
相信第1步很容易理解,这里不作过多解释.
为什么会有第2步的判断?
因为可能出现不同的情况,我们也需要做出不同的旋转.
如下图,左子树根节点(节点3)的左儿子(节点2)使左子树高度增加为2,而右子树高度为0,所以平衡树不平衡.
再如下图,左子树根节点(节点15)的右儿子(节点18)使左子树高度增加为2,而右子树高度为0,所以平衡树不平衡.
同理,也有右子树高度高于左子树的情况.
明显可见,当多出来的那个节点与它的父亲、父亲的父亲(祖父)成一条线时,只需要通过一次旋转.
当不成一条线时,需要通过两次旋转.
单旋转分为两种,左旋(zag)与右旋(zig).
如下图,为左旋.
如下图,为右旋.
双旋转则多为先进行一次方向旋转,使其呈链状后,再进行一次反方向旋转.
如下图,为需要维护的不平衡状态.
又如下图,为进行旋转(左旋A,B)使三点共链的状态.
再如下图,为进行反方向旋转(右旋C,B)使其平衡的状态.
最后保持平衡.
因为其不同方向两次旋转的特性,所以双旋转分为左右旋(zagzig)和右左旋(zigzag).
代码实现平衡维护
|
|
代码实现旋转操作
|
|
小飞版来咯~
Code
|
|
Treap
介绍
虽然 AVL 敲可爱哒~
但是在考场上真的来得及打吗?
其实只要你练得够多,最快可在10min内打完,一般人也可以练进12min.
明显地,维护平衡操作太长了,可不可以省去?
或许我们用一些随机值赋值给不同节点,使其成为节点的优先级,在构造二叉查找树时使其满足点权有序性与优先级(随机值)有序性.
如果这样,随机生成的数字一般可以使二叉查找树接近平衡.可理解为相对平衡但不是绝对平衡.
当然,除非你考前被奆佬%而rp–, 一般随机值不会刚好使其变成一条链.
在大数据下更具有代表性,即维护平衡树平衡的成功概率与数据大小成正比.
所以,就有了时间复杂度不如 AVL 优秀,但是足够的—— Treap !!
$Treap$=$Tree+Heap$.
从名字上可见它满足点权有序性(二叉查找 tree ),和优先级(随机值)有序性(大根或小根 heap ).
平衡维护
其实 Treap 的维护与 AVL 很像,这里就不放图了.
它们之间的区别或许只是维护时的条件不同.
- AVL 是当左右子树高度相差超过1时进行维护
- Treap 是当左右子树优先级小于(或大于,这里不作定性要求,但是同一代码中必须都是小于或大于)根节点优先级时进行维护
还有一点,因为 Treap 讲究好写,所以只需要在插入数据的时候维护一下就可以了,删除后不用再次维护.
代码实现插入数据与维护
|
|
小飞版来咯~
Code
|
|
FHQ Treap
介绍
可以说,旋转操作是占主流算法的大多数.
但是一旦牵扯到可连续性等方面乃至理解方面,旋转操作是不太可取的.
于是范浩强奆佬就发明了不用旋转的 Treap 算法—— FHQ Treap !!
也称作无旋 Treap.
不旋转,怎么维护平衡?
分裂!!
合并!!
分裂操作与合并操作
分裂split
分裂操作split是最难理解的也是最关键的一步.
我们在插入数据和删除数据需要找到一个合适的点.
而找这个点的路径,可以把树分裂成两部分,把小于等于插入值的分裂到左树,大于的就分裂到右树.
就这样,我们可以得到两棵树(也有可能是空树).
合并merge
合并操作的对象是两棵树,这两棵树一定满足,左边的树权最大值小于右边的树的权值最小值.
我们根据其优先级来合并.
为了描述方面,我们设左边的树为 $L$,右边的树为 $R$.
首先比较两棵树的树根,谁优先级小,谁就作为新的树根.
假设 $L$ 的优先级较小,那么 $L$ 的根做新的树根,则问题转换为 $L$ 的右子树与 $R$ 的合并问题了;否则就是 $R$ 的根作为新树的根,问题转换为 $L$ 和 $R$ 的左子树的合并问题了.
明显地,可以写成递归.
这样递归下去,直到某棵树为空,则递归结束。
代码实现split与merge
|
|
小飞版来咯~
Code
|
|
Splay
介绍
Splay 也称为伸展树,它也满足二叉查找树的性质.
即一个节点的左子树的所有点权都会小于当前节点,右子树反之.
它和其他平衡树算法最大不同的是,它不需要主动维护平衡,我们只需要在每个操作结束后进行一次splay(x,goal)的操作,它就可以自己维护了.
当然splay(x,goal)里面还是有旋转操作的.
splay(x,goal)伸展操作
splay(x,goal)操作也就是伸展操作.
通过一系列旋转使得 x 转到 goal 节点,旋转也需要分情况而论.
以下 goal 节点为 root 根节点举例.
情况一:节点 x 的父节点 y 是根节点. 这时如果 x 是 y 的左儿子,则进行一次 zig 操作;反之同理. 如图 1 所示.
情况二:节点x的父节点y还有一个父节点z. 且此时三点成链,则进行一次zigzig操作或zagzag操作(是的,此处旋转不需要维护平衡,只是为了将x旋转到目标节点),如图2所示.
情况三:节点x的父节点y还有父节点z. 且此时三点呈“之”字形,则进行一次zagzig操作或zigzag操作,如图3所示.
如图5,是一次splay(2,rt)的操作.
明显地,经过 splay()
操作后,平衡树明显“平衡”了许多.
所以我们在一些插入、查询等操作后就做一遍 splay()
.
这样做的话,时间复杂度可以下降. 但是同样地,常数也就增加了. 所以 Splay 算法是本文 4 个算法中最慢的.
代码实现 splay(x,goal)
|
|
小飞版来咯~
Code
|
|
最后
本文写得还是很仓促的,有些漏洞大家原谅一下.(^^)
本文还有很多不完善的地方,例如 AVL 代码没有指针版、图片模糊等.
欢迎大家在评论区留下建议或疑问、收藏、转发学习.
转载请注明出处.
仅供参考、学习用,不商用.
The End