普通平衡树Treap
提醒
可以先学一下二叉搜索树,学完了平衡树差不多也会了~
前置知识:二叉树的定义
Part 1 介绍
平衡树的性质:
与搜索树类似,任意一个节点的真实值小于右儿子的真实值大于左儿子的真实值;维护一个小根堆;每个节点除了有一个真实值之外,还有一个随机值。
平衡树相对于搜索树的优点:
平衡树就是为了解决链而生的,当树中出现链时,再使用搜索树进行各种操作消耗时间很大,因为树上问题的复杂度是与节点深度成正相关的,比如说下图:
这是一棵树,也是一条链,在这上边跑搜索树,假设节点数是n,那么最坏的复杂度是O(n)。
如果使用平衡树,就可能会把它变成这样的形状:
因为平衡树维护的是一个小根堆,是根据每个点的随机值进行维护的,因为随机值大概率不会单调递增或递减,所以就可以把一条链变成一颗比较正常的树。
那复杂度就大大减小了,所以这就是平衡树的优点呐~
Part 2 详解
首先要知道要实现的东西是啥:
①插入元素;
②删除元素;
③查询元素排名;
④查询在某个排名上的元素;
⑤找前驱;
⑥找后继;
然后捏,讲准备工作。
- 先知道几个变量的意义:
t[now].siz(节点now的子树大小)
t[now].cnt(节点now的大小)
t[now].ran(节点now上的随机值)
t[now].ls(节点now的左儿子)
t[now].rs(节点now 的右儿子) - 左右旋
//左旋
int lx(int &now) {
int y = t[now].r;
t[now].r = t[y].l;
t[y].l = now;
pushup(now), pushup(y);
now = y;
}
解释一下,如图一,假设3号点的ran小于1号点的ran,为了维护小根堆的性质,要进行左旋,将三号点旋转为一号点的父亲,同时为了维护二叉树的性质,使4号点成为1号点的右儿子,这样既不会影响平衡树的性质,也维护了二叉树和小根堆的性质。
//右旋
int rx(int &now) {
int y = t[now].l;
t[now].l = t[y].r;
t[y].r = now;
pushup(now), pushup(y);
now = y;
}
与左旋同理~
//合并信息
void pushup(int &now) {
t[now].siz = t[t[now].l].siz + t[t[now].r].siz + t[now].cnt;
}
Part 3 解决问题
- 添加元素。
就是分四种情况,没有儿子了、当前点储存的值等于要添加的值、当前值大于要添加的、当前值小于要添加的。
代码如下:
void add(int &now, int &c) {
if(!now) {
now = ++cnt;
t[now].cnt = t[now].siz = 1;
t[now].val = c;
t[now].ran = rand();
return;
}
t[now].siz ++;
if(t[now].val == c) t[now].cnt ++;
if(c > t[now].val) {
add(t[now].r, c);
if(t[t[now].r].ran < t[now].ran) lx(now);
} else if(c < t[now].val) {
add(t[now].l, c);
if(t[t[now].l].ran > t[now].ran) rx(now);
}
pushup(now);
}
- 删除元素
与添加类似,少了没有点这一情况,因为没有点就不用删了,所以不用管。
void del(int &now, int &c) {
if(t[now].val == c) {
if(t[now].cnt > 1) t[now].cnt--, t[now].siz--;
else if(!t[now].l || !t[now].r) now = t[now].l + t[now].r;
else if(t[t[now].l].ran < t[t[now].r].ran) rx(now), del(now, c);
else lx(now), del(now, c);
return;
}
t[now].siz--;
if(t[now].val < c) del(t[now].r, c);
else del(t[now].l, c);
}
- 按值找排名
解释见代码
int qpm(int &c) {
int now = root, k = 0;//k表示之前在已经找到多少比c小的节点了
while(now) {
if(t[now].val == c) return k + t[t[now].l].siz + 1;
//如果该节点存的值与所要找的相等了,也就是找到了,那么直接返回已找过的点加上
//自己左子树的大小,因为排名是值比自己小的点的个数加一,所以返回值时要加一。
if(c < t[now].val) now = t[now].l;
else k += t[t[now].l].siz + t[now].cnt, now = t[now].r;
//如果要找的值比当前点的值大了,那就去右子树找,现在 当前节点和当前节点的左子
//树上所有的点都比c小,所以都存到k中去。
}
return k;
}
- 按排名找值
这与按值找排名是差不多的,不多赘述了。
int qval(int &k) {
int now = root;
while(now) {
if(t[t[now].l].siz < k && t[t[now].l].siz + t[now].cnt >= k) return t[now].val;
if(t[t[now].l].siz >= k) now = t[now].l;
else k -= t[t[now].l].siz + t[now].cnt, now = t[now].r;
}
return 0;
}
- 找前驱、后继
没什么好说的……
//找前驱
int qfr(int &c) {
int now = root, re = -INF;
while(now) {
if(t[now].val < c) re = t[now].val, now = t[now].r;
else now = t[now].l;
}
return re;
}
//找后继
int qne(int &c) {
int now = root, re = INF;
while(now) {
if(t[now].val > c) re = t[now].val, now = t[now].l;
else now = t[now].r;
}
return re;
}
关于复杂度及其证明:
设n为树内节点的个数,h为树的高度
树的各种操作均为O(h),h可能是log(n),也可能是n
普通的二叉树,操作复杂度均为log(n),最坏情况下可能是O(n),随机构造树的平均高度为log(n),所以平均复杂度为log(n)
插入和删除操作的复杂度均为log(n),旋转操作可能会到达log(n)次
那就讲完啦~