acm-【平衡树】学习笔记(Splay,Treap,fhq Treap,替罪羊树,红黑树,avl tree,B树,B+树)

引言

本文的写作目的主要是为了作者日后复习,也供浏览本文的群众以参考,若有不严谨之处欢迎在评论区指出。
本文需要的前置知识:二叉查找树

目录

  • 引言
  • Splay
  • Treap
  • fhq Treap
  • 替罪羊树
  • 红黑树
  • avl tree
  • B tree
  • B+ tree

下面所有的代码都以LuoGu P3369 【模板】普通平衡树为模板题进行编写。

Splay

Splay又名伸展树,是一种比较常见的平衡树,它的核心操作主要是旋转操作,通过连续的旋转将某个节点 u u u旋转到指定位置,一般指定为某个节点 r t rt rt的子节点。

先实现最简单的单旋操作,分为左旋和右旋,方便起见只讨论右旋(即 u u u是其父亲的左子节点),通过异或操作可以很方便的统一这两种旋转。
如下图所示,我们现在要把 u u u节点旋转到 f f f节点的位置,可以看成是向右旋转了一下,相应地 u u u在旋转后就会变成 f f f节点的父亲
acm-【平衡树】学习笔记(Splay,Treap,fhq Treap,替罪羊树,红黑树,avl tree,B树,B+树)_第1张图片
旋转后如下图所示,我们不难发现旋转后节点之间仍然满足平衡树的性质,即左边子树的节点小于当前节点小于右边子树的节点。
acm-【平衡树】学习笔记(Splay,Treap,fhq Treap,替罪羊树,红黑树,avl tree,B树,B+树)_第2张图片
那么旋转需要改变哪些量呢,由图可以看出,我们改变了三个连边关系,若假设 f f f最初的父亲节点为 f f ff ff,那么从原图来看,我们改变了 f → u , u → r s , f f → f f\rightarrow u,u\rightarrow rs,ff\rightarrow f fu,urs,fff这三条有向边,而新图中在去掉这三条边后重建了三条边,即 u → f , f → r s , f f → u u\rightarrow f,f\rightarrow rs,ff\rightarrow u uf,frs,ffu

那么代码就很好写了,不过为了统一左旋和右旋我们采用异或的写法,具体看代码注释,如下所示:

void pushup(int rt){//维护关于节点的一些信息,这里维护的是子树大小
	sz[rt]=sz[ch[rt][0]]+sz[ch[rt][1]]+cnt[rt];
}
int dir(int u){//获取u节点是它父亲的左儿子还是右儿子
	return ch[fa[u]][1]==u;
}
void connect(int u,int f,int d){//让u变成f的儿子,d=0代表左儿子,d=1代表右儿子
	ch[f][d]=u;
	fa[u]=f;
}
void rotate(int u){
	int f=fa[u],ff=fa[f],du=dir(u);
	if(!f)return;//已经是根节点就不用旋转
	int df=dir(f);
	connect(u,ff,df),connect(ch[u][du^1],f,du),connect(f,u,du^1);//连上旋转后新图中的三条边的同时也删除掉了原图中的三条边(所以没必要写额外的删边函数)
	pushup(f),pushup(u);//更新一下f,然后再更新一下u,注意顺序
}

有了旋转函数,我们可以将任意一个非根节点(根节点就保持不变)旋转到它父亲节点的位置,那么现在将实现连续旋转操作,这种操作能将一个指定节点 u u u不断旋转直到变成 r t rt rt的子节点为止。
不过需要注意不能直接一直 r o t a t e ( u ) rotate(u) rotate(u)然后检查是否满足 f a [ u ] = r t fa[u]=rt fa[u]=rt,这样的话并不能保证复杂度,我们可以发现当树是一条链的时候通过这样的操作之后树仍然是一条链(可以手动模拟5个节点一条链的情况),因此我们采用双旋的办法来解决这个问题,所谓双旋就是每次考虑两个节点,并且一次性向上旋转两次。

具体来说有两种情况。
情况一: d i r ( u ) ≠ d i r ( f ) dir(u)\ne dir(f) dir(u)=dir(f),这时候我们可以直接旋转两次 u u u即可。
情况二: d i r ( u ) = d i r ( f ) dir(u)=dir(f) dir(u)=dir(f),这时候我们先旋转 f f f节点,再旋转 u u u节点即可。

实现就很简单了,代码也非常简洁:

void splay(int u,int rt){//splay操作,最最核心的操作
	for(int f=fa[u];f!=rt;f=fa[u]){
		if(fa[f]!=rt)(dir(u)^dir(f))?rotate(u):rotate(f);//判断情况
		rotate(u);
	}
	if(!rt)root=u;//如果rt=0,那么u就成为了新的根节点
}

有了 s p l a y splay splay这个操作,我们就可以保证平衡树的复杂度了,剩下的内容其实和普通的二叉查找树几乎没有任何区别,唯一的区别就是每次操作都要splay一下,可以认为splay能够将树压扁。下面一个个解析每个操作,相当于复习一遍二叉查找树。

插入节点 i n s e r t ( x ) : insert(x): insert(x): 考虑先沿着二叉查找树的搜索路径查找看是否存在值为 x x x的节点,如果找到了,先把这个节点 s p l a y splay splay到根节点(这一步是为了保证复杂度),然后把这个节点的 c n t cnt cnt加一下,代表这个节点又多了一个;如果没有找到,那就新建一个节点,然后把这个节点连接在二叉搜索树后面(也就是搜索路径的最后一个节点的子节点,具体是右子节点还是左子节点要看它们值的大小关系),最后把它 s p l a y splay splay到根节点即可。代码如下所示:

void insert(int x){
	int u=root,f=0;//f用于记录搜索路径上的最后一个节点
	while(u && val[u]!=x){//先沿着搜索路径寻找值为x的节点
		f=u;u=ch[u][x>val[u]];
	}
	if(u)splay(u,0),cnt[u]++;//如果存在就splay到根节点并cnt++,此时f没有用到
	else{//否则新建节点
		u=++tot;
		val[u]=x;
		cnt[u]=sz[u]=1;
		connect(u,f,x>val[f]);//将新建节点连接到f上
		splay(u,0);//记得要把新建节点splay一下
	}
}

删除节点 d e l ( x ) : del(x): del(x): 考虑先找到值为 x x x的节点,如果找不到那么程序结束;否则假设该节点为 u u u,我们看它 c n t cnt cnt是否大于1,大于1的话说明有多个节点,那直接 c n t − − cnt-- cnt删除一个即可,不过需要注意要把 u u usplay到根节点,若 c n t cnt cnt等于1那么我们需要删除这个节点,考虑找到它的前驱,然后将前驱(假设是 v v v节点) s p l a y splay splay u u u节点的左儿子的位置,然后让 v v v作为 u u u的右儿子的父亲即可,需要把 v v v设置为新的根节点,如果没有前驱就更好办了,直接把右儿子设置为根节点。代码实现还有一些细节,详见注释:

void destory(int u,int f){//辅助函数,断开u和f的边,f是u的父亲节点
	ch[f][dir(u)]=0;
	fa[u]=0;
}
int fd(int x){//寻找值为x的节点
	int u=root;
	while(u && val[u]!=x){
		u=ch[u][x>val[u]];
	}
	if(u)splay(u,0);//每次找到都要旋转到根部,保证复杂度
	return u;
}
void del(int x){
	int u=fd(x);//这一步是寻找值为x的节点,如果找到了就把该节点splay到根部
	if(!u)return;//没有节点就结束程序即可
	if(cnt[u]>1)cnt[u]--;//如果该节点有多个,就删除一个
	else{
		int v=ch[u][0];//开始找前驱
		if(!v){//没有前驱
			root=ch[u][1];//直接设置根节点为u的右儿子
			destory(root,u);//记得删除u与它右儿子之间的边
		}else{
			while(ch[v][1])v=ch[v][1];//找前驱
			splay(v,u);//把前驱splay到u的儿子位置
			u=ch[u][1];
			destory(v,root);//下面就是简单的断边和加边
			destory(u,root);
			connect(u,v,1);
			pushup(v);//记得更新一下v维护的信息
			root=v;//更新一下root
		}
	}
}

