C++编写Splay Tree——算法设计与分析,详细解释

什么是Splay Tree

Splay Tree 就是一颗的二叉排序树,首先是二叉树,其次对于任意节点,其左子节点的值 ≤ \le 该节点的值 ≤ \le 右子节点的值,那与普通的二叉排序树有什么区别呢?它能使得树变得更加平衡,但是不像平衡树那样严格限制树的形状,使得左右子树高度差小于1。

  • 基本思想:假设数据具有时间局部性,即一个数据访问后不久还会再次访问,Splay Tree 能让数据在访问后不久再次访问的时间变短
  • 操作:Splay 伸展操作(将节点经过一系列旋转提升到根节点),包括左旋(zag)和右旋(zig)

平摊的复杂度为 O ( l o g N ) O(logN) O(logN)

下面是一些声明,采用数组形式存放节点,val 存放原数组的值,lson 存放左子树编号,rson 存放右子树编号, p 存放父节点编号,sz 为当前节点个数,给节点编号,rt 记录了根节点的编号。

int val[MAX],			//val[i]表示编号为i的节点的值
	lson[MAX],			//左儿子编号,没有则为0 
	rson[MAX],			//右儿子编号,没有则为0 
	p[MAX],				//父节点编号,没有则为0
	sz = 0;					//节点编号从1到sz,每次新增一个节点sz++
