首页 > 笔记 > 各种树结构之三 Splay

各种树结构之三 Splay

丧心病狂的splay

伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。它由Daniel Sleator和Robert Tarjan创造。

(1) 伸展树属于二叉查找树,即它具有和二叉查找树一样的性质:假设x为树中的任意一个结点,x节点包含关键字key,节点x的key值记为key[x]。如果y是x的左子树中的一个结点,则key[y] <= key[x];如果y是x的右子树的一个结点,则key[y] >= key[x]。

(2) 除了拥有二叉查找树的性质之外,伸展树还具有的一个特点是:当某个节点被访问时,伸展树会通过旋转使该节点成为树根。这样做的好处是,下次要访问该节点时,能够迅速的访问到该节点。

假设想要对一个二叉查找树执行一系列的查找操作。为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法,在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生,它是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。

相比于”二叉查找树“和”AVL树“,学习伸展树时需要重点关注是”伸展树的旋转算法”。

 

变量声明:f[i]表示i的父结点,ch[i][0]表示i的左儿子,ch[i][1]表示i的右儿子,key[i]表示i的值,cnt[i]表示i结点的关键字出现的次数(bst不允许有重复的节点),size[i]表示包括i的这个子树的大小;sz为整棵树的大小,root为整棵树的根。

int f[Maxn];//father
int ch[Maxn][2];//child ; 0 for left ; 1 for right
int key[Maxn];//key
int cnt[Maxn];//value
int siz[Maxn];//size of subtree
int sz,root;//size of tree and root

还有几个基本操作:

【clear操作】:将当前点的各项值都清0(用于删除之后)

//clear the ndoe
void clear(int x)
{
	ch[x][0]=ch[x][1]=f[x]=cnt[x]=key[x]=siz[x]=0;
}

【getson操作】:判断当前点是它父结点的左儿子还是右儿子

//rightson return 1;left son return 0
int getson(int x)
{
	return ch[f[x]][1]==x;
}

【update操作】:更新当前点的size值(用于发生修改之后)

//update the size
void update(int x)
{
	siz[x]=cnt[x];
	if (ch[x][0]) siz[x]+=siz[ch[x][0]];
	if (ch[x][1]) siz[x]+=siz[ch[x][1]];
}

【rotate操作】

比如我们现在有一棵splay

images

我们要把D旋转到他父亲的位置

images

我们先找到三条必须断掉的边

images

然后对于D,B>D,所以B要成为D的右孩子,但是已经有G了。因为G<B,所以正好连到B的左孩子,最后把A和D连起来

images

然后就完成了

images

具体步骤

step 1:

找出D的父亲结点(B)以及父亲的父亲(A)并记录。判断D是B的左结点还是右结点,设为K。

step 2:

将D与K关系相反的儿子的父亲,记为B与K关系相同的儿子(这里即为D的右儿子的父亲记为B的左儿子);

将D与K关系相反的儿子的父亲记为B(这里即为把G的父亲记为B);

将B的父亲即为D;将D与K关系相反的儿子记为B(这里即为把D的右儿子记为B);

将D的父亲记为A,A的儿子记为D。

step 3:

update一下当前点和各个父结点的各个值

代码

//retation
int rotate(int x)
{
	int fa=f[x],fafa=f[fa],k=getson(x);
	ch[fa][k]=ch[x][k^1];f[ch[fa][k]]=fa;
	ch[x][k^1]=fa;f[fa]=x;
	f[x]=fafa;
	if (fafa)
		ch[fafa][ch[fafa][1]==fa]=x;
	update(fa);update(x);
}

【splay操作】

伸展操作只是在不停的rotate,一直到达到根。

splay的过程中需要分类讨论,如果是三点一线的话(x,x的父亲,x的祖父)需要先rotate x的父亲,否则需要先rotate x本身(否则会形成单旋使平衡树失衡)

//rotate until x is the root
void splay(int x)
{
	for (int fa;fa=f[x];rotate(x))
		if (f[fa])
			rotate(getson(x)==getson(fa) ? fa : x);
	root=x;
}

【insert操作】

其实插入操作是比较简单的,和普通的bst基本一样。

step 1:

如果root=0,即树为空的话,新建一个节点,直接返回即可。

step 2:

按照二叉查找树的方法一直向下找,其中:

如果遇到一个结点的关键字等于当前要插入的点的话,我们就等于把这个结点加了一个cnt。因为在bst中是不可能出现两个相同的点的。并且要将当前点和它父亲结点的各项值更新一下。做一下splay。

如果已经到了最底下了,那么就可以直接插入。整个树的大小要+1,新结点的左儿子右儿子(虽然是空)父亲还有各项值要一一对应。并且最后要做一下他父亲的update(做他自己的没有必要)。做一下splay。

//ceate a new splay node
void create(int v)
{
	sz++;
	ch[sz][0]=ch[sz][1]=f[sz]=0;
	key[sz]=v;
	cnt[sz]=1;
	siz[sz]=1;
	//root=sz;
}


//insert a node
void insert(int v)
{
	if (!root)
		create(v),root=sz;
	else
	{
		int now=root,fa=0;
		while(1)
		{
			if (key[now]==v)
			{
				cnt[now]++;
				update(now);update(fa);
				splay(now);
				break;
			}
			fa=now;
			now=ch[fa][v>key[fa]];
			if (!now)
			{
				create(v);
				f[sz]=fa;
				ch[fa][v>key[fa]]=sz;
				update(fa);
				splay(sz);
				break;
			}
		}
	}
}

【findpos操作】查询x的排名

初始化:ans=0,当前点=root

和其它二叉搜索树的操作基本一样。但是区别是:

如果x比当前结点小,即应该向左子树寻找,ans不用改变。

