目录
CSDN常年出问题,所以注释的图里面可能会移位,请诸位稍微自己调整一下,谢谢!!!
题目描述
【题意描述】
写一种数据结构,来维护一些数,其中需要提供以下操作:
1. 插入x数
2. 删除x数(若有多个相同的数,应只删除一个)
3. 查询x数的排名(若有多个相同的数,应输出最小的排名)
4. 查询排名为x的数
5. 求x的前驱(前驱定义为小于x,且最大的数)
6. 求x的后继(后继定义为大于x,且最小的数)
【输入格式】
第一行为n,表示操作的个数,下面n行每行有两个数opt和x,opt表示操作的序号(1<=opt<=6)
(n < = 100000, 所有数字均在-10^7到10^7内 )
【输出格式】
对于操作3,4,5,6每行输出一个数,表示对应答案
Sample Input
8
1 10
1 20
1 30
3 20
4 2
2 10
5 25
6 -1
Sample Output
2
20
20
20
伸展树的基本概念
伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。它由丹尼尔·斯立特Daniel Sleator 和 罗伯特·恩卓·塔扬Robert Endre Tarjan 在1985年发明的。
在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法, 在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。
它的优势在于不需要记录用于平衡树的冗余信息。
伸展操作
伸展操作Splay(x,S)是在保持伸展树有序性的前提下,通过一系列旋转将伸展树S中的元素x调整至树的根部。在调整的过程中,要分以下三种情况分别处理:
情况一:节点x的父节点y是根节点。这时,如果x是y的左孩子,我们进行一次Zig(右旋)操作;如果x是y的右孩子,则我们进行一次Zag(左旋)操作。经过旋转,x成为二叉查找树S的根节点,调整结束。即:如果当前结点父结点即为根结点,那么我们只需要进行一次简单旋转即可完成任务,我们称这种旋转为单旋转。
情况二:节点x的父节点y不是根节点,y的父节点为z,且x与y同时是各自父节点的左孩子或者同时是各自父节点的右孩子。这时,我们进行一次Zig-Zig操作或者Zag-Zag操作。即:设当前结点为X,X的父结点为Y,Y的父结点为Z,如果Y和X同为其父亲的左孩子或右孩子,那么我们先旋转Y,再旋转X。我们称这种旋转为一字形旋转。
情况三:节点x的父节点y不是根节点,y的父节点为z,x与y中一个是其父节点的左孩子而另一个是其父节点的右孩子。这时,我们进行一次Zig-Zag操作或者Zag-Zig操作。即:这时我们连续旋转两次X。我们称这种旋转为之字形旋转。
如图4所示,执行Splay(1,S),我们将元素1调整到了伸展树S的根部。再执行Splay(2,S),如图5所示,我们从直观上可以看出在经过调整后,伸展树比原来“平衡”了许多。而伸展操作的过程并不复杂,只需要根据情况进行旋转就可以了,而三种旋转都是由基本得左旋和右旋组成的,实现较为简单。
Find(x,S):判断元素x是否在伸展树S表示的有序集中。
首先,与在二叉查找树中的查找操作一样,在伸展树中查找元素x。如果x在树中,则再执行Splay(x,S)调整伸展树。
- 加入操作
Insert(x,S):将元素x插入伸展树S表示的有序集中。
首先,也与处理普通的二叉查找树一样,将x插入到伸展树S中的相应位置上,再执行Splay(x,S)。
- 删除操作
Delete(x,S):将元素x从伸展树S所表示的有序集中删除。
首先,用在二叉查找树中查找元素的方法找到x的位置。如果x没有孩子或只有一个孩子,那么直接将x删去,并通过Splay操作,将x节点的父节点调整
到伸展树的根节点处。否则,则向下查找x的后继y,用y替代x的位置,最后执行Splay(y,S),将y调整为伸展树的根。
- 合并操作
join(S1,S2):将两个伸展树S1与S2合并成为一个伸展树。其中S1的所有元素都小于S2的所有元素。首先,我们找到伸展树S1中最大的一个元素x,再通过Splay(x,S1)将x调整到伸展树S1的根。然后再将S2作为x节点的右子树。这样,就得到了新的伸展树S。
- 启发式合并
当S1和S2元素大小任意时,将规模小的伸展树上的节点一一插入规模大的伸展树,总时间复杂度O(Nlg^2N)。
- 划分操作
Split(x,S):以x为界,将伸展树S分离为两棵伸展树S1和S2,其中S1中所有元素都小于x,S2中的所有元素都大于x。首先执行Find(x,S),将元素x调整为伸展树的根节点,则x的左子树就是S1,而右子树为S2。
- 其他操作
除了上面介绍的五种基本操作,伸展树还支持求最大值、求最小值、求前驱、求后继等多种操作,这些基本操作也都是建立在伸展操作的基础上的。
通常来说,每进行一种操作后都会进行一次Splay操作,这样可以保证每次操作的平摊时间复杂度是O(logn)。
优势
可靠的性能——它的平均效率不输于其他平衡树。
存储所需的内存少——伸展树无需记录额外的什么值来维护树的信息,相对于其他平衡树,内存占用要小。
由于Splay Tree仅仅是不断调整,并没有引入额外的标记,因而树结构与标准红黑树没有任何不同,从空间角度来看,它比Treap、SBT、AVL要高效得多。因为结构不变,因此只要是通过左旋和右旋进行的操作对Splay Tree性质都没有丝毫影响,因而它也提供了BST中最丰富的功能,包括快速的拆分和合并,并且实现极为便捷。这一点是其它结构较难实现的。其时间效率也相当稳定,和Treap基本相当,常数较高。
缺点
伸展树最显著的缺点是它有可能会变成一条链。这种情况可能发生在以非降顺序访问n个元素之后。然而均摊的最坏情况是对数级的——O(logn)。
【来源:百度百科】
定义结构体
struct trnode
{
int d,n,c,f,son[2];
/*
d为值,f为父亲的编号,
c为控制的节点个数,以他为根节点的那棵树的所有的节点数
n为同值的节点个数,把多个同样的值浓缩成一个结构体
(这一步是为了省空间,比如说3个为100的数,可以说这个d为100,n为3)
son[0]为左孩子,son[1]为右孩子
*/
}tr[110000]; int len;//len表示用到了第几个节点
结构体的每一步定义都要搞清楚,这个关乎到了代码的所有意义。这里有一个很重要的东西,就是结构体中的n,他把相同的数字都保存在了一起,大大减少了内存,不占用空间。
更新控制的节点数的函数
void update(int x)//更新编号为x的节点所控制的节点数
{
int lc=tr[x].son[0];//左孩子的编号
int rc=tr[x].son[1];//右孩子的编号
tr[x].c=tr[lc].c+tr[rc].c+tr[x].n;
//x总共的节点数=左孩子的节点数+右孩子的节点数+同值的数
}
更新节点数的这个函数在整个代码当中起到了一个很重要的作用,因为几乎每一步的操作都离不开这个update函数,所以这个函数必须要记住,其实也很简单的,就是 左孩子+右孩子+重复的 就是更新后的节点数
增加一个点的函数
void add(int d,int f)//添加值为d的点,认f为父亲,同时,f也认他为孩子
{
len++;//增加一个节点数
tr[len].d=d; tr[len].n=1; tr[len].c=1;
/*
这一步是关于加入的值的
加入的这个值的值就是定义的d——值,
然后只有他自己一个,所以n=1
同时他控制的节点数也只有他自己一个
*/
tr[len].f=f; if(d<tr[f].d) tr[f].son[0]=len; else tr[f].son[1]=len;
/*
这一步是关于他认的父亲的操作
这个节点的父亲就是定义的f-父亲
我们默认比父亲节点的值小的为左孩子,比父亲节点的值大的为右孩子
所以说如果加入的这个节点的值比父亲节点的值大,就为左孩子,否则右孩子
状态:左小右大
这里可能会考虑到如果这个节点原本就有孩子怎么办?
这个的话我也解释不清楚,
因为我们是增加进去的
所以我们只要找到合适的位置插入就好了
比如说
8
/ \
3 25
/ \ / \
2 4 20 30
假如我们要插入10的话,10离4最近,所以应该插入到4的下面,
但是这一整棵子树的每一个节点都要比根节点小
所以这样的话,这个10就只能认20为父亲,也是最接近的答案了
*/
tr[len].son[0]=tr[len].son[1]=0;
/*
定义最开始的加入的这个点,是一个叶子节点
既没有左孩子,也没有右孩子
*/
}
这个函数主要实在插入的时候用的,用这个函数使得插入的时候少了一大串东西,而且也可以直接统计好更新之后的节点数。
rotate旋转的函数(重要)
void rotate(int x,int w)
/*
这是整个代码当中的一个关键点
首先我们定义了x是我们要选择旋转的节点
w有两个形式,一个是0,一个是1
0表示左转,1表示右转
(x,0)表示x这个点左转
(x,1)表示x这个点右转
注意,我们要转的可能是x,但是变化的不止x,和x有关系的也有变动
*/
{
int y=tr[x].f; int z=tr[y].f;//x在旋转之前,要确定x的父亲y和爷爷z
//下来建立关系
int r,R;//r表示儿辈,R表示父辈 (ren,Ren)
//有四个角色:我x,我的儿子,我的父亲,我的爷爷
/*
接下来的就是在旋转的时候发生的关系
x为左孩子才可以右转,为右孩子才可以左转
在这里可能要有图才讲得清楚
y x
/ \ / \
c x y b
/ \ / \
a b c a
左转前 左转后
y x
/ \ / \
x c a y
/ \ / \
a b b c
右转前 右转后
稍微解释一下:
我们之前定义过左孩子的值比父亲节点的值要小,
右孩子比父亲节点的值要大,是吧?
那么这个时候我们就可以看到,
x左转之后一定跟y换了位置,这个是必然的,
然后,y是比x小的(x是y的右孩子),
所以x替代了y的位置之后,y就成为了x的左孩子(比x小),
然后c是y的右孩子,比y小,旋转之后跟着y成为y的右孩子即可。
然后我们知道b是右孩子,比x要大,所以依旧成为x的右孩子即可,
那么剩下a,首先我们知道a是比x小的,但是总体来看是比y要大的,
因为x比y大,所以a也比y大,然而x的左右孩子都有了,
y还有右孩子的空位,那么a又比y大,所以a在y的右孩子的位置刚刚好。
*/
//更换过程是从下到上的,而且是儿子先认父亲,父亲再认儿子
r=tr[x].son[w]; R=y;//x的儿子->准备当新儿子
/*
左边旋转的话,x的左孩子就变成别人的孩子;
右边旋转的话,x的右孩子就变成别人的孩子。
然后这个孩子的新父亲就是x的父亲y
*/
tr[R].son[1-w]=r;
/*
左边旋转的话,x的左孩子就变成y的右孩子;
x的右孩子仍然是x的右孩子
右边旋转的话,x的右孩子就变成y的左孩子。
x的左孩子仍然是x的左孩子
*/
if(r!=0) tr[r].f=R;
//如果这个x的孩子节点不是0的话,这个孩子节点的父亲就是前面认过的y节点
r=x; R=z;//x->准备当新儿子
if(tr[R].son[0]==y) tr[R].son[0]=r;
/*
首先我们知道,x左转之后就变成了z的孩子节点,
因为y原来是z的孩子,现在x代替了y的位置
所以z就是x的父亲节点
*/
//如果y所在的是z的左孩子,那么x的位置就是z的左孩子
else tr[R].son[1]=r;
//否则就为z的右孩子,其实就是顶替的y的位置,其他不变
tr[r].f=R;
//x的父亲节点变为z
r=y; R=x;//x的父亲y->准备当新儿子
//y这个时候变成了孩子节点,他的父亲节点是x
tr[R].son[w]=r;
/*
左转的话,y就是x的左孩子
y的左孩子仍然是y的左孩子
右转的话,y就是x的右孩子
y的右孩子仍然是y的右孩子
*/
tr[r].f=R;
//x就是y的父亲节点
update(y);//先更新处于下层的点y,因为我们是先换下面的
update(x);//再更新上层的x,后换上面的
}
void splay(int x,int rt)
//该函数的功能是:经过旋转之后,使x成为rt的孩子(左右都可以)
//最关键的操作
{
while(tr[x].f!=rt)//如果rt是x的父亲,则什么都不用做,否则x就要不断向上旋转
{
int f=tr[x].f; int ff=tr[f].f;//准备x的父亲和爷爷
if(ff==rt)//如果x的爷爷是rt,那么x只需要旋转一次(相当于跳一层)
{
if(tr[f].son[0]==x) rotate(x,1); else rotate(x,0);
//如果x是f的左孩子的话,就右旋,也只能右旋
//如果x是f的右孩子的话,就左旋,也只能左旋
}
else//rt在ff的上面
{
if(tr[ff].son[0]==f && tr[f].son[0]==x) {rotate(f,1); rotate(x,1);}
/*
ff 第一次右转 f 第二次右转 x
/ f变成爷爷 / \ x变成爷爷 \
f x ff f
/ \
x ff
*/
else if(tr[ff].son[1]==f && tr[f].son[1]==x) {rotate(f,0); rotate(x,0);}
/*
ff 第一次左转 f 第二次右转 x
\ f变成爷爷 / \ x变成爷爷 /
f ff x f
\ /
x ff
*/
else if(tr[ff].son[0]==f && tr[f].son[1]==x) {rotate(x,0); rotate(x,1);}
/*
ff 第一次左转 ff 第二次右转 x
/ x变成父亲 / x变成爷爷 / \
f x f ff
\ /
x f
这一次的旋转比较特殊,如果f右转的话就会出现这样的情况
f
\
ff
/
x
转了跟没转一样,所以只能转x,不能动y
*/
else if(tr[ff].son[1]==f && tr[f].son[0]==x) {rotate(x,1); rotate(x,0);}
/*
ff 第一次右转 ff 第二次右转 x
\ x变成父亲 \ x变成爷爷 / \
f x ff f
/ \
x f
跟上面一样也是只能转x,不能转y
*/
}
}
if(rt==0) root=x;
/*
每一棵树都要有一个最终极的根节点,如果x不能成为rt的孩子节点的话
说明x就是最终级的根节点
*/
}
这一步是非常重要的,因为伸展树最大的作用就是把访问过的移到根节点的位置,那么这样来说,就可以通过rotate这个函数来实现,所以代码要记住,建议不要死背,按照每一种方法的图来背是最有效的。注意一下,调整的只是我们选中以x为中心的父亲和爷爷,以及孩子,出来这四方,在x孩子的以下是不会受到改变的,他们的孩子节点跟着父亲走就可以了。
rotate的几种情况
也就是旋转的情况,这个是挺重要的一个函数
当p为根节点时,进行zip step操作。
当x是p的左孩子时,对x右旋;
当x是p的右孩子时,对x左旋。
当p不是根节点,且x和p同为左孩子或右孩子时进行Zig-Zig操作。
当x和p同为左孩子时,依次将p和x右旋;
当x和p同为右孩子时,依次将p和x左旋。
当p不是根节点,且x和p不同为左孩子或右孩子时,进行Zig-Zag操作。
当p为左孩子,x为右孩子时,将x左旋后再右旋。
当p为右孩子,x为左孩子时,将x右旋后再左旋。
这里只有一个图,但是规律就是这样的。
找某个值的编号的函数
int findip(int d)
//找值为d的节点的地址,补充:如果不存在d,就找到有可能是接近d的(或大或小)
{
int x=root;//root表示的是根节点,从根节点出发,x是我们找到的合适的值
//接下来就要判断往左边走还是往右边走
while(tr[x].d!=d)//如果根节点的值等于要找的d的值,就不用找了
{
if(d<tr[x].d)//如果d小于根节点值
{
if(tr[x].son[0]==0) break;
/*
那就往左边找,因为左孩子小于根节点
如果没有左孩子,就退出,因为找不到合适的,只能去较大的右边找
*/
else x=tr[x].son[0];
/*
如果有左孩子,那么x就为根节点的左孩子,因为最开始的是最接近的
再往下的也不及根节点自己的孩子和根节点最近
*/
}
else//if(tr[x].d<d) //如果d大于根节点的值
{
if(tr[x].son[1]==0) break;
/*
那就往右边找,因为右孩子大于根节点
如果没有右孩子,就退出,因为找不到合适的,只能去较小的地方找
*/
else x=tr[x].son[1];//如果有右孩子,那么x就为根节点的右孩子
}
}
return x;
/*
返回x的编号
伸展树就是保存了我们访问过的所有数据,使得最快的找到我们要找的
而这一步其实就解决了我们题目中的第5步和第6步,找前驱和找后继
*/
}
显然这一步是为了后面的寻找的,这在寻找中会起到很重要的作用,知道一个值是不够的,要知道这个值的编号才能进行整一棵伸展树的调整。
插入的函数
void ins(int d)
//插入数值为d的一个节点
//这可以说是伸展树的一大亮点,插入和删除,是之前所有树形结构所做不到的
{
if(root==0) {add(d,0); root=len; return ;}
/*
root为0,说明没有根节点,表示这是一棵空树
既然没有,那就增加一个点,父亲为0,也就是当前的root
root不能等于1,因为len是全局变量
但是如果原来有一棵树但是被全部删掉之后,len是没有清除数据的
所以这个时候我们就要接着len往下建树
*/
int x=findip(d);//先看看能不能找到d
if(tr[x].d==d)//如果在这棵树中找到了d,那就很简单了
//比如说,要找7,但是编号为3的节点的值就为7,
//那就直接增加编号3的n(相同值的个数)就可以了
{
tr[x].n++;//直接把x相同的再增加一个,就算插入了
update(x);//更新x控制的人数,就是增加一个人
splay(x,0);
/*
把x提高到根节点
因为增加了一个,但是这个数据要汇报给根节点
所以就是要让x为根节点
提高的过程中不断旋转,不断更新孩子与父亲的关系
所以我们找到的这个7的节点在跳的过程中
会不断告诉别人7控制了多少个节点
这样就不会混乱,也不会影响后面
*/
}
else//如果找不到
{
add(d,x);//增加一个值为d的点
update(x);//更新x
splay(len,0);
/*
新的这个点要拉上去,作为根节点
成为根节点就是伸展树最神奇的地方
因为伸展树把访问过的点都提拔到了根节点
因为他觉得之后还会访问,而且也确实如此,所以才能够更快的实现寻找
*/
}
}
插入要判断,判断是否要真正意义上的插入,还是只是增加一个相同的值。每一次的插入,都要更新节点数,所以update是一个众观全局的函数。
删除的函数
void del(int d)//删除数值为d的一个节点
{
int x=findip(d); splay(x,0);
/*
找人,并且让找到的这个人旋转到根节点
这就是我们伸展树的优点,把访问过的旋转到根节点
*/
if(tr[x].n>1) {tr[x].n--; update(x); return ;}
//如果重复度大于一,减少一个然后再更新一下就好了
if(tr[x].son[0]==0 && tr[x].son[1]==0) {root=0; len=0;}
/*
我们已经把这个点提到了根节点的话
而如果我们要删的这个点既没有左孩子也没有右孩子
那就说明全世界只有他一个点,那删掉之后就什么都为0
根节点为0,节点数也为0
*/
else if(tr[x].son[0]==0 && tr[x].son[1]!=0) {root=tr[x].son[1]; tr[root].f=0;}
/*
如果这个点没有左孩子但是有右孩子的话
右孩子成为根节点,并且这个右孩子没有父亲节点
*/
else if(tr[x].son[0]!=0 && tr[x].son[1]==0) {root=tr[x].son[0]; tr[root].f=0;}
/*
如果这个点没有右孩子但是有左孩子的话
左孩子成为根节点,并且这个左孩子没有父亲节点
*/
else//if(tr[x].son[0]!= 0 && tr[x].son[1]!=0) //既有左孩子,也有右孩子
{
int p=tr[x].son[0];//定义p为x的左孩子
while(tr[p].son[1]!=0)//如果p有右孩子的话
{
p=tr[p].son[1];//那么p就更新为自己的右孩子
splay(p,x);//把右孩子旋转到x的孩子节点,也就是转到p的位置
/*
一直往右边跳,因为右边是比根节点的值要大的,所以往右边
*/
}//循环到没有有孩子的时候,这个值就是最大的
//又因为没有这个p点没有了右孩子,所以就可以收x的右孩子成为自己的右孩子
int r=tr[x].son[1];//小人为x节点的右孩子
int R=p;//大人为p,也就是x节点的右孩子成为p节点的右孩子
/*
4 经过第一次 4
/ \ 转动了3 /
2 6 而且4的右孩子也 3
/ \ / \ 成为了3的右孩子 / \
1 3 5 7 2 6
/ / \
1 5 7
这个时候就成为了我们要的,只有一个孩子节点
*/
tr[R].son[1]=r;
tr[r].f=R;//定下结论,我们现在只有一个子树了
root=R; tr[root].f=0;
/*
这个时候新的root就等于我们找到的最大的值
目的就是把每一个访问过的都记录下来
*/
update(R);//更新这一整棵树就好了
}
}
删除在意义上和插入有几分相似,大概也是判断直接删除重复的值还是删除单个的值,但是这里要比插入复杂一点,因为我们删掉的那个值之后,可能会导致整棵伸展树的倒塌,所以在背代码的时候要考虑清楚这些细节的东西。
找排名的函数
int findpaiming(int d)//找排名
{
int x=findip(d); splay(x,0);
//先找到这个值,然后让他成为根节点
return tr[tr[x].son[0]].c+1;
//左孩子的控制人数再+1就是自己的排名
/*
100 第一次 100 第二次 23
/ \ 移动23 / \ 旋转23 \
55 120 23 120 100
/ \ \ \ / \
23 144 55 144 55 144
\ / /
34 34 34
/ / /
30 30 30
比如说我们要找23的排名
排名为1
这样就对了因为我们要找的是从小到大的排名
所以23最小就为1
*/
}
找排名是极其简单的一个函数,排名是值从小到大排序,只要搞清楚为什么是左孩子控制的人数+1就是自己的排名就可以了。
找某个排名对应的值的函数
int findzhi(int k)//找排名为k的值
{
int x=root;//定义x为根节点,从根节点开始找
while(1)//
{
int lc=tr[x].son[0]; int rc=tr[x].son[1];//左边和右边
if(k<=tr[lc].c) x=lc;
/*
如果k的这个排名比左边控制的人数还要少
就去左边找
这个时候就把左边设置为要继续往下找的一个终点位置
其实就是伸展树的好处,记录访问过的
*/
else if(k>tr[lc].c+tr[x].n)
/*
如果这个排名比(左边控制的人数+根节点重复的节点数)都要大
就去右边找
*/
{
k-=tr[lc].c+tr[x].n;
/*
注意:光继续在右边找还不够
要减去(左边的控制人数+根节点重复的节点数)
比如说:我们要找17
3
/ \
10 ?
这个时候右边控制的人数+根节点重复的节点数=13
比17要小,说明我们要去右边找
去右边找的就是 17-13=4,找排名为4的节点的值
*/
x=rc;//去右边继续找
}
else break;//否则要找的排名就在根节点中间
}
splay(x,0);//把找到的合适的移到根节点
return tr[x].d;//把我们找到的这个节点的值返回给函数findzhi
}
这个要稍微复杂一点,因为你要判断当前这个排名是在左孩子还是右孩子,其他的就很简单了。
找前驱的函数
int findqianqu(int d)//找前驱
{
int x=findip(d); splay(x,0);//找到d的编号,使他成为根节点
if(d<=tr[x].d && tr[x].son[0]!=0)
//如果是if( d<tr[x].d && tr[x].son[0]!=0 )则找到的是:小于等于d的前驱
//如果这个值比根节点的值要小,并且有左孩子的话
{
x=tr[x].son[0];//把这个点的左孩子移到根节点
while(tr[x].son[1]!=0) x=tr[x].son[1];
/*
找完之后一直往右边跳(也就是寻找),找右边的最大值
前驱是比要找的值小的最大值
所以只要是左孩子的话就一定比d要小
那么左孩子的右孩子就是比d小而且是比左孩子要大的
这样就可以找到最大的值
*/
}
if(tr[x].d>=d) x=0;//如果是if(tr[x].d>d)则找到的是:小于等于d的前驱
/*
如果我们找到的这个值大于等于d的话
说明以d为根节点的这棵数没有左孩子
那就说明没有合适的前驱
就只能为0
*/
return x;//返回x的值
}
找前驱,一点都不难,唯一要注意的就是要搞清楚一个节点的左孩子的值比自己要小,右孩子的值比自己要大,还有一个就是要判断没有前驱的情况。大概就是这三种。
找后继的函数
int finddouji(int d)//找后继
{
int x=findip(d); splay(x,0);//找到d的编号,使他成为根节点
if(tr[x].d<=d && tr[x].son[1]!=0)
//如果这个值比根节点的值要大,并且有右孩子的话
{
x=tr[x].son[1];//把这个点的右孩子移到根节点
while(tr[x].son[0]!=0) x=tr[x].son[0];
/*
找完之后一直往左边跳(也就是寻找),找左边的最小值
后继是比要找的值大的最小值
所以只要是右孩子的话就一定比d要大
那么右孩子的左孩子就是比d大而且是比右孩子要小的
这样就可以找到最小的值
*/
}
if(tr[x].d<=d) x=0;
/*
如果我们找到的这个值小于等于d的话
说明以d为根节点的这棵数没有右孩子
那就说明没有合适的后继
就只能为0
*/
return x;//返回x的值
}
找后继,跟找前驱一样的道理。
智障的主函数
int main()
{
int n; n=read();
root=0; len=0;//初始化没有根节点,也没有节点
for(int i=1;i<=n;i++)
{
int cz,x; cz=read(); x=read();
if(cz==1) ins(x);//插入
else if(cz==2) del(x);//删除
else if(cz==3) printf("%d\n",findpaiming(x));//找排名
else if(cz==4) printf("%d\n",findzhi(x));//找排名值
else if(cz==5) printf("%d\n",tr[findqianqu(x)].d);//找前驱
else if(cz==6) printf("%d\n",tr[finddouji(x)].d);//找后继
}
return 0;
}
不解释
最后,我的思路相对来讲会没有那么完善,但是详解都在代码里面了,把函数的作用搞清楚就可以了。
完整代码
/*
要求:画图理解并且默打
记住:是理解性默打(不然背死你)
*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
inline int read()
{
char c=getchar();
int x=0,f=1;
while(c<48 || c>57)
{
if(c=='-') f=-1;
c=getchar();
}
while(c>=48 && c<=57)
{
x=x*10+c-48;
c=getchar();
}
return x*f;
}
int root;//存储根节点
struct trnode
{
int d,n,c,f,son[2];
/*
d为值,f为父亲的编号,
c为控制的节点个数,以他为根节点的那棵树的所有的节点数
n为同值的节点个数,把多个同样的值浓缩成一个结构体
(这一步是为了省空间,比如说3个为100的数,可以说这个d为100,n为3)
son[0]为左孩子,son[1]为右孩子
*/
}tr[110000]; int len;//len表示用到了第几个节点
void update(int x)//更新编号为x的节点所控制的节点数
{
int lc=tr[x].son[0];//左孩子的编号
int rc=tr[x].son[1];//右孩子的编号
tr[x].c=tr[lc].c+tr[rc].c+tr[x].n;
//x总共的节点数=左孩子的节点数+右孩子的节点数+同值的数
}
void add(int d,int f)//添加值为d的点,认f为父亲,同时,f也认他为孩子
{
len++;//增加一个节点数
tr[len].d=d; tr[len].n=1; tr[len].c=1;
/*
这一步是关于加入的值的
加入的这个值的值就是定义的d——值,
然后只有他自己一个,所以n=1
同时他控制的节点数也只有他自己一个
*/
tr[len].f=f; if(d<tr[f].d) tr[f].son[0]=len; else tr[f].son[1]=len;
/*
这一步是关于他认的父亲的操作
这个节点的父亲就是定义的f-父亲
我们默认比父亲节点的值小的为左孩子,比父亲节点的值大的为右孩子
所以说如果加入的这个节点的值比父亲节点的值大,就为左孩子,否则右孩子
状态:左小右大
这里可能会考虑到如果这个节点原本就有孩子怎么办?
这个的话我也解释不清楚,
因为我们是增加进去的
所以我们只要找到合适的位置插入就好了
比如说
8
/ \
3 25
/ \ / \
2 4 20 30
假如我们要插入10的话,10离4最近,所以应该插入到4的下面,
但是这一整棵子树的每一个节点都要比根节点小
所以这样的话,这个10就只能认20为父亲,也是最接近的答案了
*/
tr[len].son[0]=tr[len].son[1]=0;
/*
定义最开始的加入的这个点,是一个叶子节点
既没有左孩子,也没有右孩子
*/
}
void rotate(int x,int w)
/*
这是整个代码当中的一个关键点
首先我们定义了x是我们要选择旋转的节点
w有两个形式,一个是0,一个是1
0表示左转,1表示右转
(x,0)表示x这个点左转
(x,1)表示x这个点右转
注意,我们要转的可能是x,但是变化的不止x,和x有关系的也有变动
*/
{
int y=tr[x].f; int z=tr[y].f;//x在旋转之前,要确定x的父亲y和爷爷z
//下来建立关系
int r,R;//r表示儿辈,R表示父辈 (ren,Ren)
//有四个角色:我x,我的儿子,我的父亲,我的爷爷
/*
接下来的就是在旋转的时候发生的关系
x为左孩子才可以右转,为右孩子才可以左转
在这里可能要有图才讲得清楚
y x
/ \ / \
c x y b
/ \ / \
a b c a
左转前 左转后
y x
/ \ / \
x c a y
/ \ / \
a b b c
右转前 右转后
稍微解释一下:
我们之前定义过左孩子的值比父亲节点的值要小,
右孩子比父亲节点的值要大,是吧?
那么这个时候我们就可以看到,
x左转之后一定跟y换了位置,这个是必然的,
然后,y是比x小的(x是y的右孩子),
所以x替代了y的位置之后,y就成为了x的左孩子(比x小),
然后c是y的右孩子,比y小,旋转之后跟着y成为y的右孩子即可。
然后我们知道b是右孩子,比x要大,所以依旧成为x的右孩子即可,
那么剩下a,首先我们知道a是比x小的,但是总体来看是比y要大的,
因为x比y大,所以a也比y大,然而x的左右孩子都有了,
y还有右孩子的空位,那么a又比y大,所以a在y的右孩子的位置刚刚好。
*/
//更换过程是从下到上的,而且是儿子先认父亲,父亲再认儿子
r=tr[x].son[w]; R=y;//x的儿子->准备当新儿子
/*
左边旋转的话,x的左孩子就变成别人的孩子;
右边旋转的话,x的右孩子就变成别人的孩子。
然后这个孩子的新父亲就是x的父亲y
*/
tr[R].son[1-w]=r;
/*
左边旋转的话,x的左孩子就变成y的右孩子;
x的右孩子仍然是x的右孩子
右边旋转的话,x的右孩子就变成y的左孩子。
x的左孩子仍然是x的左孩子
*/
if(r!=0) tr[r].f=R;
//如果这个x的孩子节点不是0的话,这个孩子节点的父亲就是前面认过的y节点
r=x; R=z;//x->准备当新儿子
if(tr[R].son[0]==y) tr[R].son[0]=r;
/*
首先我们知道,x左转之后就变成了z的孩子节点,
因为y原来是z的孩子,现在x代替了y的位置
所以z就是x的父亲节点
*/
//如果y所在的是z的左孩子,那么x的位置就是z的左孩子
else tr[R].son[1]=r;
//否则就为z的右孩子,其实就是顶替的y的位置,其他不变
tr[r].f=R;
//x的父亲节点变为z
r=y; R=x;//x的父亲y->准备当新儿子
//y这个时候变成了孩子节点,他的父亲节点是x
tr[R].son[w]=r;
/*
左转的话,y就是x的左孩子
y的左孩子仍然是y的左孩子
右转的话,y就是x的右孩子
y的右孩子仍然是y的右孩子
*/
tr[r].f=R;
//x就是y的父亲节点
update(y);//先更新处于下层的点y,因为我们是先换下面的
update(x);//再更新上层的x,后换上面的
}
void splay(int x,int rt)
//该函数的功能是:经过旋转之后,使x成为rt的孩子(左右都可以)
//最关键的操作
{
while(tr[x].f!=rt)//如果rt是x的父亲,则什么都不用做,否则x就要不断向上旋转
{
int f=tr[x].f; int ff=tr[f].f;//准备x的父亲和爷爷
if(ff==rt)//如果x的爷爷是rt,那么x只需要旋转一次(相当于跳一层)
{
if(tr[f].son[0]==x) rotate(x,1); else rotate(x,0);
//如果x是f的左孩子的话,就右旋,也只能右旋
//如果x是f的右孩子的话,就左旋,也只能左旋
}
else//rt在ff的上面
{
if(tr[ff].son[0]==f && tr[f].son[0]==x) {rotate(f,1); rotate(x,1);}
/*
ff 第一次右转 f 第二次右转 x
/ f变成爷爷 / \ x变成爷爷 \
f x ff f
/ \
x ff
*/
else if(tr[ff].son[1]==f && tr[f].son[1]==x) {rotate(f,0); rotate(x,0);}
/*
ff 第一次左转 f 第二次右转 x
\ f变成爷爷 / \ x变成爷爷 /
f ff x f
\ /
x ff
*/
else if(tr[ff].son[0]==f && tr[f].son[1]==x) {rotate(x,0); rotate(x,1);}
/*
ff 第一次左转 ff 第二次右转 x
/ x变成父亲 / x变成爷爷 / \
f x f ff
\ /
x f
这一次的旋转比较特殊,如果f右转的话就会出现这样的情况
f
\
ff
/
x
转了跟没转一样,所以只能转x,不能动y
*/
else if(tr[ff].son[1]==f && tr[f].son[0]==x) {rotate(x,1); rotate(x,0);}
/*
ff 第一次右转 ff 第二次右转 x
\ x变成父亲 \ x变成爷爷 / \
f x ff f
/ \
x f
跟上面一样也是只能转x,不能转y
*/
}
}
if(rt==0) root=x;
/*
每一棵树都要有一个最终极的根节点,如果x不能成为rt的孩子节点的话
说明x就是最终级的根节点
*/
}
int findip(int d)
//找值为d的节点的地址,补充:如果不存在d,就找到有可能是接近d的(或大或小)
{
int x=root;//root表示的是根节点,从根节点出发,x是我们找到的合适的值
//接下来就要判断往左边走还是往右边走
while(tr[x].d!=d)//如果根节点的值等于要找的d的值,就不用找了
{
if(d<tr[x].d)//如果d小于根节点值
{
if(tr[x].son[0]==0) break;
/*
那就往左边找,因为左孩子小于根节点
如果没有左孩子,就退出,因为找不到合适的,只能去较大的右边找
*/
else x=tr[x].son[0];
/*
如果有左孩子,那么x就为根节点的左孩子,因为最开始的是最接近的
再往下的也不及根节点自己的孩子和根节点最近
*/
}
else//if(tr[x].d<d) //如果d大于根节点的值
{
if(tr[x].son[1]==0) break;
/*
那就往右边找,因为右孩子大于根节点
如果没有右孩子,就退出,因为找不到合适的,只能去较小的地方找
*/
else x=tr[x].son[1];//如果有右孩子,那么x就为根节点的右孩子
}
}
return x;
/*
返回x的编号
伸展树就是保存了我们访问过的所有数据,使得最快的找到我们要找的
而这一步其实就解决了我们题目中的第5步和第6步,找前驱和找后继
*/
}
void ins(int d)
//插入数值为d的一个节点
//这可以说是伸展树的一大亮点,插入和删除,是之前所有树形结构所做不到的
{
if(root==0) {add(d,0); root=len; return ;}
/*
root为0,说明没有根节点,表示这是一棵空树
既然没有,那就增加一个点,父亲为0,也就是当前的root
root不能等于1,因为len是全局变量
但是如果原来有一棵树但是被全部删掉之后,len是没有清除数据的
所以这个时候我们就要接着len往下建树
*/
int x=findip(d);//先看看能不能找到d
if(tr[x].d==d)//如果在这棵树中找到了d,那就很简单了
//比如说,要找7,但是编号为3的节点的值就为7,
//那就直接增加编号3的n(相同值的个数)就可以了
{
tr[x].n++;//直接把x相同的再增加一个,就算插入了
update(x);//更新x控制的人数,就是增加一个人
splay(x,0);
/*
把x提高到根节点
因为增加了一个,但是这个数据要汇报给根节点
所以就是要让x为根节点
提高的过程中不断旋转,不断更新孩子与父亲的关系
所以我们找到的这个7的节点在跳的过程中
会不断告诉别人7控制了多少个节点
这样就不会混乱,也不会影响后面
*/
}
else//如果找不到
{
add(d,x);//增加一个值为d的点
update(x);//更新x
splay(len,0);
/*
新的这个点要拉上去,作为根节点
成为根节点就是伸展树最神奇的地方
因为伸展树把访问过的点都提拔到了根节点
因为他觉得之后还会访问,而且也确实如此,所以才能够更快的实现寻找
*/
}
}
void del(int d)//删除数值为d的一个节点
{
int x=findip(d); splay(x,0);
/*
找人,并且让找到的这个人旋转到根节点
这就是我们伸展树的优点,把访问过的旋转到根节点
*/
if(tr[x].n>1) {tr[x].n--; update(x); return ;}
//如果重复度大于一,减少一个然后再更新一下就好了
if(tr[x].son[0]==0 && tr[x].son[1]==0) {root=0; len=0;}
/*
我们已经把这个点提到了根节点的话
而如果我们要删的这个点既没有左孩子也没有右孩子
那就说明全世界只有他一个点,那删掉之后就什么都为0
根节点为0,节点数也为0
*/
else if(tr[x].son[0]==0 && tr[x].son[1]!=0) {root=tr[x].son[1]; tr[root].f=0;}
/*
如果这个点没有左孩子但是有右孩子的话
右孩子成为根节点,并且这个右孩子没有父亲节点
*/
else if(tr[x].son[0]!=0 && tr[x].son[1]==0) {root=tr[x].son[0]; tr[root].f=0;}
/*
如果这个点没有右孩子但是有左孩子的话
左孩子成为根节点,并且这个左孩子没有父亲节点
*/
else//if(tr[x].son[0]!= 0 && tr[x].son[1]!=0) //既有左孩子,也有右孩子
{
int p=tr[x].son[0];//定义p为x的左孩子
while(tr[p].son[1]!=0)//如果p有右孩子的话
{
p=tr[p].son[1];//那么p就更新为自己的右孩子
splay(p,x);//把右孩子旋转到x的孩子节点,也就是转到p的位置
/*
一直往右边跳,因为右边是比根节点的值要大的,所以往右边
*/
}//循环到没有有孩子的时候,这个值就是最大的
//又因为没有这个p点没有了右孩子,所以就可以收x的右孩子成为自己的右孩子
int r=tr[x].son[1];//小人为x节点的右孩子
int R=p;//大人为p,也就是x节点的右孩子成为p节点的右孩子
/*
4 经过第一次 4
/ \ 转动了3 /
2 6 而且4的右孩子也 3
/ \ / \ 成为了3的右孩子 / \
1 3 5 7 2 6
/ / \
1 5 7
这个时候就成为了我们要的,只有一个孩子节点
*/
tr[R].son[1]=r;
tr[r].f=R;//定下结论,我们现在只有一个子树了
root=R; tr[root].f=0;
/*
这个时候新的root就等于我们找到的最大的值
目的就是把每一个访问过的都记录下来
*/
update(R);//更新这一整棵树就好了
}
}
int findpaiming(int d)//找排名
{
int x=findip(d); splay(x,0);
//先找到这个值,然后让他成为根节点
return tr[tr[x].son[0]].c+1;
//左孩子的控制人数再+1就是自己的排名
/*
100 第一次 100 第二次 23
/ \ 移动23 / \ 旋转23 \
55 120 23 120 100
/ \ \ \ / \
23 144 55 144 55 144
\ / /
34 34 34
/ / /
30 30 30
比如说我们要找23的排名
排名为1
这样就对了因为我们要找的是从小到大的排名
所以23最小就为1
*/
}
int findzhi(int k)//找排名为k的值
{
int x=root;//定义x为根节点,从根节点开始找
while(1)//
{
int lc=tr[x].son[0]; int rc=tr[x].son[1];//左边和右边
if(k<=tr[lc].c) x=lc;
/*
如果k的这个排名比左边控制的人数还要少
就去左边找
这个时候就把左边设置为要继续往下找的一个终点位置
其实就是伸展树的好处,记录访问过的
*/
else if(k>tr[lc].c+tr[x].n)
/*
如果这个排名比(左边控制的人数+根节点重复的节点数)都要大
就去右边找
*/
{
k-=tr[lc].c+tr[x].n;
/*
注意:光继续在右边找还不够
要减去(左边的控制人数+根节点重复的节点数)
比如说:我们要找17
3
/ \
10 ?
这个时候右边控制的人数+根节点重复的节点数=13
比17要小,说明我们要去右边找
去右边找的就是 17-13=4,找排名为4的节点的值
*/
x=rc;//去右边继续找
}
else break;//否则要找的排名就在根节点中间
}
splay(x,0);//把找到的合适的移到根节点
return tr[x].d;//把我们找到的这个节点的值返回给函数findzhi
}
int findqianqu(int d)//找前驱
{
int x=findip(d); splay(x,0);//找到d的编号,使他成为根节点
if(d<=tr[x].d && tr[x].son[0]!=0)
//如果是if( d<tr[x].d && tr[x].son[0]!=0 )则找到的是:小于等于d的前驱
//如果这个值比根节点的值要小,并且有左孩子的话
{
x=tr[x].son[0];//把这个点的左孩子移到根节点
while(tr[x].son[1]!=0) x=tr[x].son[1];
/*
找完之后一直往右边跳(也就是寻找),找右边的最大值
前驱是比要找的值小的最大值
所以只要是左孩子的话就一定比d要小
那么左孩子的右孩子就是比d小而且是比左孩子要大的
这样就可以找到最大的值
*/
}
if(tr[x].d>=d) x=0;//如果是if(tr[x].d>d)则找到的是:小于等于d的前驱
/*
如果我们找到的这个值大于等于d的话
说明以d为根节点的这棵数没有左孩子
那就说明没有合适的前驱
就只能为0
*/
return x;//返回x的值
}
int finddouji(int d)//找后继
{
int x=findip(d); splay(x,0);//找到d的编号,使他成为根节点
if(tr[x].d<=d && tr[x].son[1]!=0)
//如果这个值比根节点的值要大,并且有右孩子的话
{
x=tr[x].son[1];//把这个点的右孩子移到根节点
while(tr[x].son[0]!=0) x=tr[x].son[0];
/*
找完之后一直往左边跳(也就是寻找),找左边的最小值
后继是比要找的值大的最小值
所以只要是右孩子的话就一定比d要大
那么右孩子的左孩子就是比d大而且是比右孩子要小的
这样就可以找到最小的值
*/
}
if(tr[x].d<=d) x=0;
/*
如果我们找到的这个值小于等于d的话
说明以d为根节点的这棵数没有右孩子
那就说明没有合适的后继
就只能为0
*/
return x;//返回x的值
}
int main()
{
int n; n=read();
root=0; len=0;//初始化没有根节点,也没有节点
for(int i=1;i<=n;i++)
{
int cz,x; cz=read(); x=read();
if(cz==1) ins(x);//插入
else if(cz==2) del(x);//删除
else if(cz==3) printf("%d\n",findpaiming(x));//找排名
else if(cz==4) printf("%d\n",findzhi(x));//找排名值
else if(cz==5) printf("%d\n",tr[findqianqu(x)].d);//找前驱
else if(cz==6) printf("%d\n",tr[finddouji(x)].d);//找后继
}
return 0;
}