-1.想看代码的人
luoguP3369 普通平衡树,想学的人往下翻
#include "bits/stdc++.h"
using namespace std;
int son[100010][2],value[100010],ran_dom[100010],size[100010],root,t;
void update(int p)
{
size[p]=size[son[p][0]]+size[son[p][1]]+1;
}
void rotate(int &p,bool op){
int a=son[p][!op];
son[p][!op]=son[a][op];
son[a][op]=p;
update(p);
update(a);
p=a;
}
void insert(int &p,int v){
if(!p){
p=++t;
value[p]=v;
ran_dom[p]=rand();
size[p]=1;
return;
}
size[p]++;
bool op=v>value[p];
insert(son[p][op],v);
if(ran_dom[son[p][op]]>ran_dom[p])rotate(p,!op);
}
void delet_(int &p,int v)
{
if(!p)return;
if(v==value[p]){
if(son[p][0]&&son[p][1]){
bool op=ran_dom[son[p][0]]>ran_dom[son[p][1]];
rotate(p,op),delet_(son[p][op],v);
}
else{
p=son[p][0]|son[p][1];
return;
}
}
else delet_(son[p][v>value[p]],v);
update(p);
}
int rank(int p,int v){
if(!p)return 1;
if(v>value[p])return rank(son[p][1],v)+size[son[p][0]]+1;
else return rank(son[p][0],v);
}
int knar(int p,int v){
int op=size[son[p][0]]+1;
if(v<op)return knar(son[p][0],v);
else if(v>op)return knar(son[p][1],v-op);
else return value[p];
}
int pre(int p,int v){
if(!p)return -9999999;
if(v>value[p])return max(pre(son[p][1],v),value[p]);
else return pre(son[p][0],v);
}
int erp(int p,int v){
if(!p)return 9999999;
if(v<value[p])return min(erp(son[p][0],v),value[p]);
else return erp(son[p][1],v);
}
int main(){
int n,op,m;
cin>>n;
for(int i=1;i<=n;i++){
cin>>op>>m;
if(op==1)insert(root,m);
if(op==2)delet_(root,m);
if(op==3)cout<<rank(root,m)<<endl;
if(op==4)cout<<knar(root,m)<<endl;
if(op==5)cout<<pre(root,m)<<endl;
if(op==6)cout<<erp(root,m)<<endl;
}
return 0;
}
0.前置知识
构造一棵树。中序遍历。
1.引入
二叉搜索树是一种神秘的高级数据结构,他可以做很多神秘的高级操作,比如说 l o g n logn logn排序等等。它把一些数据存在了一棵树中,这棵树严格遵循着一种特定的构造规律。
2.构造规律
一般来说,二叉搜索树遵循这样一个规律:
对于树中任意一颗子树来说,它的根节点的左子树的所有节点都比根节点小,右子树的所有节点都比根节点大。根节点的左右子树也是二叉搜索树。
比如这样。
更直观的理解,如果你把一颗二叉搜索树标准的放置,从左到右看,它的点权应该是有序的。或者说,它的中序遍历是有序的。
每次插入一个点时,首先来到根节点的位置,如果他比这个节点小那就走到这个节点的左子节点,否则去右子节点,然后持续做这个操作,直到这个节点为空,把他放在这里。
代码:
void insert(int &p,int v){
if(!p){
p=++t;
value[p]=v;
return;
}
if(v>value[p])insert(r[p],v);
else insert(l[p],v);
}
那么删除呢?
删除的时候,有三种可能:
-
删除叶子结点
直接删除 -
删除有一个子节点的节点
删除节点并将其子节点移动至该点位置。
-
删除有两个子节点的节点
找到这个节点的右子树的中序遍历的第一个节点(其实就是找后继),把他和这个后继交换,然后删除。(或者找前驱应该也可行)
3.实现操作
刚才说到了,二叉搜索树中序遍历是有序的,那么我们就可以利用这点来排序。还可以查找某个数,找最大值,找最小值,求排名,还有找前驱,后继(大于x最小,小于x最大)
还有很多神秘操作你们自己去探索吧。
4.隐秘问题
有时,这样一个好好地二叉搜索树可能会被毒瘤数据卡爆。
比如
1
2
3
4
5
6
7
8
。
98765234523454524321哈哈被你发现了哈哈哈哈哈哈哈
\color{green}1\color{blue}2\color{yellow}3\color{purple}4\color{red}5\color{pink}6\color{mint}7\color{gray}8\color{black}。\color{white}\text{98765234523454524321哈哈被你发现了哈哈哈哈哈哈哈}
12345678。98765234523454524321哈哈被你发现了哈哈哈哈哈哈哈
这种毒瘤会让二叉搜索树在建立时退化成一条链。
所以
5.平衡二叉树
有好多种平衡二叉树,总的来说就是让这个二叉树期望深度成为
l
o
g
n
logn
logn,不成为像上图那样的链。
先来讲第一种:
这是一种严格平衡的算法(即任意节点的两个子树的深度差<=1)
如果出现了不平衡的二叉树,就需要调整。
怎么调整呢?这里就要用到一种非常玄幻的操作:旋转。
在这种算法中,由于他是需要严格平衡的,所以一共由四种(不算对称的情况,有两种)情况:
第一种情况,是根的左子树深度大,左子树的左子树深度大。在下图中,
α
β
δ
\alpha\beta\delta
αβδ都代表着子树,其中
β
\beta
β更深。很显然,为了平衡,需要把
β
\beta
β向上转。
这种情况下,我们把y提到根的位置,然后因为
α
\alpha
α比x小,放在x的左子节点的位置。x的右子节点保留是
δ
\delta
δ,y的左子节点保留是
β
\beta
β。
变成这样:
这样
/
b
e
t
a
/beta
/beta就向上移动了一层,使得我们离平衡进了一步。这种旋转称作左左旋转。
如果反过来(也就是跟的右子树深度大,右子树的右子树深度大),也是这样旋转。这就是右右旋转。
第三种情况:根的左子树深,左子树的右子树深。
还是这个图,但是这次是
α
\alpha
α更深。
这次处理就需要添加一些细节。要旋转两次,第一次旋转是旋转把
/
a
l
p
g
a
/alpga
/alpga旋转到左子节点,和右右旋转是一样的,旋转结果是这样:
第二次旋转转是把
α
\alpha
α转到根节点,这次是左左旋转,结果转成这样:
他不就平衡了吗。所以先右右,再左左,就转完了。这就是左右旋转。
反之就是右左旋转。
四种情况都说完了,对每个节点都判断一下就行了。
这种算法叫做AVL算法,他有四种旋转,写起来非常繁琐。我们一般不写他,但是他确实是严格的二叉平衡树。由于代码非常繁琐,这里就不贴了,想看代码的出门左转。
为什么要这么繁琐,因为在旋转的同时要保证他二叉搜索树的特性。读者可以自己试一试,看看这些树的中序排列有没有变。
还有一种方便得多的算法。
这种方法首先给每个点赋一个随机权值
r
r
r,然后根据原来的点权
v
v
v和这个
r
r
r建立一颗确定的树。这棵树要求满足以
v
v
v为关键字是一颗二叉搜索树,以
r
r
r为关键字是一个堆,也就是tree+heap,这就是算法名字由来:Treap。
虽然说这棵树的形态是随机的,但是他的期望深度是
l
o
g
n
log n
logn的。
为什么呢?我们来做个思想实验。
有这么一棵树,你不断随机往里加节点,节点在任意位置的概率是同等的,随着树深度的增加,截面也增加了,出现极端状况的概率也就减小了。如果要出现卡爆的极端情况,那就需要每层都出现极端情况,这个概率不就小到指数级爆炸抽搐了吗。
这种算法另一个好处是只用写两种(其实只用一种,另一种直接反过来)旋转。
什么旋转呢?其实和刚才AVL的左左和右右旋转是一样的。。
那我就直接放代码了
void rotate_l(int &p){
int a=left[p];
left[p]=right[a];
right[a]=p;
p=a;
}
void rotate_r(int &p){
int a=right[p];
right[p]=left[a];
left[a]=p;
p=a;
}
如果看得够仔细的话,可以发现,这个代码的赋值形成了一个环形回路。
这不就很好记了吗。
再给一个插入的完整代码:
void insert(int &p,int v){
if(!p){
p=++t;
value[p]=v;
ran_dom[p]=rand();
return;
}
if(v>value[p]){
insert(r[p],v);
if(ran_dom[r[p]]<ran_dom[p]){
int a=r[p];
r[p]=l[a];
l[a]=p;
p=a;
}
}else{
insert(l[p],v);
if(ran_dom[l[p]]<ran_dom[p]){
int a=l[p];
l[p]=r[a];
r[a]=p;
p=a;
}
}
}
用这个代码就可以往里加各种东西,然后在短时间内完成高妙操作。
加油吧!
再附一个及精妙的宝藏。