Splay平衡树

Splay平衡树

基于 Splay 伸展操作维持平衡的二叉树,不断将某个节点旋转到根节点,可以在均摊 \(O(\log n)\) 的时间内完成插入查找和删除操作


节点的定义:

struct node{
	int s[2];//左儿子0,右儿子1
	int v;//节点的权值
	int cnt;//权值的次数
	int p;//节点的父亲
	int siz;//以节点为根的子树大小
	void init(int x,int y){//初始化
		v=x,p=y;
		cnt=siz=1;
	}
};

其他数组的定义:

node tr[N];
int root,idx;//根节点以及节点序号

Rotate 旋转操作,根据树形态的不同,分为左旋和右旋

img

将传入的节点 \(x\) 进行旋转,如果 \(x\) 是他父亲的左儿子,那么进行右旋,如果 \(x\) 是他父亲的右儿子,那么进行左旋

定义 \(y\) 表示 \(x\) 的父亲,\(z\) 表示 \(y\) 的父亲

需要维护平衡二叉树的性质,即一个节点的左子树上的所有权值都小于该节点的权值,右子树上的所有权值都大于该节点的权值

右旋:

  • \(x\) 的右儿子重新绑定在 \(y\) 的左儿子上
  • \(x\) 旋转为 \(y\) 的父亲,即把 \(y\) 绑定在 \(x\) 的右儿子上
  • 最后将 \(x\) 绑定在原来 \(y\) 对应 \(z\) 的某个儿子的位置上

同样左旋的操作可以类比

void rotate(int x){
	int y=tr[x].p,z=tr[y].p;
	bool r=tr[y].s[1]==x;//判断左旋还是右旋
	//x是y的右儿子时r=1,进行左旋操作。x是y的左儿子时r=0,进行右旋操作
	tr[y].s[r]=tr[x].s[r^1];
	tr[tr[x].s[r^1]].p=y;
	//绑定x的某个儿子到y上
	tr[x].s[r^1]=y;
	tr[y].p=x;
	//x旋转为y的父亲
	tr[z].s[tr[z].s[1]==y]=x;
	tr[x].p=z;
	//维护z的儿子位置关系
	pushup(y);//自下向上更新
	pushup(x);
}

void pushup(int x){//更新子树大小
	tr[x].siz=tr[tr[x].s[0]].siz+tr[tr[x].s[1]].siz+tr[x].cnt;
}

Splay 伸展操作,将链伸展为树,压缩树高,分为单旋和双旋

将传入的节点所在链进行伸展并把 \(x\) 旋转到 \(k\) 的下方 ,如果链上有两个节点,那么执行单旋,否则执行双旋

需要一直执行伸展旋转操作,\(x\) 旋转到根节点时树高被压缩到最小

定义 \(y\) 表示 \(x\) 的父亲,\(z\) 表示 \(y\) 的父亲

单旋:

图 2

如上图展示,\(x\) 通过旋转一次就能变为 \(y\) 的父亲

双旋直线型:

图 4

当链上没有折线时,需要先旋转 \(y\),再旋转 \(x\)

双旋折线型:

图 6

链上有折线时,通过两次旋转 \(x\) 操作,可以将树高压缩一次,并维护的二叉树的性质

void splay(int x,int k){
	while(tr[x].p!=k){//一直旋转到k节点的下方
		int y=tr[x].p,z=tr[y].p;
		if (z!=k)//判断单旋还是双旋
			(tr[y].s[0]==x)^(tr[z].s[0]==y)? rotate(x):rotate(y);
		//折线型旋转y,直线型旋转x
		rotate(x);
	}
	if (k==0) root=x;//更新根节点
}

Find 查找操作,查询传入的 \(val\) ,并把有这个权值的节点旋转到根节点上

如果不存在 \(val\) 元素,则会将与 \(val\) 最接近的点旋转到根节点上,这样做的原因是为了方便查找前后驱

基于平衡二叉树的性质,根据值的大小判断搜索左子树还是右子树

void find(int v){
	int x=root;//从根节点进入
	while (tr[x].s[v>tr[x].v]&&v!=tr[x].v)
	//v大于当前节点的权值,搜索右子树,否则搜索左子树
		x=tr[x].s[v>tr[x].v];//走到儿子节点
	splay(x,0);//伸展到根节点
}