查询排名 r a n k ( x ) : rank(x): rank(x): 直接应用 f d ( x ) fd(x) fd(x)函数找到 x x x节点,(经过splay后)此时它位于根节点,因此根节点左边的都是比它更小的,返回 s z [ c h [ f d ( x ) ] [ 0 ] ] + 1 sz[ch[fd(x)][0]]+1 sz[ch[fd(x)][0]]+1即可。

int rankx(int x){
	return sz[ch[fd(x)][0]]+1;
}

找到第 k k k名的节点的值 f i n d k ( x ) : findk(x): findk(x): 写法非常容易懂,见代码吧:

int findk(int k){
	int u=root;
	while(u){
		if(sz[ch[u][0]]+cnt[u]>=k){
			if(sz[ch[u][0]]<k){
				splay(u,0);//记得要splay
				return val[u];
			}
			else u=ch[u][0];
		}else k-=sz[ch[u][0]]+cnt[u],u=ch[u][1];
	}
	return -1;//没有找到
}

寻找前驱/后继 p r e ( x ) / n x t ( x ) : pre(x)/nxt(x): pre(x)/nxt(x): 不好描述,看代码容易理解:

int pre(int x){
	int u=root,ans=-inf,id=0;
	while(u){
		if(val[u]<x && val[u]>ans)ans=val[u],id=u;
		u=ch[u][x>val[u]];
	}
	splay(id,0);//记得要splay
	return ans;
}
int nxt(int x){
	int u=root,ans=inf,id=0;
	while(u){
		if(val[u]>x && val[u]<ans)ans=val[u],id=u;
		u=ch[u][x>=val[u]];
	}
	splay(id,0);//记得要splay
	return ans;
}

下面给出完整的代码以供参考:

#include 
using namespace std;

const int maxn = 1e5+5;
const int inf = 2147483647;

int ch[maxn][2],val[maxn],cnt[maxn],sz[maxn],fa[maxn],root,tot;

void display(int u){//用于调试,功能是打印整棵树的所有节点信息
	if(!u)return;
	printf("%d,%d,%d,%d,%d,%d\n",u,val[u],cnt[u],sz[u],ch[u][0],ch[u][1]);
	display(ch[u][0]);
	display(ch[u][1]);
}
void pushup(int rt){
	sz[rt]=sz[ch[rt][0]]+sz[ch[rt][1]]+cnt[rt];
}
int dir(int u){
	return ch[fa[u]][1]==u;
}
void connect(int u,int f,int d){
	ch[f][d]=u;
	fa[u]=f;
}
void destory(int u,int f){
	ch[f][dir(u)]=0;
	fa[u]=0;
}
void rotate(int u){
	int f=fa[u],ff=fa[f],du=dir(u);
	if(!f)return;//已经是根节点
	int df=dir(f);
	connect(u,ff,df),connect(ch[u][du^1],f,du),connect(f,u,du^1);
	pushup(f),pushup(u);
}
void splay(int u,int rt){
	for(int f=fa[u];f!=rt;f=fa[u]){
		if(fa[f]!=rt)(dir(u)^dir(f))?rotate(u):rotate(f);
		rotate(u);
	}
	if(!rt)root=u;
}

void insert(int x){
	int u=root,f=0;
	while(u && val[u]!=x){
		f=u;u=ch[u][x>val[u]];
	}
	if(u)splay(u,0),cnt[u]++;
	else{
		u=++tot;
		val[u]=x;
		cnt[u]=sz[u]=1;
		connect(u,f,x>val[f]);
		splay(u,0);
	}
}
int fd(int x){
	int u=root;
	while(u && val[u]!=x){
		u=ch[u][x>val[u]];
	}
	if(u)splay(u,0);
	return u;
}
void del(int x){
	int u=fd(x);
	if(!u)return;
	if(cnt[u]>1)cnt[u]--;
	else{
		int v=ch[u][0];
		if(!v){
			root=ch[u][1];
			destory(root,u);
		}else{
			while(ch[v][1])v=ch[v][1];
			splay(v,u);
			u=ch[u][1];
			destory(v,root);
			destory(u,root);
			connect(u,v,1);
			pushup(v);
			root=v;
		}
	}
}

int rankx(int x){
	return sz[ch[fd(x)][0]]+1;
}
int findk(int k){
	int u=root;
	while(u){
		if(sz[ch[u][0]]+cnt[u]>=k){
			if(sz[ch[u][0]]<k){
				splay(u,0);
				return val[u];
			}
			else u=ch[u][0];
		}else k-=sz[ch[u][0]]+cnt[u],u=ch[u][1];
	}
	return -1;
}
int pre(int x){
	int u=root,ans=-inf,id=0;
	while(u){
		if(val[u]<x && val[u]>ans)ans=val[u],id=u;
		u=ch[u][x>val[u]];
	}
	splay(id,0);
	return ans;
}
int nxt(int x){
	int u=root,ans=inf,id=0;
	while(u){
		if(val[u]>x && val[u]<ans)ans=val[u],id=u;
		u=ch[u][x>=val[u]];
	}
	splay(id,0);
	return ans;
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		int op,x;
		scanf("%d%d",&op,&x);
		if(op==1){
			insert(x);
		}else if(op==2){
			del(x);
		}else if(op==3){
			printf("%d\n",rankx(x));
		}else if(op==4){
			printf("%d\n",findk(x));
		}else if(op==5){
			printf("%d\n",pre(x));
		}else if(op==6){
			printf("%d\n",nxt(x));
		}
	}
}

Treap

T r e a p Treap Treap中每个节点都有两个键值,第一个是权值,代表的是它所要记录的信息,第二个是附加值,用于平衡整个树的结构。
T r e a p Treap Treap既具有堆的性质又具有二叉查找树的性质,其中权值符合二叉查找树的性质,而附加值符合堆的性质,一般我们默认 T r e a p Treap Treap是一个小根堆。
T r e a p Treap Treap能够成为一颗平衡树最核心的原因是它的每个节点的附加值都是随机的,这样的话能够保证整颗树是均匀的,也就能够保证树的最大深度不会太深。

在后面的程序中我们用 v a l [ u ] , t a r [ u ] val[u],tar[u] val[u],tar[u]分别表示节点 u u u的权值和附加值。
下面是关于 T r e a p Treap Treap所需的一些基本变量声明:

int ch[maxn][2],cnt[maxn],sz[maxn],val[maxn],tar[maxn],fa[maxn],tot,root;

可以看到这些变量与 S p l a y Splay Splay的变量声明大同小异,唯一的区别在于多了一个 t a r tar tar用于维护整棵树的平衡状态。
下面先给出 i n s e r t insert insert d e l del del这两个核心操作的实现。

插入 i n s e r t ( x ) : insert(x): insert(x): T r e a p Treap Treap使用递归的方式插入一个节点,先找到将要插入的位置,然后我们直接插入一个 n e w n o d e newnode newnode并为这个节点分配一个随机的附加值,然后再回溯的过程中我们会不断检查父子的附加值之间的大小关系,如果不满足父亲小于等于儿子的附加值的话我们就 r o t a t e ( 儿 子 ) rotate(儿子) rotate()即可( r o t a t e rotate rotate的原理以及实现见 S p l a y Splay Splay的介绍),这样儿子就会替换父亲的位置,最终可以将整颗树修正为符合堆的性质。具体实现有一些小细节,详见注释:

