ni_ju_ge 的 Splay 学习笔记

前言

书接上回,我们来学习要转来转去的平衡树—— Splay 树。

旋转

左旋

左旋拎右左挂右,即

代码实现:

void lturn(int &pos) {
	int ri=tree[pos].r;
	tree[pos].r=tree[ri].l;//左挂右
	tree[ri].l=pos;//拎右
	pos=ri;//还是拎右
	up(tree[pos].l);
	up(pos);
}

右旋

右旋拎左右挂左,就是复读机。

代码实现:

void rturnzag(int &pos) {
	int le=tree[pos].l;
	tree[pos].l=tree[le].r;
	tree[le].r=pos;
	pos=le;
	up(tree[pos].r);
	up(pos);
}

Splay

提问:旋转很多次的叫什么?
回答:那维莱特 Splay 树!

提问二:有一个退化成链的二叉查找树,从根节点开始分别为 123456,插入一个数 7 后,将其旋转到根节点后,这棵树变成了什么样?
回答:没错,它还是一条链。

提问三:那旋转还有什么用?
回答:装 B! 可以用双旋!

双旋

前面的左旋(zag)和右旋(zig)都是单旋,两次单旋就是双旋操作了,双旋操作能有效打乱二叉搜索树。

Splay 树有以下四种双旋操作:

zig-zig

p 不是根节点且 xp 都是都是左侧子节点时操作。首先按照连接 p 与其父节点 g 边 zig,然后按照连接 xp 的边 zig。

从 oi-wiki 搬来的 zig-zig

zag-zag

其实就是 zig-zig 的复读机。

zag-zig

p 不是根节点且 xp 分别是左侧子节点和右侧子节点时操作。首先按 px 之间的边 zag,然后按 xg 新生成的结果边 zig。

从 oi-wiki 搬来的 zag-zig

zig-zag

其实就是 zag-zig 的复读机。

存储

简单的 Splay 存左子节点、右子节点、数据、子树大小和重复次数(可以不存,但要多维护一个父亲节点)即可。

struct node {
	int l,r,dat,size,same;
} tree[100000];
int root,cnt;
void make(int &pos,int val) {
	tree[++cnt].dat=val;
	tree[cnt].size=1;
	tree[cnt].same=1;
	pos=cnt;
}

伸展

Splay 很重要的操作就是伸展。即将某个节点旋转到根节点的操作。

我代码里的伸展是递归实现的,常数比较大,但不递归的要维护父节点,难度比较大,只能说鱼和熊掌不可兼得 XD。

void splay(int x,int &y) {
	if(x==y)return;
	int &l=tree[y].l,&r=tree[y].r;
	if(x==l)zig(y);
	else if(x==r)zag(y);
	else {
		if(tree[x].dat<tree[y].dat) {
			if(tree[x].dat<tree[l].dat)splay(x,tree[l].l),zig(y),zig(y);
			else splay(x,tree[l].r),zag(l),zig(y);
		} else {
			if(tree[x].dat<tree[r].dat)splay(x,tree[r].l),zig(r),zag(y);
			else splay(x,tree[r].r),zag(y),zag(y);
		}
	}
}

插入

与二叉搜索树一样,不过要注意,插入后还要伸展到根节点。

void take(int &pos,int val) {
	if(pos==0)make(pos,val),splay(pos,root);
	else if(val<tree[pos].dat)take(tree[pos].l,val);
	else if(val>tree[pos].dat)take(tree[pos].r,val);
	else tree[pos].same++,tree[pos].size++,splay(pos,root);//值相同,重复次数和子树大小增加
}

删除

找到该节点,将其伸展到根节点。

  • 若其重复个数不止一个,直接从里面减去就行。
  • 否则若其没有右子节点,直接将 root 替换为它的左子节点
  • 否则找到它的后缀,将后缀伸展到它的右子节点,然后将右子节点的左子节点变为它的左子节点,然后将 root 改为它的右子节点(如果看不懂,就再看一遍)
void del(int val,int pos) {
	if(pos==0)return;
	else if(val<tree[pos].dat)del(val,tree[pos].l);
	else if(val>tree[pos].dat)del(val,tree[pos].r);
	else {
		splay(pos,root);
		if(tree[root].same!=1)tree[root].same--,tree[root].size--;
		else if(tree[root].r==0)root=tree[root].l;
		else {
			pos=tree[root].r;
			while(tree[pos].l!=0)pos=tree[pos].l;//找到它的后缀
			splay(pos,tree[root].r);//将后缀伸展到它的右子节点
			tree[tree[root].r].l=tree[root].l;//将右子节点的左子节点变为它的左子节点
			root=tree[root].r;//将 root 改为它的右子节点
			up(root);
		}
	}
}

查询排名

将二叉搜索树的改一下就行,注意最后还要伸展一下。

int wrank(int val) {
	int pos=root,rnk=1;
	while(pos) {
		if(tree[pos].dat==val) {
			rnk+=tree[tree[pos].l].size;
			splay(pos,root);
			break;
		}
		if(val<=tree[pos].dat)
			pos=tree[pos].l;
		else {
			rnk+=tree[tree[pos].l].size+tree[pos].same;
			pos=tree[pos].r;
		}
	}
	return rnk;
}

查询数字

同样改一下二叉搜索树的。

int num(int val) {
	int pos=root;
	while(pos) {
		int l=tree[tree[pos].l].size;
		if(l<val&&val<=l+tree[pos].same) {//注意这里判断它是否在区间内
			splay(pos,root);
			break;
		} else if(l>=val)pos=tree[pos].l;
		else {
			val-=l+tree[pos].same;
			pos=tree[pos].r;
		}
	}
	return tree[pos].dat;
}

前缀与后缀

有了查询排名和查询数字,只要充分发扬人类智慧,有它俩来实现这俩即可。自行理解。

int pre(int val) {
	return num(wrank(val)-1);
}
int last(int val) {
	return num(wrank(val+1));
}

后记

笑点解析:模版题P3369中,Treap 和 Splay 的时间均在 475 ms 左右,但普遍认为 Splay 比 Treap 快。

posted @   ni_ju_ge  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示