class Splay{	
private:
	int rt = 0;				//根节点编号

Zig和Zag(单旋)操作

先看看简单的情况,假设当前节点为 x,现在想把它提升一个高度,就要和父节点 y 做旋转,如果 x 和 y 之间的边在 x 节点的右侧,则进行右旋,以 x 为轴,将右边的边往下旋转,左旋同理,如下图所示。
在这里插入图片描述
理解了什么是左旋右旋后,再来看看复杂一点的情况,拿右旋 x 为例,假设 x 有右儿子为 b ,首先将 b 放在 y 的左节点,再将 y 子树放在 x 的右节点上,如果原来的 y 有父节点 z,则将 x 的父节点设置为 z,左旋类似,旋转前后不改变遍历顺序,可以验证右旋前后先序遍历都是 c x b y a 。
在这里插入图片描述
按照上面的旋转方法,可以编写出单旋的代码

void Splay::zag(int x){	//左旋x
	int y = p[x], z = p[y];
	int b = lson[x];
	rson[y] = b;		//x有左节点b,将y的右节点设置成b 
	if(b) p[b] = y;		//如果x有左节点b
	lson[x] = y;
	p[y] = x;
	p[x] = z;
	if(z){				//如果y不是根节点,存在父节点z 
		if(lson[z] == y)	lson[z] = x;
		else 				rson[z] = x;
	}
} 
void Splay::zig(int x){	//右旋x
	int y = p[x], z = p[y];
	int b = rson[x];
	lson[y] = b; 		//x有右节点b,将y的左节点设置成b 
	if(b)	p[b] = y;
	rson[x] = y;
	p[y] = x;
	p[x] = z;
	if(z){
		if(lson[z] == y)	lson[z] = x;
		else 				rson[z] = x;
	} 
} 

双旋操作

双旋就是进行两次单旋操作,将节点提升两个高度,具体而言,有之字形和一字形,共四类情况,而对应的旋转操作如下图所示:
在这里插入图片描述
对于一字形,如下第一张图,首先对 y 进行左旋,得到中间的树型,再对 x 进行左旋,最终将 x 提升了两个高度,另一种情况就不再展示。对于下图之字形情况,首先对 x 进行右旋,提升到中间节点,再对 x 进行左旋,x 提升到了最上面。
在这里插入图片描述
编写代码如下:

if(lson[z] == y && lson[y] == x){		//		  z
    zig(y);								//		y	1.右旋 
    zig(x);								//    x		2.右旋 
} 
else if(rson[z] == y && rson[y] == x){	//	  z
    zag(y);								//		y	1.左旋 
    zag(x);								//    	  x	2.左旋 
} 
else if(lson[z] == y && rson[y] == x){	//	    z
    zag(x);								//	  y
    zig(x);								//		x   1.左旋,2.右旋 
}
else{									//	  z
    zig(x);								//	    y
    zag(x);								//	  x   	1.右旋,2.左旋 
}

Splay操作

知道了单旋和双旋,那么如何进行Splay操作,即如何把节点提升至根节点呢?我们有以下算法:

1)判断当前节点是否是根节点
​		Y:结束
​		N:(2) 判断父节点是否是根节点
​			Y:单旋,结束
​			N:双旋,并回到(1

也就是说将 x 提升到根节点只需要一直做双旋,如果父节点是根节点的话,就只需要做一次单旋即可,直到当前节点是根节点为止,Splay操作完成,代码如下,时间复杂度为 O ( l o g N ) O(log N) O(logN)

void Splay::splay(int x){
	while(p[x]){	//当x的父节点不为根节点,重复执行循环 
		int y = p[x], z = p[y];
		if(!z){		//如果父亲节点是根节点,即p[y] = 0 
			if(lson[y] == x)	zig(x);		//如果x是y的左儿子,进行右旋
			else				zag(x);		//否则进行左旋 
		} 
		else{
			if(lson[z] == y && lson[y] == x){		//		  z
				zig(y);								//		y	1.右旋 
				zig(x);								//    x		2.右旋 
			} 
			else if(rson[z] == y && rson[y] == x){	//	  z
				zag(y);								//		y	1.左旋 
				zag(x);								//    	  x	2.左旋 
			} 
			else if(lson[z] == y && rson[y] == x){	//	    z
				zag(x);								//	  y
				zig(x);								//		x   1.左旋,2.右旋 
			}
			else{									//	  z
				zig(x);								//	    y
				zag(x);								//	  x   	1.右旋,2.左旋 
			}
		} 
	}
	rt = x;		//更新根结点 
}

插入元素

插入元素只需要从根节点往下走,找到合适的位置满足二叉排序树的要求即可,具体而言,如果插入元素比当前节点小,则去左子树,如果大,则去右子树,下面是插入元素4的例子。
在这里插入图片描述
插入元素的时间复杂度为 O ( l o g N ) O(log N) O(logN),插入完新的元素后,还要将其提升至根节点,做一次 Splay 操作,代码如下:

void Splay::add(int v){
	val[++sz] = v;			//插入新的元素到val数组后
	if(!rt)  rt = sz;		//设置根节点
	else{
		int x = rt;
		while(true){
			if(v < val[x]){		//去往左子树 
				if(lson[x])		x = lson[x];
				else{			//找到插入位置了 
					lson[x] = sz;
					p[sz] = x;
					break; 
				}
			}
			else{				//去往右子树 
				if(rson[x])		x = rson[x];
				else{
					rson[x] = sz;
					p[sz] = x;
					break;
				} 
			}
		}
		splay(sz);		//将新插入的节点sz旋转到根节点 
	} 
} 

查询操作

查询值为 v 的节点的下标值,和插入元素思想类似,遇到小的到左边,遇到大的到右边,直到找到相等的或者到叶子节点为止,如果没有找到,则返回 0,否则返回值为 v 的元素下标,最后也要进行一次Splay操作,时间复杂度也为 O ( l o g N ) O(logN) O(logN)

int Splay::find(int v){
	if(!rt)	return 0;
	int x = rt;
	while(x){
		if(val[x] == v)			break;
		else if(v < val[x])		x = lson[x];
		else					x = rson[x];
	}
	if(x){				//如果搜索到了,则x为对应值为v的节点下标 
		splay(x);		//将查询得到的x旋转到根节点 
		return x;
	}
	return 0; 
} 

查询最大最小值

寻找最大值只需一直遍历右子树,直到遍历到叶子节点为止,这个就是最大值,再进行一次Splay操作,将最大值提升到根节点,如下图所示,Splay 操作完后,根节点是没有右节点的。
在这里插入图片描述
时间复杂度也为 O ( l o g N ) O(logN) O(logN),代码如下:

int Splay::find_max(){
	if(!rt)	return 0;
	int x = rt;
	while(rson[x])	x = rson[x];
	splay(x);		//将x提升到根节点 
	return x;
} 

int Splay::find_min() {
	if(!rt) return 0;
	int x = rt;
	while(lson[x])  x = lson[x];
	splay(x);
	return x;
}

删除操作

删除值为 v 的节点,首先调用 find 函数,如果没有值为 v 的节点,find 函数则返回0,删除函数啥也不做,如果存在值为 v 的节点,则 find 函数已经将其 Splay 到了根节点,现在只要分情况讨论,删除根节点即可,共有4种情况,如果没有左右子树,则设置 rt 为 0 ,其他三种情况如下图所示。
在这里插入图片描述

void Splay::del(int v){	//用左子树的最大值代替根节点 
	int x = find(v);
	if(!x)	return;		//如果没有该节点,则啥事不做
	if(!lson[x] && !rson[x])	rt = 0;		//因为有Splay操作,所以x现在为根节点,如果左右节点都没有,则设置根节点为0
	else if(!lson[x]){		//没有左子树,则删除根节点后根节点设置为原来根节点的右子树 
		rt = rson[rt];
		p[rt] = 0; 
	}
	else if(!rson[x]){
		rt = lson[rt];
		p[rt] = 0;
	}
	else{	//左右子树均不为空,则从左子树中找到最大值,然后将rson[rt]变成这个节点的右儿子 
		Splay tree1, tree2;
		tree1.rt = lson[rt];//		 rt
		p[tree1.rt] = 0;	//	   /    \
							//	tree1  tree2
		tree2.rt = rson[rt];
		p[tree2.rt] = 0;
		tree1.find_max();
		rt = tree1.rt;
		rson[rt] = tree2.rt;
		p[tree2.rt] = rt; 
	} 
}

上代码

#include<iostream>
#include<queue> 
#include<algorithm>
using namespace std;
#define MAX 50050

int val[MAX],			//val[i]表示编号为i的节点的值
	lson[MAX],			//左儿子编号,没有则为0 
	rson[MAX],			//右儿子编号,没有则为0 
	p[MAX],				//父节点编号,没有则为0
	sz = 0;					//节点编号从1到sz,每次新增一个节点sz++
	
	
class Splay{	
private:
	int rt = 0;				//根节点编号		
public:
	void zag(int x);		//对x进行左旋
	void zig(int x);		//对x进行右旋
	void splay(int x);		//将x变成根节点
	void add(int v);		//插入一个值为v的节点
	int find(int v);	//返回权值为v的节点的编号,没有则返回0
	void del(int v);		//删除权值为v的节点,如果没有则什么也不做
	int find_max();			//返回最大值对应节点的编号
	int find_min();			//返回最小值对应节点的编号 
};

void Splay::zag(int x){	//左旋 
	int y = p[x], z = p[y];
	int b = lson[x];
	rson[y] = b;		//x有左节点b,将y的右节点设置成b 
	if(b) p[b] = y;		//如果x有左节点 
	lson[x] = y;
	p[y] = x;
	p[x] = z;
	if(z){				//如果y不是根节点,存在父节点z 
		if(lson[z] == y)	lson[z] = x;
		else 				rson[z] = x;
	}
} 

void Splay::zig(int x){	//右旋
	int y = p[x], z = p[y];
	int b = rson[x];
	lson[y] = b; 		//x有右节点b,将y的左节点设置成b 
	if(b)	p[b] = y;
	rson[x] = y;
	p[y] = x;
	p[x] = z;
	if(z){
		if(lson[z] == y)	lson[z] = x;
		else 				rson[z] = x;
	} 
} 

void Splay::splay(int x){
	while(p[x]){	//当x的父节点不为根节点,重复执行循环 
		int y = p[x], z = p[y];
		if(!z){		//如果父亲节点是根节点,即p[y] = 0 
			if(lson[y] == x)	zig(x);		//如果x是y的左儿子,进行右旋
			else				zag(x);		//否则进行左旋 
		} 
		else{
			if(lson[z] == y && lson[y] == x){		//		  z
				zig(y);								//		y	1.右旋 
				zig(x);								//    x		2.右旋 
			} 
			else if(rson[z] == y && rson[y] == x){	//	  z
				zag(y);								//		y	1.左旋 
				zag(x);								//    	  x	2.左旋 
			} 
			else if(lson[z] == y && rson[y] == x){	//	    z
				zag(x);								//	  y
				zig(x);								//		x   1.左旋,2.右旋 
			}
			else{									//	  z
				zig(x);								//	    y
				zag(x);								//	  x   	1.右旋,2.左旋 
			}
		} 
	}
	rt = x;		//更新根结点 
}

void Splay::add(int v){
	val[++sz] = v;
	if(!rt)  rt = sz;		//设置根节点
	else{
		int x = rt;
		while(true){
			if(v < val[x]){		//去往左子树 
				if(lson[x])		x = lson[x];
				else{			//找到插入位置了 
					lson[x] = sz;
					p[sz] = x;
					break; 
				}
			}
			else{				//去往右子树 
				if(rson[x])		x = rson[x];
				else{
					rson[x] = sz;
					p[sz] = x;
					break;
				} 
			}
		}
		splay(sz);		//将新插入的节点sz旋转到根节点 
	} 
} 

int Splay::find(int v){
	if(!rt)	return 0;
	int x = rt;
	while(x){
		if(val[x] == v)			break;
		else if(v < val[x])		x = lson[x];
		else					x = rson[x];
	}
	if(x){				//如果搜索到了,则x为对应值为v的节点下标 
		splay(x);		//将查询得到的x旋转到根节点 
		return x;
	}
	return 0; 
} 

void Splay::del(int v){	//用左子树的最大值代替根节点 
	int x = find(v);
	if(!x)	return;		//如果没有该节点,则啥事不做
	if(!lson[x] && !rson[x])	rt = 0;		//因为有Splay操作,所以x现在为根节点,如果左右节点都没有,则设置根节点为0
	else if(!lson[x]){		//没有左子树,则删除根节点后根节点设置为原来根节点的右子树 
		rt = rson[rt];
		p[rt] = 0; 
	}
	else if(!rson[x]){
		rt = lson[rt];
		p[rt] = 0;
	}
	else{	//左右子树均不为空,则从左子树中找到最大值,然后将rson[rt]变成这个节点的右儿子 
		Splay tree1, tree2;
		tree1.rt = lson[rt];//		 rt
		p[tree1.rt] = 0;	//	   /    \
							//	tree1  tree2
		tree2.rt = rson[rt];
		p[tree2.rt] = 0;
		tree1.find_max();
		rt = tree1.rt;
		rson[rt] = tree2.rt;
		p[tree2.rt] = rt; 
	} 
}

int Splay::find_max(){
	if(!rt)	return 0;
	int x = rt;
	while(rson[x])	x = rson[x];
	splay(x);		//将x提升到根节点 
	return x;
} 

int Splay::find_min() {
	if(!rt) return 0;
	int x = rt;
	while(lson[x])  x = lson[x];
	splay(x);
	return x;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

rebibabo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值