void pushup(int rt){
	sz[rt]=sz[ch[rt][0]]+sz[ch[rt][1]]+cnt[rt];
}
int newnode(int f,int x){//创建新节点,权值为x,父亲为f
	val[++tot]=x;
	sz[tot]=cnt[tot]=1;
	tar[tot]=rnd();//随机分配一个附加值
	fa[tot]=f;
	return tot;
}
int dir(int u){
	return ch[fa[u]][1]==u;
}
void connect(int u,int f,int d){
	fa[u]=f;ch[f][d]=u;
}
void rotate(int u){//同Splay的旋转
	int f=fa[u],ff=fa[f],du=dir(u);
	if(!f)return;
	int df=dir(f);
	connect(u,ff,df),connect(ch[u][du^1],f,du),connect(f,u,du^1);
	pushup(f),pushup(u);
	if(!ff)root=u;
}
void insert(int &u,int f,int x){//我们需要记录父亲节点
	if(!u){//已经没有路走了
		u=newnode(f,x);//创建新节点代替当前空位置
		return;
	}
	sz[u]++;//插入肯定会增加sz
	if(val[u]==x){//遇到权值相同的节点就没必要新建节点了
		cnt[u]++;
	}else insert(ch[u][x>val[u]],u,x);//继续查找
	if(ch[u][0] && tar[u]>tar[ch[u][0]]){//比较u与其两个儿子的附加值,根据需要旋转
		rotate(ch[u][0]);
	}else if(ch[u][1] && tar[u]>tar[ch[u][1]]){
		rotate(ch[u][1]);
	}
}

删除 d e l ( x ) : del(x): del(x): 还是先搜索 x x x所在位置,如果能够找到一个节点权值为 x x x,若它的 c n t cnt cnt还满足大于 1 1 1,那么就 c n t − − cnt-- cnt即可,否则的话我们把这个节点向它的两个儿子中附加值较大的一个的方向旋转,这样能够让另一个附加值较小的儿子旋转到当前节点的位置,然后我们递归进入下一层继续考虑,这样能够在保持堆的性质的情况下将这个节点旋转到只有一个儿子(或没有儿子)的情况,这时候我们让它的儿子代替它即可(或置空)。代替的时候要注意维护 f a fa fa变量。这个细节更麻烦一点,详见代码:

void del(int &u,int x){
	if(!u)return;
	else if(val[u]==x){//找到了节点
		if(cnt[u]>1){
			cnt[u]--;
		}else if(ch[u][0] && ch[u][1]){//两个儿子都在的时候需要将当前节点旋转下去
			int v;
			if(val[ch[u][0]]<val[ch[u][1]]){//考虑两个儿子附加值的大小关系来决定谁替换当前位置
				v=ch[u][0];//一定要用这个中间变量,因为我们递归是引用传值
				rotate(v);//把v提上来,替换当前的u节点
				del(ch[v][1],x);//这样递归能够保证绝对的正确性,不能直接del(u,x)
			}else {
				v=ch[u][1];
				rotate(v);
				del(ch[v][0],x);
			}
		}else {//这时候可以删除u节点了
			int v=fa[u];//记录一下fa
			fa[u]=0;
			u=ch[u][0]+ch[u][1];//当前节点替换为它的儿子即可
			fa[u]=v;//记住要把儿子的父亲更新
			return;
		}
	}else del(ch[u][x>val[u]],x);//递归寻找合适节点
	pushup(u);//维护子树信息
}

有了两个核心操作,后面的操作都很简单了,除了 r a n k ( x ) rank(x) rank(x)以外的操作和 S p l a y Splay Splay是一模一样的,只不过每个操作最后都不需要 S p l a y Splay Splay,由于写法都很简单,下面就直接放出整体代码吧。

#include 
using namespace std;

const int maxn = 1e5+5;
const int inf = 2147483647;

int ch[maxn][2],cnt[maxn],sz[maxn],val[maxn],tar[maxn],fa[maxn],tot,root;

mt19937 rnd(time(0));

void display(int u){
	if(!u)return;
	printf("%d,%d,%d,%d,%d,%d,%d,%d\n",u,val[u],cnt[u],sz[u],fa[u],tar[u],ch[u][0],ch[u][1]);
	display(ch[u][0]);
	display(ch[u][1]);
}
void pushup(int rt){
	sz[rt]=sz[ch[rt][0]]+sz[ch[rt][1]]+cnt[rt];
}
int newnode(int f,int x){
	val[++tot]=x;
	sz[tot]=cnt[tot]=1;
	tar[tot]=rnd();
	fa[tot]=f;
	return tot;
}
int dir(int u){
	return ch[fa[u]][1]==u;
}
void connect(int u,int f,int d){
	fa[u]=f;ch[f][d]=u;
}
void rotate(int u){
	int f=fa[u],ff=fa[f],du=dir(u);
	if(!f)return;
	int df=dir(f);
	connect(u,ff,df),connect(ch[u][du^1],f,du),connect(f,u,du^1);
	pushup(f),pushup(u);
	if(!ff)root=u;
}
void insert(int &u,int f,int x){
	if(!u){
		u=newnode(f,x);
		return;
	}
	sz[u]++;
	if(val[u]==x){
		cnt[u]++;
	}else insert(ch[u][x>val[u]],u,x);
	if(ch[u][0] && tar[u]>tar[ch[u][0]]){
		rotate(ch[u][0]);
	}else if(ch[u][1] && tar[u]>tar[ch[u][1]]){
		rotate(ch[u][1]);
	}
}
void del(int &u,int x){
	if(!u)return;
	else if(val[u]==x){
		if(cnt[u]>1){
			cnt[u]--;
		}else if(ch[u][0] && ch[u][1]){
			int v;
			if(val[ch[u][0]]<val[ch[u][1]]){
				v=ch[u][0];
				rotate(v);
				del(ch[v][1],x);
			}else {
				v=ch[u][1];
				rotate(v);
				del(ch[v][0],x);
			}
		}else {
			int v=fa[u];
			fa[u]=0;
			u=ch[u][0]+ch[u][1];
			fa[u]=v;
			return;
		}
	}else del(ch[u][x>val[u]],x);
	pushup(u);
}

int rankx(int x){
	int u=root,ans=0;
	while(u){
		if(val[u]<x)ans+=sz[ch[u][0]]+cnt[u],u=ch[u][1];
		else u=ch[u][0];
	}
	return ans+1;
}
int findk(int k){
	int u=root;
	while(u){
		if(sz[ch[u][0]]+cnt[u]>=k){
			if(sz[ch[u][0]]<k)return val[u];
			else u=ch[u][0];
		}else k-=sz[ch[u][0]]+cnt[u],u=ch[u][1];
	}
	return -1;
}
int pre(int x){
	int u=root,ans=-inf;
	while(u){
		if(val[u]<x && val[u]>ans)ans=val[u];
		u=ch[u][x>val[u]];
	}
	return ans;
}
int nxt(int x){
	int u=root,ans=inf;
	while(u){
		if(val[u]>x && val[u]<ans)ans=val[u];
		u=ch[u][x>=val[u]];
	}
	return ans;
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		int op,x;
		scanf("%d%d",&op,&x);
		if(op==1){
			insert(root,0,x);
		}else if(op==2){
			del(root,x);
		}else if(op==3){
			printf("%d\n",rankx(x));
		}else if(op==4){
			printf("%d\n",findk(x));
		}else if(op==5){
			printf("%d\n",pre(x));
		}else if(op==6){
			printf("%d\n",nxt(x));
		}
	}
}

fhq Treap

f h q    T r e a p fhq\;Treap fhqTreap又叫做范浩强 T r e a p Treap Treap,是范浩强所发明的,以其简洁的编码方式闻名 O I OI OI界,它是一种无旋树,相对于有旋树原理会更加直观易懂,代码也更加好写。这里给出它的相关原理及实现。

f h q    T r e a p fhq\;Treap fhqTreap最核心的操作就是 m e r g e merge merge(合并)和 s p l i t split split(分裂)操作,能够将两颗平衡树合并或者将一棵平衡树分裂成两个。由于这种平衡树其实也是一种 T r e a p Treap Treap,因此它也主要靠随机性来保证树的平衡,即每个节点除了权值外都还需要带一个附加值。

合并 m e r g e ( u , v ) : merge(u,v): merge(u,v): 将两颗根节点为 u , v u,v u,v的树合并为一棵,并返回新树的根节点。需要强调的是,合并操作的前提是 u u u树中节点的最大权值不大于 v v v树中的最小权值。我们依次比较 u , v u,v u,v子树节点附加值的大小,取附加值小的作为当前的节点,如果是 u u u树的话,那么该节点的左子树其实就不用管了,因为该节点的右子树和 v v v剩余的节点的权值一定都是不小于该节点左子树中的最大权值的, v v v树同理,这样描述可能不容易理解,看代码会更容易理解:

