一、平衡树用来干什么
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
- 插入 xxx 数
- 删除 xxx 数(若有多个相同的数,因只删除一个)
- 查询 xxx 数的排名(排名定义为比当前数小的数的个数 +1+1+1 )
- 查询排名为 xxx 的数
- 求 xxx 的前驱(前驱定义为小于 xxx,且最大的数)
- 求 xxx 的后继(后继定义为大于 xxx,且最小的数)
二、平衡树与二叉排序树区别
平衡树是二叉搜索树和堆合并构成的数据结构,它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡树的平均查找长度要小于等于二叉排序树的平均查找长度
平衡树是二叉排序树通过旋转来达到最优二叉排序树
平衡树本身就是二叉排序树
不懂二叉排序树的可以看一下:二叉排序树的构造 && 二叉树的先序、中序、后序遍历
三、二叉平衡树复杂度
前提:包含n个顶点的二叉平衡树(AVL树)
1、查找一个节点时间复杂度为O(lgn)
2、插入的时间复杂度O(lgn)
3、删除一个节点时间复杂度为O(lgn)
代码中会发现每一次操作之后都会进行旋转操作,这样导致平衡树的时间复杂度常数很大
四、平衡树的构造
1、变量声明
f[i]表示i的父结点,ch[i][0]表示i的左儿子,ch[i][1]表示i的右儿子,key[i]表示i的关键字(即结点i代表的那个数字),cnt[i]表示i结点的关键字出现的次数(相当于权值),sizes[i]表示包括i的这个子树的大小;sz为整棵树的大小,rt为整棵树的根。
注意:
sz代表的是不同节点种类数,比如你要插入5 5 4 3 4这些数,那么sz的大小是3
sizes代表的就是节点有几个,就不是种类个数了
几个小函数:
void clears(int x) //删除x点信息 { f[x]=cnt[x]=ch[x][0]=ch[x][1]=sizes[x]=key[x]=0; } bool get(int x) //判断x是父节点的左孩子还是右孩子 { return ch[f[x]][1]==x; //返回1就是右孩子,返回0就是左孩子 } void pushup(int x) //重新计算一下x这棵子树的节点数量 { if(x) { sizes[x]=cnt[x]; if(ch[x][0]) sizes[x]+=sizes[ch[x][0]]; if(ch[x][1]) sizes[x]+=sizes[ch[x][1]]; } }
2、旋转操作
怎么旋转看下面
让4旋转到他的父亲位置2(我们代码中的旋转就是让一个节点移动到他的父亲节点位置)
首先我们要知道平衡树就是二叉排序树,那么二叉排序树儿子节点的特点就是左节点的值小于父节点的值,右节点的值大于父节点的值
那么4节点移动到了2节点(4节点的父亲节点)的位置,4节点会把它的右儿子(也就是9号节点)给2号节点充当左儿子,然后2号节点在作为4号节点的右儿子节点出现
旋转操作代码:
1 void rotates(int x) //将x移动到他父亲的位置,并且保证树依旧平衡 2 { 3 int fx=f[x],ffx=f[fx],which=get(x); 4 //x点父亲,要接受x的儿子。而且x与x父亲身份交换 5 ch[fx][which]=ch[x][which^1]; 6 f[ch[fx][which]]=fx; 7 8 ch[x][which^1]=fx; 9 f[fx]=x; 10 11 f[x]=ffx; 12 if(ffx) ch[ffx][ch[ffx][1]==fx]=x; 13 14 pushup(fx); 15 pushup(x); 16 }
3、splay操作
这个就是把树上的一个节点旋转的树根位置,分为两种操作:
<一>、
一个点p和他的父亲(fa)同为一边的儿子(如图同为左儿子) 此时应先rotate(p.fa), 再rotate(p)
<二>、
一个点p和他的父亲不是同一边的儿子 此时两次rotate(p)即可
1 void splay(int x) //将x移动到数根节点的位置,并且保证树依旧平衡 2 { 3 for(int fx; fx=f[x]; rotates(x)) 4 { 5 if(f[fx]) 6 { 7 rotates((get(x)==get(fx))?fx:x); 8 //如果祖父三代连城一条线,就要从祖父哪里rotate 9 //至于为什么要这样做才能最快……可以去看看Dr.Tarjan的论文 10 } 11 } 12 rt=x; 13 }
注意:void splay(int x) 这个x是这个节点在树上的位置
假设上面节点1、fa、3的cnt值都为1
那么这个fa这个节点在树上的位置就是2,3这个节点在树上的位置就是3
4、void inserts(int x),插入一个节点(这个x是你要插入值的大小)
代码实现
1 void inserts(int x) 2 { 3 if(rt==0) 4 { 5 sz++; 6 key[sz]=x; 7 rt=sz; 8 cnt[sz]=sizes[sz]=1; 9 f[sz]=ch[sz][0]=ch[sz][1]=0; 10 return; 11 } 12 int now=rt,fx=0; 13 while(1) 14 { 15 if(x==key[now]) 16 { 17 cnt[now]++; 18 pushup(now); 19 pushup(fx); 20 splay(now); //splay的过程会rotates now点的所有祖先节点,这个时候它们所有子树权值也更新了 21 return; 22 } 23 fx=now; 24 now=ch[now][key[now]<x]; 25 if(now==0) 26 { 27 sz++; 28 sizes[sz]=cnt[sz]=1; 29 ch[sz][0]=ch[sz][1]=0; 30 ch[fx][x>key[fx]]=sz; //二叉查找树特性”左大右小“ 31 f[sz]=fx; 32 key[sz]=x; 33 pushup(fx); 34 splay(sz); 35 return ; 36 } 37 } 38 }
注意:插入完之后要把这个刚插入到树上的点给通过splay函数旋转到树根,至于为什么要这样做。大家可以这样想,假设插入之前的树是一颗最优二叉排序树,那么插入一个点之后可能就不最优了,所以我们要旋转这棵树,以保证这棵树还是最优的
5、int rnk(int x) //查询x的排名
这个函数就是插入x这个值,他在平衡树上的位置
代码实现
1 /* 2 有人问: 3 很想知道为什么rnk操作也要splay操作呢?如果del要用的话直接splay(x)是不是就可以了 4 5 原博客答: 6 呃不不不这个貌似不是随便splay以下就可以的 首先find之后的splay就是将找到的这个点转到根, 7 当然你不加这个应该是也可以,只不过这道题加上的话对于这一堆操作来说比较方便,不过一般来说转一转splay的 8 平衡性会好一点(当然也不要转得太多了就tle了...) 但是del之前直接splay(x)要视情况而定,关键在于分清楚 9 “点的编号”和“点的权值”这两个概念。如果你已经知道了该转的点的编号,当然可以直接splay(x),但是如果你只 10 知道应该splay的点的权值,你需要在树里find到这个权值的点的编号,然后再splay 其实最后splay写起来都是非 11 常灵活的,而且有可能一个点带若干个权之类的。对于初学者的建议就是先把一些最简单的情况搞清楚,比如说一 12 个编号一个权的这种,然后慢慢地多做题就能运用得非常熟练了。最好的方法就是多画画树自己转一转,对之后 13 复杂题目的调试也非常有益 14 15 我说: 16 我在洛谷上得模板题上交了一下rnk里面不带splay(now)的,一共12个样例,就对了两个样例。错了一个样例,其他全TLE 17 18 我解释: 19 为什么作者解释可以删去,但是删过之后还错了。因为它的代码中函数之前是相互联系的 20 就比如它调用rnk(x)函数之后就已经认为x为平衡树树根,然后直接对它进行下一步操作(这个假设在del函数里面) 21 22 如果你光删了rnk(x)里面的splay(),你肯定还要改其他地方代码。。。。。。 23 */ 24 int rnk(int x) //查询x的排名 25 { 26 int now=rt,ans=0; 27 while(1) 28 { 29 if(x<key[now]) now=ch[now][0]; 30 else 31 { 32 ans+=sizes[ch[now][0]]; 33 if(x==key[now]) 34 { 35 splay(now); //这个splay是为了后面函数的调用提供前提条件 36 //就比如pre函数的前提条件就是x(x是我们要求谁的前驱,那个谁就是x)已经在平衡树树根 37 return ans+1; 38 } 39 ans+=cnt[now]; //cnt代表now这个位置值(key[now])出现了几次 40 now=ch[now][1]; 41 } 42 } 43 }
6、int kth(int x)
这个是查找树上面第x大的数是多少
1 int kth(int x) 2 { 3 int now=rt; 4 while(1) 5 { 6 if(ch[now][0] && x<=sizes[ch[now][0]]) 7 { 8 //满足这个条件就说明它在左子树上 9 now=ch[now][0]; 10 } 11 else 12 { 13 int temp=sizes[ch[now][0]]+cnt[now]; 14 if(x<=temp) //这个temp是now左子树权值和now节点权值之和 15 return key[now]; //进到这个判断里面说明他不在左子树又不在右子树,那就是now节点了 16 x-=temp; 17 now=ch[now][1]; 18 } 19 } 20 }
7、求根节点的前驱节点和后继结点在树上的位置
int pre()//由于进行splay后,x已经到了根节点的位置 { //求x的前驱其实就是求x的左子树的最右边的一个结点 //为什么呢,因为这是平衡树(带有二叉排序树特点),根据二叉排序树中序遍历结果我么可以知道,一个数的前序就在 //x的左子树的最右边的一个结点 int now=ch[rt][0]; while(ch[now][1]) now=ch[now][1]; return now; } int next() { //求后继是求x的右子树的最左边一个结点 //为什么呢,因为这是平衡树(带有二叉排序树特点),根据二叉排序树中序遍历结果我么可以知道,一个数的前序就在 //x的右子树的最左边一个结点 int now=ch[rt][1]; while(ch[now][0]) now=ch[now][0]; return now; }
8、删除一个值
1 /* 2 删除操作是最后一个稍微有点麻烦的操作。 3 step 1:随便find一下x。目的是:将x旋转到根。 4 step 2:那么现在x就是根了。如果cnt[root]>1,即不只有一个x的话,直接-1返回。 5 step 3:如果root并没有孩子,就说名树上只有一个x而已,直接clear返回。 6 step 4:如果root只有左儿子或者右儿子,那么直接clear root,然后把唯一的儿子当作根就可以了(f赋0,root赋为唯一的儿子) 7 剩下的就是它有两个儿子的情况。 8 step 5:我们找到新根,也就是x的前驱(x左子树最大的一个点),将它旋转到根。然后将原来x的右子树接到新根的 9 右子树上(注意这个操作需要改变父子关系)。这实际上就把x删除了。不要忘了update新根。 10 */ 11 void del(int x) 12 { 13 rnk(x); 14 if(cnt[rt]>1)//如果这个位置权值大于1,那就不用删除这个点 15 { 16 cnt[rt]--; 17 pushup(rt); 18 return; 19 } 20 if(!ch[rt][0] && !ch[rt][1]) //这个就代表平衡树只有一个节点 21 { 22 clears(rt); 23 rt=0; 24 return; 25 } 26 if(!ch[rt][0]) //只有左儿子,树根只有左儿子那就把树根直接删了就行 27 { //然后左儿子这棵子树变成新的平衡树 28 int frt=rt; 29 rt=ch[rt][1]; 30 f[rt]=0; 31 clears(frt); 32 return; 33 } 34 else if(!ch[rt][1]) //只有右儿子,和上面差不多 35 { 36 int frt=rt; 37 rt=ch[rt][0]; 38 f[rt]=0; 39 clears(frt); 40 return; 41 } 42 int frt=rt; 43 int leftbig=pre(); 44 splay(leftbig); //让前驱做新根 45 ch[rt][1]=ch[frt][1]; //这个frt指向的还是之前的根节点 46 /* 47 看着一点的时候就会发现,数在数组里面的位置一直没有改变,平衡树旋转改变的是根节点儿子数组ch[x][]指向的值 48 */ 49 f[ch[frt][1]]=rt; 50 clears(frt); 51 pushup(rt); 52 }
五、例题
题目描述
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
- 插入 xxx 数
- 删除 xxx 数(若有多个相同的数,因只删除一个)
- 查询 xxx 数的排名(排名定义为比当前数小的数的个数 +1+1+1 )
- 查询排名为 xxx 的数
- 求 xxx 的前驱(前驱定义为小于 xxx,且最大的数)
- 求 xxx 的后继(后继定义为大于 xxx,且最小的数)
输入格式
第一行为 nnn,表示操作的个数,下面 nnn 行每行有两个数 opt\text{opt}opt 和 xxx,opt\text{opt}opt 表示操作的序号( 1≤opt≤6 1 \leq \text{opt} \leq 6 1≤opt≤6 )
输出格式
对于操作 3,4,5,63,4,5,63,4,5,6 每行输出一个数,表示对应答案
输入输出样例
10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598
106465
84185
492737
说明/提示
【数据范围】
对于 100%100\%100% 的数据,1≤n≤1051\le n \le 10^51≤n≤105,∣x∣≤107|x| \le 10^7∣x∣≤107
1 /* 2 注意: 3 1、看平衡树之前你要注意,对于1 3 5 3 2这一组数据。sz的值是4,因为sz保存的是节点种类 4 为什么要这样,因为sz涉及到要为几个点开空间 5 6 2、sizes[x]保存的是以x为树根的子树上节点数量,比如x这颗子树所有节点为1,2,1.那么它的sizes[x]=3 7 而且实际上不会有两个1放在树上。而是给1的权值数组cnt[1]加1 8 9 3、对于1 3 5 3 2这一组数据。sz的值是4。那么1对应位置就是1,3对应位置就是2,5对应位置就是3,2对应位置就是4 10 之后他们所对应的位置都不会改变 11 在平衡树旋转的过程中只是每一个点的儿子节点ch[x][0]和ch[x][1]里面保存的值在改变 12 */ 13 #include<stdio.h> 14 #include<string.h> 15 #include<algorithm> 16 #include<iostream> 17 using namespace std; 18 const int maxn=1e5+10; 19 int f[maxn],cnt[maxn],ch[maxn][2],sizes[maxn],key[maxn],sz,rt; 20 /* 21 f[i]:i节点的父节点,cnt[i]每个点出现的次数,ch[i][0/1]:0表示左孩子, 22 1表示右孩子, size[i]表示以i为根节点的子树的节点个数 23 key[i]表示点i代表的数的值;sz为整棵树的节点种类数,rt表示根节点 24 */ 25 void clears(int x) //删除x点信息 26 { 27 f[x]=cnt[x]=ch[x][0]=ch[x][1]=sizes[x]=key[x]=0; 28 } 29 bool get(int x) //判断x是父节点的左孩子还是右孩子 30 { 31 return ch[f[x]][1]==x; //返回1就是右孩子,返回0就是左孩子 32 } 33 void pushup(int x) //重新计算一下x这棵子树的节点数量 34 { 35 if(x) 36 { 37 sizes[x]=cnt[x]; 38 if(ch[x][0]) sizes[x]+=sizes[ch[x][0]]; 39 if(ch[x][1]) sizes[x]+=sizes[ch[x][1]]; 40 } 41 } 42 void rotates(int x) //将x移动到他父亲的位置,并且保证树依旧平衡 43 { 44 int fx=f[x],ffx=f[fx],which=get(x); 45 //x点父亲,要接受x的儿子。而且x与x父亲身份交换 46 ch[fx][which]=ch[x][which^1]; 47 f[ch[fx][which]]=fx; 48 49 ch[x][which^1]=fx; 50 f[fx]=x; 51 52 f[x]=ffx; 53 if(ffx) ch[ffx][ch[ffx][1]==fx]=x; 54 55 pushup(fx); 56 pushup(x); 57 } 58 void splay(int x) //将x移动到数根节点的位置,并且保证树依旧平衡 59 { 60 for(int fx; fx=f[x]; rotates(x)) 61 { 62 if(f[fx]) 63 { 64 rotates((get(x)==get(fx))?fx:x); 65 //如果祖父三代连城一条线,就要从祖父哪里rotate 66 //至于为什么要这样做才能最快……可以去看看Dr.Tarjan的论文 67 } 68 } 69 rt=x; 70 } 71 /* 72 将x这个值插入到平衡树上面 73 如果这个值在树上存在过,那就sz不再加1,更新一下权值即可 74 如果这个值在树上不存在,那就sz加1,再更新一下权值 75 76 sz是书上节点种类数 77 sizes[x]是x这棵子树上有多少节点 78 */ 79 void inserts(int x) 80 { 81 if(rt==0) 82 { 83 sz++; 84 key[sz]=x; 85 rt=sz; 86 cnt[sz]=sizes[sz]=1; 87 f[sz]=ch[sz][0]=ch[sz][1]=0; 88 return; 89 } 90 int now=rt,fx=0; 91 while(1) 92 { 93 if(x==key[now]) 94 { 95 cnt[now]++; 96 pushup(now); 97 pushup(fx); 98 splay(now); //splay的过程会rotates now点的所有祖先节点,这个时候它们所有子树权值也更新了 99 return; 100 } 101 fx=now; 102 now=ch[now][key[now]<x]; 103 if(now==0) 104 { 105 sz++; 106 sizes[sz]=cnt[sz]=1; 107 ch[sz][0]=ch[sz][1]=0; 108 ch[fx][x>key[fx]]=sz; //二叉查找树特性”左大右小“ 109 f[sz]=fx; 110 key[sz]=x; 111 pushup(fx); 112 splay(sz); 113 return ; 114 } 115 } 116 } 117 /* 118 有人问: 119 qwq很想知道为什么find操作也要splay操作呢?如果del要用的话直接splay(x)是不是就可以了 120 121 原博客答: 122 呃不不不这个貌似不是随便splay以下就可以的 首先find之后的splay就是将找到的这个点转到根, 123 当然你不加这个应该是也可以,只不过这道题加上的话对于这一堆操作来说比较方便,不过一般来说转一转splay的 124 平衡性会好一点(当然也不要转得太多了就tle了...) 但是del之前直接splay(x)要视情况而定,关键在于分清楚 125 “点的编号”和“点的权值”这两个概念。如果你已经知道了该转的点的编号,当然可以直接splay(x),但是如果你只 126 知道应该splay的点的权值,你需要在树里find到这个权值的点的编号,然后再splay 其实最后splay写起来都是非 127 常灵活的,而且有可能一个点带若干个权之类的。对于初学者的建议就是先把一些最简单的情况搞清楚,比如说一 128 个编号一个权的这种,然后慢慢地多做题就能运用得非常熟练了。最好的方法就是多画画树自己转一转,对之后 129 复杂题目的调试也非常有益 130 131 我说: 132 我在洛谷上得模板题上交了一下rnk里面不带splay(now)的,一共12个样例,就对了两个样例。错了一个样例,其他全TLE 133 134 我解释: 135 为什么作者解释可以删去,但是删过之后还错了。因为它的代码中函数之前是相互联系的 136 就比如它调用rnk(x)函数之后就已经认为x为平衡树树根,然后直接对它进行下一步操作(这个假设在del函数里面) 137 138 如果你光删了rnk(x)里面的splay(),你肯定还要改其他地方代码。。。。。。 139 */ 140 int rnk(int x) //查询x的排名 141 { 142 int now=rt,ans=0; 143 while(1) 144 { 145 if(x<key[now]) now=ch[now][0]; 146 else 147 { 148 ans+=sizes[ch[now][0]]; 149 if(x==key[now]) 150 { 151 splay(now); //这个splay是为了后面函数的调用提供前提条件 152 //就比如pre函数的前提条件就是x(x是我们要求谁的前驱,那个谁就是x)已经在平衡树树根 153 return ans+1; 154 } 155 ans+=cnt[now]; //cnt代表now这个位置值(key[now])出现了几次 156 now=ch[now][1]; 157 } 158 } 159 } 160 int kth(int x) 161 { 162 int now=rt; 163 while(1) 164 { 165 if(ch[now][0] && x<=sizes[ch[now][0]]) 166 { 167 //满足这个条件就说明它在左子树上 168 now=ch[now][0]; 169 } 170 else 171 { 172 int temp=sizes[ch[now][0]]+cnt[now]; 173 if(x<=temp) //这个temp是now左子树权值和now节点权值之和 174 return key[now]; //进到这个判断里面说明他不在左子树又不在右子树,那就是now节点了 175 x-=temp; 176 now=ch[now][1]; 177 } 178 } 179 } 180 int pre()//由于进行splay后,x已经到了根节点的位置 181 { 182 //求x的前驱其实就是求x的左子树的最右边的一个结点 183 //为什么呢,因为这是平衡树(带有二叉排序树特点),根据二叉排序树中序遍历结果我么可以知道,一个数的前序就在 184 //x的左子树的最右边的一个结点 185 int now=ch[rt][0]; 186 while(ch[now][1]) now=ch[now][1]; 187 return now; 188 } 189 int next() 190 { 191 //求后继是求x的右子树的最左边一个结点 192 //为什么呢,因为这是平衡树(带有二叉排序树特点),根据二叉排序树中序遍历结果我么可以知道,一个数的前序就在 193 //x的右子树的最左边一个结点 194 int now=ch[rt][1]; 195 while(ch[now][0]) now=ch[now][0]; 196 return now; 197 } 198 /* 199 删除操作是最后一个稍微有点麻烦的操作。 200 step 1:随便find一下x。目的是:将x旋转到根。 201 step 2:那么现在x就是根了。如果cnt[root]>1,即不只有一个x的话,直接-1返回。 202 step 3:如果root并没有孩子,就说名树上只有一个x而已,直接clear返回。 203 step 4:如果root只有左儿子或者右儿子,那么直接clear root,然后把唯一的儿子当作根就可以了(f赋0,root赋为唯一的儿子) 204 剩下的就是它有两个儿子的情况。 205 step 5:我们找到新根,也就是x的前驱(x左子树最大的一个点),将它旋转到根。然后将原来x的右子树接到新根的 206 右子树上(注意这个操作需要改变父子关系)。这实际上就把x删除了。不要忘了update新根。 207 */ 208 void del(int x) 209 { 210 rnk(x); 211 if(cnt[rt]>1)//如果这个位置权值大于1,那就不用删除这个点 212 { 213 cnt[rt]--; 214 pushup(rt); 215 return; 216 } 217 if(!ch[rt][0] && !ch[rt][1]) //这个就代表平衡树只有一个节点 218 { 219 clears(rt); 220 rt=0; 221 return; 222 } 223 if(!ch[rt][0]) //只有左儿子,树根只有左儿子那就把树根直接删了就行 224 { //然后左儿子这棵子树变成新的平衡树 225 int frt=rt; 226 rt=ch[rt][1]; 227 f[rt]=0; 228 clears(frt); 229 return; 230 } 231 else if(!ch[rt][1]) //只有右儿子,和上面差不多 232 { 233 int frt=rt; 234 rt=ch[rt][0]; 235 f[rt]=0; 236 clears(frt); 237 return; 238 } 239 int frt=rt; 240 int leftbig=pre(); 241 splay(leftbig); //让前驱做新根 242 ch[rt][1]=ch[frt][1]; //这个frt指向的还是之前的根节点 243 /* 244 看着一点的时候就会发现,数在数组里面的位置一直没有改变,平衡树旋转改变的是根节点儿子数组ch[x][]指向的值 245 */ 246 f[ch[frt][1]]=rt; 247 clears(frt); 248 pushup(rt); 249 } 250 int main() 251 { 252 int n; 253 scanf("%d",&n); 254 for (int i=1; i<=n; i++) 255 { 256 int type,k; 257 scanf("%d%d",&type,&k); 258 if (type==1) inserts(k); 259 if (type==2) del(k); 260 if (type==3) printf("%d\n",rnk(k)); 261 if (type==4) printf("%d\n",kth(k)); 262 if (type==5) 263 { 264 inserts(k); 265 //插入操作中存在splay操作,这样的话插入之后平衡树树根就是k 266 printf("%d\n",key[pre()]); 267 del(k); 268 } 269 if (type==6) 270 { 271 inserts(k); 272 printf("%d\n",key[next()]); 273 del(k); 274 } 275 } 276 printf("%d %d %d %d\n",sz,sizes[1],sizes[2],sizes[3]); 277 return 0; 278 }