Insert插入操作,将传入的 \(val\) 放进平衡树中并维持平衡性

从根节点进入,根据 \(val\) 的大小向下移动,当走到空节点或与 \(val\) 权值相同的节点时停止,然后做插入操作

void insert(int v){
	int x=root,p=0;//从根节点进入
	while (x&&tr[x].v!=v){
		p=x;
		x=tr[x].s[v>tr[x].v];
	}
	if (x) tr[x].cnt++;//如果存在权值相同的节点,该节点计数加一
	else{//否则
		x=++idx;//对新权值编号
		tr[p].s[v>tr[p].v]=x;//判断插入左儿子还是右儿子
		tr[x].init(v,p);//插入新节点,初始化
	}
	splay(x,0);//伸展操作,维持树高
}

Getpre查找前驱操作,查找权值比 \(val\) 小的第一个节点

与Find操作结合,先将权值为 \(val\) 的节点旋转到根节点上,如果不存在 \(val\) 的节点,那就传入最接近他的节点

如果选装上来的根节点比 \(val\) 小,那么这个点就是前驱,否则就走左子树然后通过右子树一直逼近

int getpre(int v){
	find(v);
	int x=root;
	if (tr[x].v<v) return x;//判断旋转上来的根节点是否就是前驱
	x=tr[x].s[0];//走左子树
	while (tr[x].s[1]) x=tr[x].s[1];//右子树无限逼近
	splay(x,0);
	return x;
}

Getsuc查找后驱操作,查找权值比 \(val\) 大的第一个节点

int getsuc(int v){
	find(v);
	int x=root;
	if (tr[x].v>v) return x;
	x=tr[x].s[1];
	while (tr[x].s[0]) x=tr[x].s[0];
	splay(x,0);
	return x;
}

Del删除操作,删去一个权值为 \(val\) 的节点

如果在没有特殊出理的平衡树上删除一个节点,需要维护他子树的性质,较难实现

所以考虑将需要删去的节点架空到一个叶子节点上,这样便不需要对子树进行维护

原理是先将 \(val\)前驱旋转到根节点上,然后将后驱转到前驱的右子树上,那么两者夹住的节点(后驱的左子树),就是需要删除的 \(val\) 节点

void del(int v){
	int pre=getpre(v);
	int suc=getsuc(v);
	splay(pre,0);
	splay(suc,pre);
	int del=tr[suc].s[0];
	if (tr[del].cnt>1){
		tr[del].cnt--;
		splay(del,0);
	} 
	else{//如果能被删空,那么就将这个节点移除
		tr[suc].s[0]=0;
		splay(suc,0);
	}
}

Getrank查询排名操作,查询有多少个权值大于 \(val\) 的节点

先插入一个权值为 \(val\) 的节点,插入操作后,这个节点被旋转到了根节点上,那么他的左子树的大小就是比他小的元素个数

为了能保证查询过程中左右子树都至少有一个节点,所以在平衡树建立时,插入左右哨兵

insert(-Iinf);//最小值哨兵
insert(Iinf);//最大值哨兵
int getrank(int v){
	insert(v);
	int res=tr[tr[root].s[0]].siz;
	del(v);
	return res;
}

Getval查询权值操作,查询节点编号为 \(k\) 的权值

仍然从根节点进入,如果左子树非空且剩余排名 \(k\) 不大于左子树的大小,那么向左子树查找;否则减去左子树的大小。

如果减去左子树的大小后,剩余排名 \(k\) 不大于父节点的节点次数,那么所查询的节点就是父节点;否则减去父节点次数向右子树查找

找到需要的节点后将节点伸展到根上,返回根节点的权值

int getval(int k){
	int x=root;
	while (1){
		int y=tr[x].s[0];
		if (tr[y].siz+tr[x].cnt<k){//搜索右子树
			k-=tr[y].siz+tr[x].cnt;
			x=tr[x].s[1];
		}else{
			if (tr[y].siz>=k) x=tr[x].s[0];//搜索左子树
			else break;//找到查询的节点
		}
	}
	splay(x,0);
	return tr[x].v;
}
posted @ 2025-06-01 13:18  才瓯  阅读(72)  评论(0)    收藏  举报