int merge(int u,int v){//保证u树的最大权值小于等于v树的最小权值 
	if(!u || !v)return u+v;//如果有个节点是空的直接返回即可
	if(tar[u]<tar[v]){//取附加值小的作为当前树的根节点
		ch[u][1]=merge(ch[u][1],v);//将右子树与v树继续递归处理
		pushup(u);
		return u;
	}
	ch[v][0]=merge(u,ch[v][0]);
	pushup(v);
	return v;
}

分裂 s p l i t : split: split: 分裂操作有两种写法,第一种是将一颗以 r t rt rt为根节点的树分裂成两部分,使得第一部分以 x x x为根节点并且树的所有节点权值都不大于 v v v,第二部分以 y y y为根节点并且树的所有节点权值都大于 v v v,这里用函数 s p l i t v ( r t , v , x , y ) splitv(rt,v,x,y) splitv(rt,v,x,y)表示,其中 x , y x,y x,y都是引用传值。第二种是将一颗以 r t rt rt为根节点的树分裂成两部分,使得第一部分以 x x x为根节点并且树的所有节点权值的排名都不大于 k k k,第二部分以 y y y为根节点并且树的所有节点权值都大于 k k k,这里用函数 s p l i t k ( r t , k , x , y ) splitk(rt,k,x,y) splitk(rt,k,x,y)表示,其中 x , y x,y x,y都是引用传值,权值的排名指的是从小到大的排名。写法很容易看懂就不多说了。

void splitv(int rt,int v,int &x,int &y){
	if(!rt){
		x=y=0;return;
	}
	if(val[rt]<=v){
		x=rt;
		splitv(ch[rt][1],v,ch[x][1],y);
	}else{
		y=rt;
		splitv(ch[rt][0],v,x,ch[y][0]);
	}
	pushup(rt);
}
void splitk(int rt,int k,int &x,int &y){
	if(!rt){
		x=y=0;return;
	}
	if(sz[ch[rt][0]]+1<=k){
		x=rt;splitk(ch[rt][1],k-sz[ch[rt][0]]-1,ch[x][1],y);
	}else{
		y=rt;splitk(ch[rt][0],k,x,ch[y][0]);
	}
	pushup(rt);
}

有了这两个操作,我们就能很方便的实现其它基本操作了。

插入 i n s e r t ( v ) : insert(v): insert(v): 考虑先把平衡树按照权值 v v v分裂为两部分,然后按照第一部分,新节点(权值为 v v v,附加值随机),第二部分的顺序依次合并即可。

int newnode(int x){
	val[++tot]=x;sz[tot]=1;
	tar[tot]=rnd();
	return tot;
}
void insert(int v){
	if(!root){
		root=newnode(v);
		return;
	} 
	int x,y;
	splitv(root,v,x,y);
	root=merge(merge(x,newnode(v)),y);
}

删除 d e l ( v ) : del(v): del(v): 考虑将原树分成三部分,第一部分权值都小于 v v v,第二部分权值都等于 v v v,第三部分权值都大于 v v v,然后第二部分我们把它的根节点的两个儿子合并,取一个作为第二部分新的根节点,再将第一、第二、第三部分依次合并即可。

void del(int v){
	int x,y,z;
	splitv(root,v,x,z);
	splitv(x,v-1,x,y);
	root=merge(merge(x,merge(ch[y][0],ch[y][1])),z);
}

获取权值 v v v的排名 r a n k ( v ) : rank(v): rank(v): 考虑按照权值 v − 1 v-1 v1分裂为两颗树,第一部分的大小加一就是 v v v对应的排名。

int rankx(int v){
	int x,y,ans;
	splitv(root,v-1,x,y);
	ans=sz[x]+1;
	root=merge(x,y);
	return ans;
}

查找第 k k k名权值 f i n d k ( k ) : findk(k): findk(k): 将原树直接按照名次 k k k分裂即可,第一部分中的最大权值就是我们要找的答案。

int maxt(int rt){//返回rt为根的树中的最大权值
	while(ch[rt][1])rt=ch[rt][1];
	return val[rt];
}
int mint(int rt){//返回rt为根的树中的最小权值
	while(ch[rt][0])rt=ch[rt][0];
	return val[rt];
}
int findk(int k){
	int x,y,ans;
	splitk(root,k,x,y);
	ans=maxt(x);
	root=merge(x,y);
	return ans;
}

前驱 p r e ( v ) : pre(v): pre(v): 将树按照权值 v − 1 v-1 v1分裂,第一部分的最大权值就是答案。

int pre(int v){
	int x,y,ans;
	splitv(root,v-1,x,y);
	ans=maxt(x);
	root=merge(x,y);
	return ans;
}

前驱 n x t ( v ) : nxt(v): nxt(v): 将树按照权值 v v v分裂,第二部分的最小权值就是答案。

int nxt(int v){
	int x,y,ans;
	splitv(root,v,x,y);
	ans=mint(y);
	root=merge(x,y);
	return ans;
}

以上就是 f h q    T r e a p fhq\;Treap fhqTreap的基本操作,需要值得注意的是,每次分裂以后都要记得再合并,虽然 f h q    T r e a p fhq\;Treap fhqTreap效率不算高,但是原理简单易懂,而且特别好写,细节相较于其它平衡树要少。
下面给出完整代码。

#include 
using namespace std;

const int maxn = 1e5+5;

int ch[maxn][2],tot,sz[maxn],val[maxn],tar[maxn],root;
mt19937 rnd(time(0));
void display(int u){
	if(!u)return;
	printf("%d,%d,%d,%d,%d,%d\n",u,val[u],tar[u],sz[u],ch[u][0],ch[u][1]); 
	display(ch[u][0]);
	display(ch[u][1]);
}
void pushup(int rt){
	sz[rt]=sz[ch[rt][0]]+sz[ch[rt][1]]+1;
}
int merge(int u,int v){//保证u树的最大权值小于等于v树的最小权值 
	if(!u || !v)return u+v;
	if(tar[u]<tar[v]){
		ch[u][1]=merge(ch[u][1],v);
		pushup(u);
		return u;
	}
	ch[v][0]=merge(u,ch[v][0]);
	pushup(v);
	return v;
}
void splitv(int rt,int v,int &x,int &y){
	if(!rt){
		x=y=0;return;
	}
	if(val[rt]<=v){
		x=rt;
		splitv(ch[rt][1],v,ch[x][1],y);
	}else{
		y=rt;
		splitv(ch[rt][0],v,x,ch[y][0]);
	}
	pushup(rt);
}
void splitk(int rt,int k,int &x,int &y){
	if(!rt){
		x=y=0;return;
	}
	if(sz[ch[rt][0]]+1<=k){
		x=rt;splitk(ch[rt][1],k-sz[ch[rt][0]]-1,ch[x][1],y);
	}else{
		y=rt;splitk(ch[rt][0],k,x,ch[y][0]);
	}
	pushup(rt);
}
int newnode(int x){
	val[++tot]=x;sz[tot]=1;
	tar[tot]=rnd();
	return tot;
}
void insert(int v){
	if(!root){
		root=newnode(v);
		return;
	} 
	int x,y;
	splitv(root,v,x,y);
	root=merge(merge(x,newnode(v)),y);
}
void del(int v){
	int x,y,z;
	splitv(root,v,x,z);
	splitv(x,v-1,x,y);
	root=merge(merge(x,merge(ch[y][0],ch[y][1])),z);
}
int maxt(int rt){
	while(ch[rt][1])rt=ch[rt][1];
	return val[rt];
}
int mint(int rt){
	while(ch[rt][0])rt=ch[rt][0];
	return val[rt];
}
int rankx(int v){
	int x,y,ans;
	splitv(root,v-1,x,y);
	ans=sz[x]+1;
	root=merge(x,y);
	return ans;
}
int findk(int k){
	int x,y,ans;
	splitk(root,k,x,y);
	ans=maxt(x);
	root=merge(x,y);
	return ans;
}
int pre(int v){
	int x,y,ans;
	splitv(root,v-1,x,y);
	ans=maxt(x);
	root=merge(x,y);
	return ans;
}
int nxt(int v){
	int x,y,ans;
	splitv(root,v,x,y);
	ans=mint(y);
	root=merge(x,y);
	return ans;
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		int op,x;
		scanf("%d%d",&op,&x);
		if(op==1){
			insert(x);
		}else if(op==2){
			del(x);
		}else if(op==3){
			printf("%d\n",rankx(x));
		}else if(op==4){
			printf("%d\n",findk(x));
		}else if(op==5){
			printf("%d\n",pre(x));
		}else if(op==6){
			printf("%d\n",nxt(x));
		}
	}
}