如果x比当前结点大,即应该向右子树寻找,ans需要加上左子树的大小以及根的大小(这里的大小指的是权值)。

不要忘记了再splay一下。

//find x's pos
int findpos(int v)
{
	int now=root,ans=0;
	while(1)
	{
		if (v<key[now])
			now=ch[now][0];
		else
		{
			ans+=ch[now][0]?siz[ch[now][0]]:0;
			if (v==key[now])
			{
				splay(now);
				return ans+1;
			}
			ans+=cnt[now];
			now=ch[now][1];
		}
	}
}

【findx操作】找到排名为x的点

初始化:当前点=root

和上面的思路基本相同:

如果当前点有左子树,并且x比左子树的大小小的话,即向左子树寻找;

否则,向右子树寻找:先判断是否有右子树,然后记录右子树的大小以及当前点的大小(都为权值),用于判断是否需要继续向右子树寻找。

//find pos's x
int findx(int x)
{
	int now=root;
	while(1)
	{
		if (ch[now][0] && x<=siz[ch[now][0]])
			now=ch[now][0];
		else
		{
			int temp=(ch[now][0]?siz[ch[now][0]]:0)+cnt[now];
			if (x<=temp)
				return key[now];
			x-=temp;
			now=ch[now][1];
		}
	}
}

【求x的前驱(后继),前驱(后继)定义为小于(大于)x,且最大(最小)的数】

这类问题可以转化为将x插入,求出树上的pre(nex),再将x删除的问题。

【pre/nex操作】

这个操作十分的简单,只需要理解一点:在我们做insert操作之后做了一遍splay。这就意味着我们把x已经splay到根了。求x的前驱其实就是求x的左子树的最右边的一个结点,后继是求x的右子树的左边一个结点。

int pre()
{
	int now=ch[root][0];
	while(ch[now][1])
		now=ch[now][1];
	return now;
}

int nex()
{
	int now=ch[root][1];
	while(ch[now][0])
		now=ch[now][0];
	return now;
}

【del操作】

删除操作是最后一个稍微有点麻烦的操作。

step 1:findpos一下x。目的是:将x旋转到根。

step 2:那么现在x就是根了。如果cnt[root]>1,即不只有一个x的话,直接-1返回。

step 3:如果root并没有孩子,就说名树上只有一个x而已,直接clear返回。

images

step 4:如果root只有左儿子或者右儿子,那么直接clear root,然后把唯一的儿子当作根就可以了(f赋0,root赋为唯一的儿子)

剩下的就是它有两个儿子的情况。

images

step 5:我们找到新根,也就是x的前驱(x左子树最大的一个点),将它旋转到根。然后将原来x的右子树接到新根的右子树上(注意这个操作需要改变父子关系)。这实际上就把x删除了。不要忘了update新根。

代码

void del(int x)
{
	int t=findpos(x);
	if (cnt[root]>1)
	{
		cnt[root]--;
		update(root);
		return;
	}
	//none
	if (!ch[root][0] && !ch[root][1])
	{
		clear(root);
		root=0;
		return;
	}
	//one
	if (!ch[root][1])
	{
		int temp=root;
		root=ch[root][0];
		f[root]=0;
		clear(temp);
		return;
	}
	else
	if (!ch[root][0])
	{
		int temp=root;
		root=ch[root][1];
		f[root]=0;
		clear(temp);
		return;
	}
	//two
	int pre1=pre(),temp=root;
	splay(pre1);
	f[ch[temp][1]]=root;
	ch[root][1]=ch[temp][1];
	clear(temp);
	update(root);
}

操作均来源于bzoj3224

总代吗

#include <cstdio>
#include <cstdlib>
#define Maxn 500000
using namespace std;
int f[Maxn],ch[Maxn][2],siz[Maxn],key[Maxn],sz,root;
int get(int x){return ch[f[x]][1]==x;}
void update(int x){siz[x]=siz[ch[x][1]]+siz[ch[x][0]];}
void clear(int x){f[x]=ch[x][0]=ch[x][1]=siz[x]=key[x]=0;}
void create(int v){sz++;ch[sz][0]=ch[sz][1]=f[sz]=0;siz[sz]=1;key[sz]=v;}
void rot(int x)
{
	int k=get(x),fa=f[x],fafa=f[fa];
	ch[fa][k]=ch[x][!k];f[ch[x][!k]]=fa;
	ch[x][!k]=fa;f[fa]=x;
	f[x]=fafa;
	if (fafa)
		ch[fafa][ch[fafa][1]==fa]=x;
	update(fa);update(x);
}
void splay(int x)
{
	for (int fa;fa=f[x];rot(x))
		if (f[fa])
			rot(get(x)==get(fa)?fa:x);
	root=x;
}
void insert(int v)
{
	if (!root)
	{create(v);root=sz;return;}
	int now=root,fa=0;
	while(1)
	{
		fa=now;now=ch[now][key[now]<v];
		if (!now)
			{create(v);f[sz]=fa;ch[fa][key[fa]<v]=sz;update(fa);splay(sz);return;}
	}
}
void del(int v)
{
	int now=root,fa=0;
	while(1){if (key[now]==v){splay(now);break;}now=ch[now][key[now]<v];}
	if (!ch[root][1] && !ch[root][0])
		clear(root),root=0;
	else if (!ch[root][1] || !ch[root][0])
	{
		int x=ch[root][1]?ch[root][1]:ch[root][0];
		clear(root);f[x]=0,root=x;
	}
	else
	{
		int pre=ch[root][0],oldr=root;
		while (ch[pre][1]) pre=ch[pre][1];
		splay(pre);
		ch[root][1]=ch[oldr][1];
		f[ch[oldr][1]]=root;
		clear(oldr);
		update(root);
	}
}
//preparation is over
//write your legendary
main()
{
}

The end.