Index
AVL
说在前面
好久没写博客了……
まさか,必须克服掉“必须先写知识点Blog才能写对应知识点Blog”的猫病吗。
我写知识点Blog实在是太慢了……
不然题目的Blog根本出不来啊。
二叉查找树
定义
首先,二叉查找树(Binary Search Tree)是一颗树。
首先,二叉查找树是一颗二叉树。
首先,BST是满足:
任意一个结点的所有的左儿子的权值都小于它,所有右儿子的权值都大于它。
的这样的一棵树。
还有对其进行中序遍历,得到的序列一定是单调递增的。
操作
因为不是重点所以暴力一点。
然后——我把要说的通通丢到namespace里面去了。
讲解啊代码啊吐槽啊什么的都在里面。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
namespace BST
{
const int MAXN=102030;
struct node
{
int lc,rc,val;
//左右儿子和关键字
}tree[MAXN];
int n,root;
void insert(int val,int &pos)//val是要插入的值,pos是当前访问的节点
//我们只需要依照定义找到新插入的值的归宿即可
{
if(pos==0)
//访问到了虚空结点!也就是说,沿着大小关系走到了不能再走了,就在此安息吧!
{
tree[++n].val=val,pos=n;
return ;
}
insert(val,val<tree[pos].val?tree[pos].lc:tree[pos].rc);
//如果相等的话倒是可以再在node结构体里开一个变量记录出现次数,这里默认了所有数据不相等(为了压行)
}
//解释一下为什么用取址符:因为有“pos=n”这一步,所以也就顺便把“tree[pos].lc”或者“tree[pos].rc”设为新增的结点n
//如果不想用取址符,也可以再开一个局部变量记录父亲结点
int find(int val,int pos)
//在树里找val,找到返回下标,找不到返回-1
{
if(pos==0) return -1;
if(tree[pos].val==val) return pos;
return find(val,val<tree[pos].val?tree[pos].lc:tree[pos].rc);
}
int pre(int val)
//找前驱,默认的是找小于的那个,网上还有叫什么lower的。
//对了,到底是前驱还是前趋啊?
{
int pos=root,ans=-2147483647;
while(pos)
{
if(tree[pos].val<val) ans=max(ans,tree[pos].val);
pos=val<tree[pos].val?tree[pos].lc:tree[pos].rc;
}
return ans;
}
//所谓前驱就是小于某个值的最大值,也可以说是这个点的左子树里面最靠右的那个端点。
int nxt(int val)
//后继后继,也有叫upper的,名字很多。
{
int pos=root,ans=2147483647;
while(pos)
{
if(tree[pos].val>val) ans=min(ans,tree[pos].val);
pos=val<tree[pos].val?tree[pos].lc:tree[pos].rc;
}
return ans;
}
//可以发现,find,pre,nxt几乎一样,只要愿意的话,都可以用循环、递归两种方案来解决……
//删除操作———就连普通的BST的删除也不简单。
/*
稍微讲一下。
如果删除的是叶子结点,那么直接注销,没问题的!
如果删除的结点只有一个儿子,那么就把它的子树接到父亲结点的下面。
如果删除的结点子女成群,有两个儿子,就找到它左子树里最右的儿子(就是前驱)来顶替它的位置。
(↑女呢?)
*/
void deleted(int val,int &pos)
{
if(pos==0) return;
if(tree[pos].val==val)//确认过眼神,是要删掉的人
{
if(tree[pos].lc==0 || tree[pos].rc==0) pos=tree[pos].lc+tree[pos].rc;
//此处偷大懒,反正有一个是零,那么和就是另一个值,如果两个都是零,那么和就是零。(我第一次看到要把点求和的时候真的被吓到了!)
else
{
int now=tree[pos].lc,cut=pos;
while(tree[now].rc) cut=now,now=tree[now].rc;
tree[pos].val=tree[now].val;
if(cut==pos) tree[cut].lc=tree[now].lc;
else tree[cut].rc=tree[now].lc;
//now这个结点被提拔了,但是now可能还是下有小,这只能交给上有老来抚养了。
//为什么要分两类呢?因为这里不是重点(所以我用代码片所以我画不了图)所以我就不画图了,自己想一下吧。
}
return ;
}
deleted(val,val<tree[pos].val?tree[pos].lc:tree[pos].rc);
}
};
//总感觉什么都甩到代码片里看起来很乱呢?
int main()
{
}
时间复杂度分析
很容易发现,每个函数都时间复杂度都是
Θ
(
层
数
)
\Theta(层数)
Θ(层数)的。
层数最大多少层呢?
如果是随机数据,层数就期望在 Θ ( log 2 n ) \Theta(\log_2{n}) Θ(log2n)级别。
可是,
通过超简单的构造(就是给的序列单调就行了),我们可以轻松地让这个二叉查找树以一条链的形式出现。
也就是说,层数最大是
Θ
(
n
)
\Theta(n)
Θ(n)级别的。
要优化。
计算机学家们如是想。
AVL算法
1962年的这一天,上面的这两位先生(就是这位AV先生(…?)和L先生),发表了一篇论文。
他们说,他们想到一个办法,可以让一颗二叉查找树保持平衡,稳定在
Θ
(
log
2
n
)
\Theta(\log_2{n})
Θ(log2n)层。
来康康。
平衡树和平衡因子
首先,怎么衡量一颗二叉查找树是否“平衡”。
对于每个节点,记录以之为根的子树的层数,即高度、深度之类的,记为
h
h
h吧。并记其左右儿子的
h
h
h值之差,叫做平衡因子,记为
w
w
w吧,我随便偷个小懒,就让w恒正吧。(用什么符号记录,加不加绝对值都无所谓吧)
(反正上课的时候w是从-2到+2的,但是左子树大还是右子树大不是很重要,所以我就偷懒用绝对值吧)
总之,如果某个点的平衡因子的超过1了,这棵树就不平衡了!
就是这么说的。
比方说:
(电脑上写字事倍功半的说)
这样子的一颗树叫做二叉平衡树,Self-balancing binary search tree,就是会自平衡的二叉查找树,也有喊它平衡二叉树的来着。
因为是AV先生(…?)和L先生提出来的这种树的概念,也称这种树为AVL树。
AVL树的定义还能推出其它性质:
1
◯
Θ
(
层
数
)
=
Θ
(
log
2
n
)
⇒
各
种
操
作
的
时
间
复
杂
度
=
Θ
(
log
2
n
)
2
◯
记
N
h
为
一
颗
高
度
为
h
的
A
V
L
树
的
最
小
结
点
数
,
F
n
为
第
n
个
斐
波
那
契
数
,
H
n
为
一
颗
有
n
个
结
点
的
A
V
L
树
的
最
大
高
度
。
则
有
,
N
0
=
0
,
N
1
=
1
,
N
2
=
2
,
N
h
=
N
h
−
1
+
N
h
−
2
+
1
发
现
N
h
=
F
h
+
2
−
1
。
⇒
N
h
≈
Φ
h
+
2
5
−
1
,
Φ
=
5
−
1
2
⇒
H
n
≈
log
2
(
5
(
n
+
2
)
)
−
2
⇒
H
n
是
Θ
(
log
2
n
)
级
别
的
。
\begin{aligned} &\text{\textcircled 1}~\Theta(层数)=\Theta(\log_2{n}) \Rightarrow 各种操作的时间复杂度=\Theta(\log_2{n})\\ &\text{\textcircled 2}~ 记N_h为一颗高度为h的AVL树的最小结点数,F_n为第n个斐波那契数,H_n为一颗有n个结点的AVL树的最大高度。\\ &则有,N_0=0,N_1=1,N_2=2,N_h=N_{h-1}+N_{h-2}+1 \\ &发现N_h=F_{h+2}-1。\\ &\Rightarrow N_h\approx \cfrac{\Phi^{h+2}}{\sqrt{5}}-1,\Phi=\frac{\sqrt{5}-1}{2} \\ &\Rightarrow H_n\approx \log_2 \left( \sqrt{5}\left( n+2 \right) \right)-2\\ &\Rightarrow H_n是\Theta(\log_2{n})级别的。 \end{aligned}
1◯ Θ(层数)=Θ(log2n)⇒各种操作的时间复杂度=Θ(log2n)2◯ 记Nh为一颗高度为h的AVL树的最小结点数,Fn为第n个斐波那契数,Hn为一颗有n个结点的AVL树的最大高度。则有,N0=0,N1=1,N2=2,Nh=Nh−1+Nh−2+1发现Nh=Fh+2−1。⇒Nh≈5Φh+2−1,Φ=25−1⇒Hn≈log2(5(n+2))−2⇒Hn是Θ(log2n)级别的。
我自已一字一句地敲了一些什么出来……
“自平衡”有很多种方法,下面介绍的就是一种叫做“AVL算法”的自平衡算法。
结点
所以说,我们的结点要在原来的基础上再加一个变量记录高度。
长这样:
struct node
{
int lc,rc,val,h;
//左右儿子和关键字和高度,平衡因子到时候算就是了
}tree[MAXN];
zig & zag
插入和删除的时候,如果一个结点失去平衡了,这个时候就需要“旋转”,来进行自平衡。
是什么
左旋
z
a
g
zag
zag,即逆时针旋转,也有叫“
R
旋
转
R旋转
R旋转”的。如图:
图片从左往右看过去,这三次操作就是一次
z
a
g
zag
zag。
因为之前紫点是蓝点的右儿子,说明紫>蓝,因此让蓝接到紫的左儿子上,是可行的。
右旋 z i g zig zig,即顺时针旋转,也有叫“ L 旋 转 L旋转 L旋转”的。
就是这样。
可以发现,“如果我是你的右儿子,那么我就左旋当你爸爸;我是你的左儿子,我就右旋当你爸爸。”
然后左旋就是把左上边的爸爸拉到左下方当左儿子。
诸如此类。
我是你的儿子?我当你爸爸? (⊙_⊙)?
有什么用
我们说过,旋转的目的是为了维护平衡。
首先,假设我们对一颗平衡树插入或删除了一个节点导致其不平衡,会出现什么状态?
只有可能是这四个。
然后A和D,B和C分别是类似的。
A D 直线型
聪明的都看出来了,镜像翻转是个好东西。
把“/
”和“\
”类型的不平衡部分通过一次
z
i
g
o
r
z
a
g
zig~ or ~zag
zig or zag,转成了“^
”的平衡部分。
这样子,原本不平衡的黄点,现在变得平衡了。
B C 折线形
对于“く
”和“>
”类型的东西,考虑先对下面那条边用
z
a
g
o
r
z
i
g
zag~or~zig
zag or zig转换成“/
”和“\
”,再如上进行
z
i
g
o
r
z
a
g
zig~or~zag
zig or zag。
就像这样:
“く
”的情况类似(实际上是不想画了)。
总之,
类型 | 应对方案 |
---|---|
/ | 上 z i g zig zig |
\ | 上 z a g zag zag |
く | 下 z a g zag zag上 z i g zig zig,合称 z a g − z i g zag-zig zag−zig |
> | 下 z i g zig zig上 z a g zag zag,合称 z i g − z a g zig-zag zig−zag |
实践中会遇到的问题
首先,并不是每次旋转都是三个孤零零的小朋友在那里坐以待毙,往往都拖家带口。
因为不管 z i g z a g zigzag zigzag还是 z a g z i g zagzig zagzig都是由两个基本函数组成, z a g zag zag和 z i g zig zig,而它俩说实话照照镜子没差,所以我们以一次 z i g zig zig为例。
比方说:
三角上标的h代表这颗子树的高度,请不要在意大小。
然后绿点的另外半边就无视吧。
我是黄点。
大逆不孝的我把位于我右上角的父亲拉了下来当成我的右儿子。 那么会出现什么后果?因为我们刚才旋转的时候没有考虑其他人,我们就假设红点的其它邻接边通通断掉,黄点和蓝三角的边也断掉。
也就是说,
我的右儿子会失去爸爸。
我的爸爸的右儿子会失去爸爸。
我的爷爷会失去某个儿子。
我会失去爸爸。
我的爸爸会失去所有儿子。
(这一家人好惨啊)
也就是上图的蓝三角、紫三角、黄点会失去父亲结点,绿点会失去左儿子(就假设它是左儿子吧),红点将没有儿子。
蓝三角,大于黄点并小于红点。
紫三角,大于红点黄点,小于绿点。
绿点,是在坐的各位最大的。
牵桥引线一下:
就这样牵起红线,不仅大小关系满足了,更是好好平衡了一下。
我的右儿子认我新的右儿子(之前的爸爸)为爸爸,成了我的孙子。 我的兄弟还是他爸爸的儿子,但是他也已经是我的孙子了。
我的前爷爷现在就是我的爸爸了。
总之,给出 z i g 和 z a g 等 等 zig和zag等等 zig和zag等等的代码:
void zig(int &pos)
{
int down=tree[pos].lc;
tree[pos].lc=tree[down].rc;
tree[down].rc=pos;
tree[pos].h=max(tree[tree[pos].rc].h,tree[tree[pos].lc].h)+1;
tree[down].h=max(tree[tree[down].rc].h,tree[tree[down].lc].h)+1;
pos=down;
}
//这里给的pos指的是待旋转边的靠上的那个节点。
//然后,高度记得要随时更新。
void zag(int &pos)
{
int down=tree[pos].rc;
tree[pos].rc=tree[down].lc;
tree[down].lc=pos;
tree[pos].h=max(tree[tree[pos].rc].h,tree[tree[pos].lc].h)+1;
tree[down].h=max(tree[tree[down].rc].h,tree[tree[down].lc].h)+1;
pos=down;
}
void zagzig(int &pos)
{
zag(tree[pos].lc),zig(pos);
}
void zigzag(int &pos)
{
zig(tree[pos].rc),zag(pos);
}
第二个问题。
这个白痴博主一直说选三个点旋转选三个点摆成什么造型。可是——到底选哪三个点?!
如果说出现了上图的情况,那么我们插入点的时候岂不是要往上爬h层以后才找得到不平衡的点?!
好像是的。
毕竟本来我们的插入操作就是
Θ
(
log
2
n
)
\Theta(\log_2{n})
Θ(log2n)的,要二分地先找到合适的位置插入。并且插入之后,刚刚我们二分地跑过的那些点的左右儿子的高度差(平衡因子)都会产生变动。
也就是说,先
Θ
(
log
2
n
)
\Theta(\log_2{n})
Θ(log2n)地的找到该插入的地方,然后插入,然后我们
Θ
(
log
2
n
)
\Theta(\log_2{n})
Θ(log2n) 地返回,同时更新高度,如果平衡因子不对头了,就旋转。
常数会大点。
插入
void insert(int val,int &pos)
//val是要插入的值,pos是当前访问的节点
//我们只需要依照定义找到新插入的值的归宿即可
//顺便看看是不是不平衡了
{
if(pos==0)
//访问到了虚空结点!也就是说,沿着大小关系走到了不能再走了,就在此安息吧!
{
tree[++n].val=val,tree[n].h=1,pos=n;
return ;
}
if(val<tree[pos].val)//新增结点在当前节点的左边
{
insert(val,tree[pos].lc);
if(tree[tree[pos].lc].h==tree[tree[pos].rc].h+2)//不平衡了!!(当然左边的高度高于右边了)
{
if(val<tree[tree[pos].lc].val) zig(pos);//“/”型
else if(val>tree[tree[pos].lc].val) zagzig(pos);//“く”
}
}
if(val>tree[pos].val)//新增结点在当前节点的左边
{
insert(val,tree[pos].rc);
if(tree[tree[pos].rc].h==tree[tree[pos].lc].h+2)//不平衡了!!(当然右边的高度高于左边了)
{
if(val>tree[tree[pos].rc].val) zag(pos);//“\”型
else if(val<tree[tree[pos].rc].val) zigzag(pos);//">"型
}
}
//如果相等的话倒是可以再在node结构体里开一个变量记录出现次数,这里默认了所有数据不相等(为了压行)
tree[pos].h=max(tree[tree[pos].lc].h,tree[tree[pos].rc].h)+1;//更新h
}
//再解释一下为什么用取址符:因为有“pos=n”这一步,所以也就顺便把“tree[pos].lc”或者“tree[pos].rc”设为新增的结点n
//如果不想用取址符,也可以再开一个局部变量记录父亲结点
删除
插入中,最多会有一次旋转。
然而,删除,并不总是在叶子结点处发生,而且可能会导致整棵树的平衡性坍塌,需要多次旋转来恢复平衡。
因此为了处理多次旋转,我们先把旋转写一个函数出来。
maintain函数
void maintain(int &pos)
{
if(tree[tree[pos].lc].h==tree[tree[pos].rc].h+2)
{
if(tree[tree[tree[pos].lc].lc].h==tree[tree[pos].rc].h+1) zig(pos);
else if(tree[tree[tree[pos].lc].rc].h==tree[tree[pos].rc].h+1) zag(tree[pos].lc),zig(pos);
}
if(tree[tree[pos].lc].h+2==tree[tree[pos].rc].h)
{
if(tree[tree[tree[pos].rc].rc].h==tree[tree[pos].lc].h+1) zag(pos);
else if(tree[tree[tree[pos].rc].lc].h==tree[tree[pos].lc].h+1) zig(tree[pos].rc),zag(pos);
}
tree[pos].h=max(tree[tree[pos].lc].h,tree[tree[pos].rc].h)+1;
}
就是把insert里面那一堆删掉insert。
删除函数
首先要注意:如果要删的数不在树上,那么怎么处理?根据题目不同的要求,可能找不到就不删除,也可能删掉前驱或后继,这里一定要注意。
接着,参考我们之前写的普通删除:
如果删除的是叶子结点,那么直接注销,没问题的!
如果删除的结点只有一个儿子,那么就把它的子树接到父亲结点的下面。
如果删除的结点子女成群,有两个儿子(↑女呢?),就找到它左子树里最右的儿子(就是前驱)来顶替它的位置。
思路是基本相同的,详见代码:
int deleted(int &pos,int val)//在pos的子树中删除val,此处是如果没有则将小于val的最大的那个删除
{
int tmp;//可能要上传val值
if(val==tree[pos].val || (val<tree[pos].val && tree[pos].lc==0) || (val>tree[pos].val && tree[pos].rc==0))
{//找到val了或者不是val但是是小于val的最大的那个
if(tree[pos].lc==0 || tree[pos].rc==0)
{//如果它没有左儿子或右儿子,就让另外一个儿子来顶替他;如果没有任何儿子,就很开心
tmp=tree[pos].val,pos=tree[pos].lc+tree[pos].rc;
return tmp;
}
else tree[pos].val=deleted(tree[pos].lc,val);
//如果子女都有,那就先往下走,找到小于它的最大值,再来顶替它
}
else tmp=deleted(val<tree[pos].val?tree[pos].lc:tree[pos].rc,val);
//这个节点是安全的,就往下找
maintain(pos);//随手关灯好习惯
return tmp;
}
就是这样,最后引用一下:
AVL仍然可以采用惰性删除,而且比普通BST的惰性删除更加科学。
对于要删除的节点,只需要给它进行一个标记。树的高度仍然是log(N),这样并不会降低效率,而删除操作却要快速的多。而且,如果遇到那些删除过后还要恢复的节点,则惰性删除更优,不需要额外占用空间,将原来的节点恢复即可。如果遇到那种允许节点重复的AVL,惰性删除更可取,将删除标记设为整型变量,表示该节点出现的数量即可。
普通平衡树
题目描述
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
插入x数
删除x数(若有多个相同的数,因只删除一个)
查询x数的排名(排名定义为比当前数小的数的个数+1。若有多个相同的数,因输出最小的排名)
查询排名为x的数
求x的前驱(前驱定义为小于x,且最大的数)
求x的后继(后继定义为大于x,且最小的数)
输入输出格式
输入格式:
第一行为nn,表示操作的个数,下面nn行每行有两个数optopt和xx,optopt表示操作的序号( 1 ≤ o p t ≤ 6 1 \leq opt \leq 6 1≤opt≤6)
输出格式:
对于操作3,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
说明
时空限制:1000ms,128M
1.n的数据范围:
n
≤
100000
n \leq 100000
n≤100000
2.每个数的数据范围:
[
−
10
7
,
10
7
]
[-{10}^7, {10}^7]
[−107,107]
来源:Tyvj1728 原名:普通平衡树
我的回合
就是码了个模板敷衍了事。
解释一下:首先因为题目会有重复的数字出现所以结构体里新增了一个cnt来记录这个节点上重复了多少个同样的这个数。
所以现在删除就可以偷下懒了,要删除某个数的时候把它的cnt减一就是了,当然这样会浪费时间和空间……(不过可以节省maintain的时间)
然后是上文没有出现过的求排名和求排名上的数。
记为
g
e
t
r
a
n
k
和
r
a
n
k
g
e
t
getrank和rankget
getrank和rankget吧。
对于给定数字来找排名,如果我们对于每个节点加一个值siz代表子树的大小(包括cnt),那么就很轻松了,(假设给定的数字在树上并且采用的是上文的偷懒删除法),我们就像find一样,找到了以后,它的排名就是它左子树的大小+1。
对于给定排名来找数字,思路和上一段差不多,看看代码吧(其实是因为不会描述)。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN=102030;
inline void Read(int &p)
{
p=0;
int f=1;
char c=getchar();
while(c<'0' || c>'9')
{
if(c=='-') f=-1;
c=getchar();
}
while(c>='0' && c<='9')
p=p*10+c-'0',c=getchar();
p*=f;
}
struct node
{
int lc,rc,val,h,cnt,siz;
}tree[MAXN];
int Q,opt,n,u,root,ans;
void zig(int &pos)
{
int down=tree[pos].lc;
tree[pos].lc=tree[down].rc;
tree[down].rc=pos;
tree[pos].h=max(tree[tree[pos].rc].h,tree[tree[pos].lc].h)+1;
tree[down].h=max(tree[tree[down].rc].h,tree[tree[down].lc].h)+1;
tree[pos].siz=tree[tree[pos].lc].siz+tree[tree[pos].rc].siz+tree[pos].cnt;
tree[down].siz=tree[tree[down].lc].siz+tree[tree[down].rc].siz+tree[down].cnt;
pos=down;
}
void zag(int &pos)
{
int down=tree[pos].rc;
tree[pos].rc=tree[down].lc;
tree[down].lc=pos;
tree[pos].h=max(tree[tree[pos].rc].h,tree[tree[pos].lc].h)+1;
tree[down].h=max(tree[tree[down].rc].h,tree[tree[down].lc].h)+1;
tree[pos].siz=tree[tree[pos].lc].siz+tree[tree[pos].rc].siz+tree[pos].cnt;
tree[down].siz=tree[tree[down].lc].siz+tree[tree[down].rc].siz+tree[down].cnt;
pos=down;
}
void zagzig(int &pos)
{
zag(tree[pos].lc),zig(pos);
}
void zigzag(int &pos)
{
zig(tree[pos].rc),zag(pos);
}
void maintain(int &pos)
{
if(tree[tree[pos].lc].h==tree[tree[pos].rc].h+2)
{
if(tree[tree[tree[pos].lc].lc].h==tree[tree[pos].rc].h+1) zig(pos);
else if(tree[tree[tree[pos].lc].rc].h==tree[tree[pos].rc].h+1) zag(tree[pos].lc),zig(pos);
}
if(tree[tree[pos].lc].h+2==tree[tree[pos].rc].h)
{
if(tree[tree[tree[pos].rc].rc].h==tree[tree[pos].lc].h+1) zag(pos);
else if(tree[tree[tree[pos].rc].lc].h==tree[tree[pos].lc].h+1) zig(tree[pos].rc),zag(pos);
}
tree[pos].h=max(tree[tree[pos].lc].h,tree[tree[pos].rc].h)+1;
tree[pos].siz=tree[tree[pos].lc].siz+tree[tree[pos].rc].siz+tree[pos].cnt;
}
void insert(int val,int &pos)
{
if(pos==0)
{
tree[++n].val=val,tree[n].h=1,tree[n].siz=1,tree[n].cnt=1,pos=n;
return ;
}
if(val<tree[pos].val) insert(val,tree[pos].lc);
if(val>tree[pos].val) insert(val,tree[pos].rc);
if(val==tree[pos].val) tree[pos].cnt++;
maintain(pos);
}
void deleted(int &pos,int val)
{
if(val==tree[pos].val) tree[pos].cnt--;
else deleted(val<tree[pos].val?tree[pos].lc:tree[pos].rc,val);
tree[pos].siz=tree[tree[pos].lc].siz+tree[tree[pos].rc].siz+tree[pos].cnt;
}
int find(int val,int pos)
{
if(pos==0) return -2147483647;
if(tree[pos].val==val) return tree[pos].cnt>0?pos:-2147483647;
return find(val,val<tree[pos].val?tree[pos].lc:tree[pos].rc);
}
int getrank(int val,int pos)
{
if(pos==0) return 2147483647;
if(val==tree[pos].val) return tree[tree[pos].lc].siz+1;
else
{
if(val<tree[pos].val) return getrank(val,tree[pos].lc);
else return getrank(val,tree[pos].rc)+tree[tree[pos].lc].siz+tree[pos].cnt;
}
}
int rankget(int rak,int pos)
{
if(pos==0) return -2147483647;
if(rak>=tree[tree[pos].lc].siz+1 && rak<=tree[tree[pos].lc].siz+tree[pos].cnt) return tree[pos].val;
else
{
if(rak<tree[tree[pos].lc].siz+1) return rankget(rak,tree[pos].lc);
return rankget(rak-tree[tree[pos].lc].siz-tree[pos].cnt,tree[pos].rc);
}
}
void pre(int val,int pos)
{
if(pos==0) return;
if(tree[pos].val<val)
{
if(tree[pos].cnt) ans=max(ans,tree[pos].val);
else pre(val,tree[pos].lc);
if(tree[tree[pos].rc].siz) pre(val,tree[pos].rc);
}
else pre(val,tree[pos].lc);
}
void nxt(int val,int pos)
{
if(pos==0) return;
if(tree[pos].val>val)
{
if(tree[pos].cnt) ans=min(ans,tree[pos].val);
else nxt(val,tree[pos].rc);
if(tree[tree[pos].lc].siz) nxt(val,tree[pos].lc);
}
else nxt(val,tree[pos].rc);
}
int main()
{
Read(Q);
while(Q--)
{
Read(opt),Read(u);
if(opt==1) insert(u,root);
if(opt==2) deleted(root,u);
if(opt==3 && find(u,root)!=-2147483647) printf("%d\n",getrank(u,root));
if(opt==4) printf("%d\n",rankget(u,root));
if(opt==5) ans=-2147483647,pre(u,root),printf("%d\n",ans);
if(opt==6) ans=2147483647,nxt(u,root),printf("%d\n",ans);
}
}