替罪羊树

替罪羊树也是一种无旋树,其思想也很简单,就是暴力重构。

虽然是暴力思想,但是其效率可不能小觑,事实上替罪羊树速度与 f h q    T r e a p fhq\; Treap fhqTreap S p l a y Splay Splay不相上下。

在替罪羊树中,节点分为两种:虚节点实节点,虚节点代表这个节点上的值其实不存在,只是还保留在树中以节点的形式存在,实节点则是实实在在存在的节点,它所拥有的值也是实际存在的。例如我们先插入一个节点 1 1 1,之后某个时刻又把它删除,但我们不会直接删除 1 1 1,而是将它置为虚节点。

首先做一些变量声明, s z [ u ] sz[u] sz[u]表示一 u u u为根的树储存的值的个数, c n t [ u ] cnt[u] cnt[u]表示 u u u这个节点储存的相同值的个数(虚节点那么 c n t [ u ] = 0 cnt[u]=0 cnt[u]=0,否则不为零), n u m [ u ] num[u] num[u]表示以 u u u为根的子树的所有节点个数(包括实节点和虚节点), r n u m [ u ] rnum[u] rnum[u]表示以 u u u为根的子树的实节点的个数, c u r [ ] cur[] cur[]数组用于拍扁重构储存树节点用, c u r t o t curtot curtot代表被拍扁的树的实节点个数。

所有变量声明如下(大多数变量与前面的平衡树相同就不做解释)

int ch[maxn][2],tot,sz[maxn],cnt[maxn],num[maxn],rnum[maxn],val[maxn],cur[maxn],curtot,root;

替罪羊树最核心的操作就是暴力重构,这个函数被定义为 c h e c k ( u , x ) check(u,x) check(u,x),其中 u u u是引用传值,它的作用在于检查从 u u u节点出发到达权值为 x x x的节点的路径上是否存在一个节点 v v v,满足以节点 v v v为根的树过于不平衡,那么我们就将 v v v这颗子树拍扁重构。这个函数被放置在插入和删除操作的末尾,用于维护树的平衡,可以证明最终的总复杂度仍然是 O ( l o g n ) O(logn) O(logn)级别的。

下面解释一下什么叫做过于不平衡,以及如何实现对一颗树的拍扁重构

如果一个节点 u u u是不平衡的,要么它的两个儿子所代表的子树中的其中一个会过大,要么 u u u的子树中的虚节点过多。我们设置一个常量值 α \alpha α,它代表如果 u u u的某个儿子的大小占 u u u的总大小超过 α \alpha α或者 u u u的子树中的实节点占总结点比例小于 α \alpha α那么这颗树就是过于不平衡的,我们将考虑将 u u u为根的子树拍扁重构。写成代码如下所示:

bool unbalanced(int u){
	return num[ch[u][0]]>0.75*num[u] || num[ch[u][1]]>0.75*num[u] || rnum[u]<0.75*num[u];
}

拍扁重构的过程很暴力,假设 u u u为根的子树过于不平衡,那么我们就将 u u u为根的子树中的所有实节点按照权值从小到大排列在一个数组中,然后再用类似线段树的方法建立一颗均匀的新树。这里实现的时候我们用到了 c u r cur cur作为辅助数组,根据平衡树的特点,我们只需要中序遍历就可以保证权值从小到大被遍历,因此只需要按照中序遍历的顺序将节点一个个放在 c u r cur cur数组中即可。具体的实现细节详见代码:

void pushup(int rt){
	sz[rt]=sz[ch[rt][0]]+sz[ch[rt][1]]+cnt[rt];
	num[rt]=num[ch[rt][0]]+num[ch[rt][1]]+1;
	rnum[rt]=rnum[ch[rt][0]]+rnum[ch[rt][1]]+(cnt[rt]>0);
}
void destory(int u){//先拍扁,用中序遍历将实节点储存在cur数组中
	if(!u)return;
	destory(ch[u][0]);
	if(cnt[u])cur[curtot++]=u;//判断是否为实节点
	destory(ch[u][1]);
}
int build(int l,int r){//重构,将cur[l~r]的节点重构一棵树
	if(l>r)return 0;
	int mid=l+r>>1,u=cur[mid];//取中间节点为根
	ch[u][0]=build(l,mid-1);//左子树的都比根节点权值小
	ch[u][1]=build(mid+1,r);//右子树的都必根节点权值大
	pushup(u);//更新一下节点维护的信息
	return u;
}

有了过于不平衡拍扁重构 的实现方法,我们就可以写出 c h e c k ( u , x ) check(u,x) check(u,x)函数了,它需要找到第一个从 u u u到权值为 x x x的节点路径上的不平的节点,然后将它的子树拍扁重构,如果找不到就返回即可。代码实现如下:

void check(int &u,int x){
	if(!u)return;
	if(unbalanced(u)){//如果不平衡就拍扁重构整颗子树
		curtot=0;
		destory(u);//先拍扁
		u=build(0,curtot-1);//再重构
		return;
	}else if(val[u]!=x)check(ch[u][x>val[u]],x);//继续找,但是不能超过x
	pushup(u);//更新一下节点维护信息
}

有了 c h e c k check check函数我们的复杂度就有保证了,下面就可以比较方便地实现 i n s e r t insert insert d e l del del操作了。

插入 i n s e r t ( x ) : insert(x): insert(x): 这个操作比较简单,直接找到空位然后插入新节点即可,如果节点已经存在那么 c n t + + cnt++ cnt++即可,不过我们需要再最后 c h e c k check check一下。这里使用 l i n s lins lins辅助 v e c t o r vector vector来储存整个寻找路径,方便 p u s h u p pushup pushup

void insert(int x){
	int u=root,f=0;lins.clear();lins.push_back(u);
	while(u && val[u]!=x){
		f=u;u=ch[u][x>val[u]];
		lins.push_back(u);
	}
	if(u){//如果节点存在
		cnt[u]++;
		for(int i=lins.size()-1;i>=0;--i)pushup(lins[i]);//先维护一下信息,要倒叙更新
		check(root,x);//check一下
	}else{//如果不存在
		u=newnode(x);//新建节点
		if(!f)root=u;else{//若f不存在那就更新root,否则我们还要设置一下儿子并check一下
			ch[f][x>val[f]]=u;lins.pop_back();//这里要注意pop一下,因为lins最后一个值是0,千万不能pushup 0,因为它代表空
			for(int i=lins.size()-1;i>=0;--i)pushup(lins[i]);
			check(root,x);
		}
	}
}

删除 d e l ( x ) : del(x): del(x): 删除操作也很简单,找到节点后不用讨论 c n t cnt cnt大小,直接 c n t − − cnt-- cnt即可,然后 c h e c k check check一下。

void del(int x){
	int u=root,f=0;lins.clear(),lins.push_back(u);
	while(u && val[u]!=x){
		f=u;u=ch[u][x>val[u]];
		lins.push_back(u);
	}
	if(!u || !cnt[u])return;
	cnt[u]--;
	for(int i=lins.size()-1;i>=0;--i)pushup(lins[i]);
	check(root,x);
}

寻找 x x x排名 r a n k ( x ) rank(x) rank(x)找到第 k k k大元素值 f i n d k ( k ) findk(k) findk(k) 这两个操作跟前面的平衡树没有任何区别,就不多说了。

