前置知识
平衡树
平衡树指的是任意节点的子树的高度差都小于等于 1 1 1 的二叉查找树。
因为他是平衡的,我们做树上的操作就可以降到 O ( l o g n ) O(log_n) O(logn) 的时间复杂度。
AVL平衡树
因为对于树有可能进行插入或删除操作,使得树有可能不平衡,甚至变成链,导致操作变成 O ( n ) O(n) O(n) 的时间复杂度。
所以我们需要对树进行一些操作,使得它变成平衡的。
这里有很多种,比如说 s p l a y , t r e a p , f h q t r e a p , A V L splay,treap,fhqtreap,AVL splay,treap,fhqtreap,AVL ,这里主要讲讲 A V L AVL AVL 。
因为我们需要插入或者删除一个节点,某个节点的平衡因子的绝对值小于等于 2 2 2 ,所以我们只需要把子树旋转一下就好了。(我们这里的平衡因子定义为左子树的高度减去右子树的高度)
然而旋转有四种情况。
我们先来看看旋转的基本操作。
旋转
基本操作
右旋(zig)
对于以 x x x 为根的子树,如下图。
我们将它右旋一下,则会变成下图。
本质上就是将 x x x 的左子树变成 y y y 的右子树, y y y 的右儿子变成 x x x ,其他不变。
代码实现如下:
void zig(int &r) {
int t = tree[r].l;
tree[r].l = tree[t].r, tree[t].r = r, updata(r), updata(t), r = t;//updata就是更新子树的大小和高度。
}
左旋(zag)
还是以 x x x 为根的子树。
左旋之后如下图。
本质上就是将 x x x 的右子树变成 z z z 的左子树,将 z z z 的左儿子变成 x x x ,其他不变。
代码实现如下:
void zag(int &r) {
int t = tree[r].r;
tree[r].r = tree[t].l, tree[t].l = r, updata(r), updata(t), r = t;
}
基本的两个操作就讲完了,我们来看看如何旋转。
旋转的分类
如果左右子树高度差的绝对值大于 1 1 1 ,则我们需要靠旋转来维护。
假设我们以 x x x 为根的子树的平衡因子的绝对值大于 1 1 1 ,则旋转分为一下四种情况。
- x x x 的平衡因子为 2 2 2 , x x x 的左子树的平衡因子为 1 1 1 ,则需要对 x x x 进行一次 z i g zig zig 。
- x x x 的平衡因子为 2 2 2 , x x x 的左子树的平衡因子为 − 1 -1 −1 ,则需要先将 x x x 的左儿子进行一次 z a g zag zag ,在对 x x x 做一次 z i g zig zig 。
- x x x 的平衡因子为 − 2 -2 −2 , x x x 的右子树的平衡因子为 − 1 -1 −1 ,则需要对 x x x 进行一次 z a g zag zag 。
- x x x 的平衡因子为 − 2 -2 −2 , x x x 的右子树的平衡因子为 1 1 1 ,则需要先将 x x x 的右儿子进行一次 z i g zig zig ,在对 x x x 做一次 z a g zag zag 。
经过这样旋转,以 x x x 为根的子树就又平衡了。
代码实现如下:
int BF(int r) { return tree[tree[r].l].h - tree[tree[r].r].h; }
void maintain(int &r) {
updata(r);
int fs = BF(r), fl;
if (fs > 1) {
fl = BF(tree[r].l);
if (fl > 0)
zig(r);
else
zag(tree[r].l), zig(r);
}
if (fs < -1) {
fl = BF(tree[r].r);
if (fl < 0)
zag(r);
else
zig(tree[r].r), zag(r);
}
}
因为平衡树也是二叉查找树,所以查询就跟二叉查找树一样,因为旋转操作不会改变树的 d f s dfs dfs 序。