int rankx(int x){
	int u=root,ans=0;
	while(u){
		if(val[u]<x)ans+=sz[ch[u][0]]+cnt[u];
		u=ch[u][x>val[u]];
	}
	return ans+1;
}
int findk(int k){
	int u=root;
	while(u){
		if(sz[ch[u][0]]+cnt[u]>=k){
			if(sz[ch[u][0]]<k)return val[u];
			else u=ch[u][0];
		}else k-=sz[ch[u][0]]+cnt[u],u=ch[u][1]; 
	}
	return -1;
}

前驱 p r e ( x ) : pre(x): pre(x): 由于替罪羊树中有很多的虚节点,这些节点是不存在的,即我们不能把它们算进答案的考虑范围内,我们不能再用以前的方法去遍历,如果直接遍历会非常麻烦,因此我们用到一个小技巧,即 f i n d k ( r a n k ( x ) − 1 ) findk(rank(x)-1) findk(rank(x)1)即可。

int pre(int x){
	return findk(rankx(x)-1);
}

后继 n x t ( x ) : nxt(x): nxt(x): 类似于前驱的实现,也是一个小技巧。

int nxt(int x){
	return findk(rankx(x+1));
} 

下面给出整体的代码实现:

#include 
using namespace std;

const int maxn = 1e5+5;
const int inf = 2147483647;

int ch[maxn][2],tot,sz[maxn],cnt[maxn],num[maxn],rnum[maxn],val[maxn],cur[maxn],curtot,root;
vector<int>lins;
void pushup(int rt){
	sz[rt]=sz[ch[rt][0]]+sz[ch[rt][1]]+cnt[rt];
	num[rt]=num[ch[rt][0]]+num[ch[rt][1]]+1;
	rnum[rt]=rnum[ch[rt][0]]+rnum[ch[rt][1]]+(cnt[rt]>0);
}
void destory(int u){
	if(!u)return;
	destory(ch[u][0]);
	if(cnt[u])cur[curtot++]=u;
	destory(ch[u][1]);
}
int build(int l,int r){
	if(l>r)return 0;
	int mid=l+r>>1,u=cur[mid];
	ch[u][0]=build(l,mid-1);
	ch[u][1]=build(mid+1,r);
	pushup(u);
	return u;
}
bool unbalanced(int u){
	return num[ch[u][0]]>0.75*num[u] || num[ch[u][1]]>0.75*num[u] || rnum[u]<0.75*num[u];
}
void check(int &u,int x){
	if(!u)return;
	if(unbalanced(u)){
		curtot=0;
		destory(u);
		u=build(0,curtot-1);
		return;
	}else if(val[u]!=x)check(ch[u][x>val[u]],x);
	pushup(u);
}
int newnode(int x){
	++tot;
	sz[tot]=num[tot]=rnum[tot]=cnt[tot]=1;
	val[tot]=x;
	return tot;
}
void insert(int x){
	int u=root,f=0;lins.clear();lins.push_back(u);
	while(u && val[u]!=x){
		f=u;u=ch[u][x>val[u]];
		lins.push_back(u);
	}
	if(u){
		cnt[u]++;
		for(int i=lins.size()-1;i>=0;--i)pushup(lins[i]);
		check(root,x);
	}else{
		u=newnode(x);
		if(!f)root=u;else{
			ch[f][x>val[f]]=u;lins.pop_back();
			for(int i=lins.size()-1;i>=0;--i)pushup(lins[i]);
			check(root,x);
		}
	}
}
void del(int x){
	int u=root,f=0;lins.clear(),lins.push_back(u);
	while(u && val[u]!=x){
		f=u;u=ch[u][x>val[u]];
		lins.push_back(u);
	}
	if(!u || !cnt[u])return;
	cnt[u]--;
	for(int i=lins.size()-1;i>=0;--i)pushup(lins[i]);
	check(root,x);
}
int rankx(int x){
	int u=root,ans=0;
	while(u){
		if(val[u]<x)ans+=sz[ch[u][0]]+cnt[u];
		u=ch[u][x>val[u]];
	}
	return ans+1;
}
int findk(int k){
	int u=root;
	while(u){
		if(sz[ch[u][0]]+cnt[u]>=k){
			if(sz[ch[u][0]]<k)return val[u];
			else u=ch[u][0];
		}else k-=sz[ch[u][0]]+cnt[u],u=ch[u][1]; 
	}
	return -1;
}
int pre(int x){
	return findk(rankx(x)-1);
}
int nxt(int x){
	return findk(rankx(x+1));
} 
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		int op,x;
		scanf("%d%d",&op,&x);
		if(op==1){
			insert(x);
		}else if(op==2){
			del(x); 
		}else if(op==3){
			printf("%d\n",rankx(x));
		}else if(op==4){
			printf("%d\n",findk(x));
		}else if(op==5){
			printf("%d\n",pre(x));
		}else if(op==6){
			printf("%d\n",nxt(x));
		}
	}	
} 

红黑树

红黑树的节点都要么是红色要么是黑色(废话 ),并且红黑树也是一颗二叉树,每颗红黑树都满足下面四条性质:

  1. 每个红色节点的儿子都一定是黑色节点
  2. 每个黑色节点的儿子可以是红色也可以黑色
  3. 从根节点出发到达每个叶子节点的黑色节点个数一样多
  4. 根节点是黑色节点

可以证明满足以上性质的树,深度最大的叶子节点的深度不会超过深度最小的叶子节点的两倍,这样的树显然是比较平衡的。这里简单证明一下,我们考虑一条到叶子结点的路径上的黑色节点数目是确定的,红色节点最多等于黑色节点的数目,最少等于零,因此一条路径的最大长度是它的最短长度的两倍。

现在考虑如何在插入和删除节点的过程中维护以上性质,首先是插入操作。

插入 i n s e r t ( x ) : insert(x): insert(x): 我们令插入的节点是红色,那么可能会违背性质 1 、 4 1、4 14,首先特判插入的节点是否是根节点,如果是根节点,那么改成黑色即可。如果不是根节点,那么只可能违背性质 1 1 1,要违背性质 1 1 1,那么该节点的父亲一定也是红色节点,下面考虑如何修正。
设当前节点为 u u u,其父亲节点为 f f f,其祖父节点为 f f ff ff,父亲的兄弟节点为 v v v,画成图就是以下样子( u u u节点既可以是 f f f的左儿子也可以是右儿子,下图画的是右儿子的情况, v v v节点少画了一个分叉):
acm-【平衡树】学习笔记(Splay,Treap,fhq Treap,替罪羊树,红黑树,avl tree,B树,B+树)_第3张图片
首先 u u u f f f都是红色节点,然后由于除去节点 u u u的子树以外的部分都是平衡的,因此都符合红黑树的性质,因此性质 1 1 1一定满足,因此 f f ff ff一定是黑色节点,而 v v v节点颜色未知,我们需要分类讨论。

一、 v v v节点红色
此时我们画出图长下面这样( v v v节点少画了一个分叉):
acm-【平衡树】学习笔记(Splay,Treap,fhq Treap,替罪羊树,红黑树,avl tree,B树,B+树)_第4张图片
当然 u u u还可以是 f f f的左儿子,不过在 v v v是红色的时候两者没有差别,因为此时我们只需要让 f f ff ff变成红色, f 、 v f、v fv变成黑色就行了,容易发现作出这样的改动后,性质 3 3 3不会被违背,并且性质 1 1 1也在以 f f ff ff为根的子树中得到了修正。不过 f f ff ff此时变成了红色,它可能会跟它的父亲节点发生冲突,因此我们还要递归地自底向上检查下去才行。

二、 v v v节点黑色
此时的情况如下图所示( v v v节点少画了一个分叉):
acm-【平衡树】学习笔记(Splay,Treap,fhq Treap,替罪羊树,红黑树,avl tree,B树,B+树)_第5张图片
上图是 u u u f f f的右儿子的情况,考虑将 f f f旋转到 f f ff ff的位置,并且把 f f f f f ff ff颜色交换,变成如下图所示:
acm-【平衡树】学习笔记(Splay,Treap,fhq Treap,替罪羊树,红黑树,avl tree,B树,B+树)_第6张图片
当然还有 v v v f f f左儿子的情况,我们作出以下变换:
acm-【平衡树】学习笔记(Splay,Treap,fhq Treap,替罪羊树,红黑树,avl tree,B树,B+树)_第7张图片
容易看出我们还是保证了红黑树的性质。

这样我们的插入操作就维护好了,以上就是插入新节点的所有情况,代码相对不算难写,下面先给出 c h e c k 1 ( u ) check1(u) check1(u)函数,代表检查 u u u节点和其父亲节点是否违背性质 1 1 1,然后给予修正,有了 c h e c k 1 ( u ) check1(u) check1(u)函数,那么插入操作也不难写了,直接找到插入位置,插入后修正即可,回溯的过程中还要不断检查修正,事实上我们可以发现只有情况一是会存在连续修正的情况,第二种情况下 f f ff ff节点即使修正后变成 u u u节点仍然保持了黑色,因此它不可能违背性质 1 1 1,它的祖先更不可能违背(我们保证了在插入新节点之前的整棵树都是符合红黑树性质的)。下面给出具体代码, c o l [ ] col[] col[]储存节点颜色, 0 0 0代表黑色, 1 1 1代表红色,有一些实现细节见代码注释。

void rotate(int u){
	int f=fa[u],ff=fa[f],du=dir(u);
	if(!f)return;
	int df=dir(f);
	connect(u,ff,df),connect(ch[u][du^1],f,du),connect(f,u,du^1);
	pushup(f),pushup(u);
	if(f==root)root=u;//这一条语句非常重要,最好写有旋平衡树都加上
}
void check1(int u){
	if(!u || !col[u])return;//u不为红色就不需要修正
	if(root==u){//当u是根节点的时候需要特判
		col[u]^=1;
	}else if(col[fa[u]]){//否则考虑父亲是否为红色来决定是否修正
		int f=fa[u];
		int df=dir(f),ff=fa[f],du=dir(u),v=ch[ff][df^1];
		if(col[v]){//情况一
			col[f]=col[v]=0;
			col[ff]=1;
		}else{//情况二
			if(du==df){//情况二中的第一种情况
				rotate(f);
				col[ff]=1,col[f]=0;
			}else{//情况二中的第二种情况
				rotate(u);
				rotate(u);
				col[ff]=1,col[u]=0;
			}
		}
	}
} 
void insert(int &u,int f,int x){
	if(!u){
		u=newnode(f,x);
		check1(u);//建立新节点然后进行第一次检查修正
		return;
	}
	++sz[u];
	if(x>=val[u])insert(ch[u][1],u,x);
	else insert(ch[u][0],u,x);
	check1(u);//回溯的时候进行检查修正
}

删除 d e l ( x ) : del(x): del(x): 这个操作就比较复杂了,我们先像二叉查找树一样去删除一个节点,也就是找到值为 x x x的节点,然后如果它有右儿子,就直接找它的直接后继节点 n t nt nt,然后将 n t nt nt与当前节点 u u u v a l val val交换一下(注意不需要交换颜色),我们递归到 n t nt nt节点继续找,当然如果它没有右儿子,直接拿它的左儿子作为 n t nt nt递归进行操作即可。

现在我们假设递归到了最底层,也就是两个儿子都为空的时候,这时候如果这个节点是红色,直接删除即可,否则直接删除就会违背性质 3 3 3,这时候我们引入 c h e c k 2 ( u ) check2(u) check2(u)函数对节点 u u u进行修正即可,不过这里的参数 u u u代表的是 u u u的子树已经符合红黑树性质且 u u u是黑色节点,但是从根节点到 u u u子树叶子的任意一条路径的黑色节点数目都比到非 u u u子树叶子节点的路径的黑色节点数少一,我们需要进行修正。

我们设 f f f u u u的父亲节点, v v v u u u的兄弟节点,现在要修正 u u u节点(也就是说 u u u的子树每条路径黑色节点偏少,并且路径上黑色节点都比其兄弟节点少一)那么一共有四种情况。
acm-【平衡树】学习笔记(Splay,Treap,fhq Treap,替罪羊树,红黑树,avl tree,B树,B+树)_第8张图片
上图是情况一,考察一下修正的正确性,设 d [ r t ] d[rt] d[rt]表示从以 r t rt rt为根的子树的根出发到达任意叶子节点会经过的黑色节点数目(如果子树满足红黑树性质,那么到任意叶子节点的黑色节点数目是相同的),那么一开始 d [ u ] + 1 = d [ v ] = d [ l s ] = d [ r s ] d[u]+1=d[v]=d[ls]=d[rs] d[u]+1=d[v]=d[ls]=d[rs],并且若 u u u子树未删除节点的时候(此时以 f f f为根节点的子树是一颗红黑树) d [ u ] = d [ l s ] + 1 = d [ r s ] + 1 d[u]=d[ls]+1=d[rs]+1 d[u]=d[ls]+1=d[rs]+1。经过左旋后, d [ l s ] 、 d [ r s ] d[ls]、d[rs] d[ls]d[rs] d [ u ] d[u] d[u]其实是没有变化的,可以发现 d [ u ] + 1 = d [ l s ] d[u]+1=d[ls] d[u]+1=d[ls]仍然满足,而从 v v v出发到达 l s ls ls子树中的叶子节点所经黑色节点数目与从 v v v出发到达 r s rs rs子树中的叶子节点所经黑色节点数目是一样的,而且这个值等于未修正前的 d [ v ] + 1 d[v]+1 d[v]+1,也就是说这些路径的长度都是正常的长度,只有到达 u u u子树的叶子节点的长度偏短。乍一看似乎没有修正好,不过我们发现此时比较的对象发生了变化,即 u u u所面临的情况恰好是 f f f红色,兄弟节点黑色” 的情况,这样我们可以转化到别的情况进行考虑,此时只要考虑以 f f f为根的这颗子树的修正即可,而 v v v替代了原来 f f f的位置,并且不需要考虑它了。
现在考虑第二种情况。
acm-【平衡树】学习笔记(Splay,Treap,fhq Treap,替罪羊树,红黑树,avl tree,B树,B+树)_第9张图片
上图是第二种情况,我们考察一下 d d d的变化,容易发现经过修正以 f f f为根的整棵树都满足了红黑树的性质,不过 d [ f ] d[f] d[f]相较于未删除节点前少了 1 1 1,这意味着我们要把 f f f当作新的 u u u进行修正。

acm-【平衡树】学习笔记(Splay,Treap,fhq Treap,替罪羊树,红黑树,avl tree,B树,B+树)_第10张图片
上图是第三种情况,不难看出交换一下颜色就能保证以 f f f为根的子树满足红黑树的性质,并且 d d d值相较于删除节点前没有变化。

下面给出最后一种情况:
acm-【平衡树】学习笔记(Splay,Treap,fhq Treap,替罪羊树,红黑树,avl tree,B树,B+树)_第11张图片
类似于前面的分析方法, 我们不难发现以上修正能够让新子树拥有红黑树的性质。

根据以上四种情况的讨论,我们可以写出如下代码,具体细节见注释:

int getred(int u,int d){//辅助函数,用于获取节点u的红儿子,并优先返回d儿子
	if(ch[u][d] && col[ch[u][d]])return d;
	else if(ch[u][d^1] && col[ch[u][d^1]])return d^1;
	return -1;//代表没有红色儿子
}
void check2(int u){//删除节点u并进行修正
	int f=fa[u],ff=fa[f];
	if(u==root){//根节点特判
		root=0;
		return;
	}
	int du=dir(u),v=ch[f][du^1];
	ch[f][du]=0;fa[u]=0;//首先把u删除
	while(true){
		if(!v)return;
		if(!col[f] && col[v]){//情况1,f黑,v红
			swap(col[f],col[v]);
			rotate(v);
			v=ch[f][du^1];
		}else if(!col[f] && !col[v] && getred(v,0)==-1){//情况2,f黑,v黑,v无红儿子
			col[v]=1;
			u=f;
			if(u==root)return;
			f=fa[u],du=dir(u),v=ch[f][du^1];
		}else if(col[f] && !col[v] && getred(v,0)==-1){//情况3,f红,v黑,v无红儿子
			swap(col[f],col[v]);
			return;
		}else {//情况4,v黑,v存在至少一个红儿子
			int dv=dir(v);
			if(getred(v,dv)==dv){
				col[v]=col[f];
				col[ch[v][dv]]=col[f]=0;
				rotate(v);
			}else{
				col[ch[v][dv^1]]=col[f];
				col[f]=0;
				int son=ch[v][dv^1];
				rotate(son);
				rotate(son);
			}
			return;
		}
	}
}
int getnxt(int u){//获取u的后继,如果没有后继就返回前驱
	if(!ch[u][1]){
		u=ch[u][0];
		if(!u)return 0;
		while(ch[u][1])u=ch[u][1];
	}else {
		u=ch[u][1];
		while(ch[u][0])u=ch[u][0];
	}
	return u;
}
void del(int &u,int x){
	if(!u)return;
	sz[u]--;
	if(val[u]==x){
		int nt=getnxt(u);//获取下一个交换val的节点
		if(!nt){//已经到达叶子节点
			if(col[u]){//红色节点直接删除即可,无需修正
				ch[fa[u]][dir(u)]=0;
				fa[u]=0;
			}else {//黑色节点需要开始修正
				check2(u);
			}
		}else {
			int v=nt;
			while(fa[v]!=u){//注意维护sz变量
				v=fa[v];
				sz[v]--;
			}
			swap(val[u],val[nt]);//交换val
			del(nt,x);
		}
	}else if(x>val[u]){
		del(ch[u][1],x);
	}else del(ch[u][0],x);
}

有了 i n s e r t ( x ) insert(x) insert(x) d e l ( x ) del(x) del(x)操作,后面的操作与其它平衡树几乎是相同的,就不多介绍了,这里给出整个代码以供参考(洛谷数据很弱,其实不太清楚红黑树写对没有,如果有人能够发现错误欢迎指正)。

#include 
using namespace std;


const int maxn = 1e5+5;
const int inf = 2147483647;

int ch[maxn][2],tot,col[maxn],sz[maxn],val[maxn],root,fa[maxn];//0黑,1红

void display(int u){
	if(!u)return;
	printf("%d,%d,%d,%d,%d,%d\n",u,col[u],val[u],sz[u],ch[u][0],ch[u][1]);
	display(ch[u][0]);
	display(ch[u][1]);
}
int newnode(int f,int x){
	sz[++tot]=1;val[tot]=x;
	col[tot]=1;fa[tot]=f;
	return tot;
}
void pushup(int rt){
	sz[rt]=sz[ch[rt][0]]+sz[ch[rt][1]]+1;
}
int dir(int u){
	return ch[fa[u]][1]==u;
}
void connect(int u,int f,int d){
	if(u)fa[u]=f;
	ch[f][d]=u;
}
void rotate(int u){
	int f=fa[u],ff=fa[f],du=dir(u);
	if(!f)return;
	int df=dir(f);
	connect(u,ff,df),connect(ch[u][du^1],f,du),connect(f,u,du^1);
	pushup(f),pushup(u);
	if(f==root)root=u;
}
void check1(int u){
	if(!u || !col[u])return;
	if(root==u){//case0
		col[u]^=1;
	}else if(col[fa[u]]){
		int f=fa[u];
		int df=dir(f),ff=fa[f],du=dir(u),v=ch[ff][df^1];
		if(col[v]){//case 1
			col[f]=col[v]=0;
			col[ff]=1;
		}else{
			if(du==df){//case 2
				rotate(f);
				col[ff]=1,col[f]=0;
			}else{//case 3
				rotate(u);
				rotate(u);
				col[ff]=1,col[u]=0;
			}
		}
	}
}
int getred(int u,int d){
	if(ch[u][d] && col[ch[u][d]])return d;
	else if(ch[u][d^1] && col[ch[u][d^1]])return d^1;
	return -1;
}
void check2(int u){
	int f=fa[u],ff=fa[f];
	if(u==root){
		root=0;
		return;
	}
	int du=dir(u),v=ch[f][du^1];
	ch[f][du]=0;fa[u]=0;
	while(true){
		if(!v)return;
		if(!col[f] && col[v]){
			swap(col[f],col[v]);
			rotate(v);
			v=ch[f][du^1];
		}else if(!col[f] && !col[v] && getred(v,0)==-1){
			col[v]=1;
			u=f;
			if(u==root)return;
			f=fa[u],du=dir(u),v=ch[f][du^1];
		}else if(col[f] && !col[v] && getred(v,0)==-1){
			swap(col[f],col[v]);
			return;
		}else {
			int dv=dir(v);
			if(getred(v,dv)==dv){
				col[v]=col[f];
				col[ch[v][dv]]=col[f]=0;
				rotate(v);
			}else{
				col[ch[v][dv^1]]=col[f];
				col[f]=0;
				int son=ch[v][dv^1];
				rotate(son);
				rotate(son);
			}
			return;
		}
	}
}
void insert(int &u,int f,int x){
	if(!u){
		u=newnode(f,x);
		check1(u);
		return;
	}
	++sz[u];
	if(x>=val[u])insert(ch[u][1],u,x);
	else insert(ch[u][0],u,x);
	check1(u);
}
int getnxt(int u){
	if(!ch[u][1]){
		u=ch[u][0];
		if(!u)return 0;
		while(ch[u][1])u=ch[u][1];
	}else {
		u=ch[u][1];
		while(ch[u][0])u=ch[u][0];
	}
	return u;
}
void del(int &u,int x){
	if(!u)return;
	sz[u]--;
	if(val[u]==x){
		int nt=getnxt(u);
		if(!nt){
			if(col[u]){
				ch[fa[u]][dir(u)]=0;
				fa[u]=0;
			}else {
				check2(u);
			}
		}else {
			int v=nt;
			while(fa[v]!=u){
				v=fa[v];
				sz[v]--;
			}
			swap(val[u],val[nt]);
			del(nt,x);
		}
	}else if(x>val[u]){
		del(ch[u][1],x);
	}else del(ch[u][0],x);
}
int rankx(int x){
	int u=root,ans=0;
	while(u){
		if(val[u]<x)ans+=sz[ch[u][0]]+1,u=ch[u][1];
		else u=ch[u][0];
	}
	return ans+1;
}
int findk(int k){
	int u=root;
	while(u){
		if(sz[ch[u][0]]+1>=k){
			if(sz[ch[u][0]]<k)return val[u];
			else u=ch[u][0];
		}else k-=sz[ch[u][0]]+1,u=ch[u][1];
	}
	return -1;
}
int pre(int x){
	int u=root,ans=-inf;
	while(u){
		if(val[u]<x && val[u]>ans)ans=val[u];
		if(val[u]<x)u=ch[u][1];
		else u=ch[u][0];
	}
	return ans;
}
int nxt(int x){
	int u=root,ans=inf;
	while(u){
		if(val[u]>x && val[u]<ans)ans=val[u];
		if(val[u]>x)u=ch[u][0];
		else u=ch[u][1];
	}
	return ans;
}

int main(){
	int n;
	scanf("%d",&n);
	vector<int>ans;
	for(int i=1;i<=n;++i){
		int op,x;
		scanf("%d%d",&op,&x);
		if(op==1){
			insert(root,0,x);
		}else if(op==2){
			del(root,x);
		}else if(op==3){
			printf("%d\n",rankx(x));
		}else if(op==4){
			printf("%d\n",findk(x));
		}else if(op==5){
			printf("%d\n",pre(x));
		}else if(op==6){
			printf("%d\n",nxt(x));
		}
	}
}

avl tree

avl tree效率比较低还难写,而且是所有平衡树的祖先,因此先鸽了


B tree

鸽了


B+ tree

还是鸽了


你可能感兴趣的:(数据结构,acm竞赛,算法,平衡树,红黑树,Splay)