平衡树之splay树入门

16 篇文章 2 订阅

splay(伸展)树的基本介绍

简介

splay树是BST的一种,其均摊复杂度是O(logN),1985年由Tarjan提出。对于单独的一步复杂度可能到达O(N)。其严格证明比较麻烦。splay树不仅提供了平衡二叉树的功能,还可以翻转区间的元素,求区间第K个元素等操作,并且实现起来也较为简单。

数据结构

如果需要翻转某个区域的值,则需要懒标记mark(和线段树中的懒标记类似)。

struct{
	int val;
	int ff;//父结点
	int ch[2];	//ch[0]左孩子,ch[1]右孩子
	int cnt;	//数量,当前结点的值重复的次数 
	int size;	//大小,当前结点cnt以及两个孩子的size(通过pushup更新)
	int mark;	//(翻转区间)懒标记(通过pushdown传给子结点)
}t[maxn];
int root = 0;	//根节点
int tot = 0;	//新添加的结点放入t[tot]中

核心操作

pushup

该操作的目的是更新当前结点的size。

void pushup(int x){
	t[x].size = t[t[x].ch[0]].size + t[x].cnt + t[t[x].ch[1]].size;
	return;
}

pushdown

该操作的目的是把当前结点的懒标记传递给孩子。

void pushdown(int x){	//把懒标记推到孩子上 
	if(t[x].mark){
        t[t[x].ch[0]].mark^=1;
        t[t[x].ch[1]].mark^=1;
        t[x].mark=0;
        swap(t[x].ch[0],t[x].ch[1]);
	}
	return;
}

rotate

rotate(x)的作用是将节点x向上旋转一步(本质是左右旋,不改变BST的性质)。下面的代码用到了很多小技巧。可以处理六种结构。参考了splay入门解析

void rotate(int x){	//x向上旋转一步 
	int y = t[x].ff;	//y是x的父亲 
	int z = t[y].ff;	//z是y的爷爷 
	
	pushdown(y);
	pushdown(x);
	
	int k = t[y].ch[1]==x; //k=0,x是左孩子,k=1,x是右孩子
	t[z].ch[t[z].ch[1]==y] = x;//y的位置会变成x 技巧:t[z].ch[1]==y的值表示了y是左右孩子
	t[x].ff = z;	//x的父亲变成z
	t[y].ch[k] = t[x].ch[k^1];	//如果k是0,则y获得了x的右孩子,如果k是1,y获得了x的左孩子
	t[t[x].ch[k^1]].ff = y;	//更新从x->y的那个孩子的父亲
	t[x].ch[k^1] = y;	//更新y为x的孩子 
	t[y].ff=x;	//更新y的父亲
	pushup(y);
	//pushup(x); x在splay操作后更新一次就够了 
	return;
}
//上述代码使用了很多小技巧,可以解决所有情况(如下六种情况 )
//需要更新的结点总结:z.ch;x.ff ;y.ch ;x.ch;x.ch.ff;y.ff; 
/*  rotate(x) 函数可以解决下列6种情况,使得x向上旋转一步。
		z    z  z  z       
 	   /    /   \  \ 
      y    y     y  y     y  y
     /     \    /   \    /   \
    x       x  x     x  x     x 
*/

splay

该步骤为最核心的一个步骤,通过不断的调用rotate,将结点上移到想要的位置。
spaly(x,y) 表示将将x旋转为y的儿子(如果y是0,则旋转到根节点)
注意:根据splay的定义,如果x的父亲和x同时是左儿子或者同时是右儿子 ,则先旋转x的父亲再旋转儿子(旋转x的父亲时,x也会被带着一起向上移动了一层)

void splay(int x,int goal){		//错误的结点x,goal可能会引发死循环!
	//将x旋转为是goal的孩子,如果goal为0,则旋转到根。 
	while(t[x].ff!=goal){
		int y = t[x].ff;
		int z = t[y].ff;
		if(z!=goal){	//y不是根节点 
			(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
			//如果xy同左或者同右,则先旋转y,(定义)
			//在转y的时候会把x也向上代了一层。所以要判断 z!=goal
		}
		rotate(x);	//把x上旋一个结点 
	}
	pushup(x);
	if(goal==0) root = x;
	return;
}

splay树相关操作

查找

查找和普通BST类似,只不过需要把与查找到的元素splay到根(如果查不到,则把最后遇到的叶子结点splay到根(这时这个结点不是前驱就是后驱))。

void find(int x){	//查找x的位置,并将其旋转到根节点。
	int u = root;
	if(!u) return;	//树空
	while(t[u].ch[x>t[u].val]&&x!=t[u].val)	//当存在儿子,且不等 
		u = t[u].ch[x>t[u].val];	
	splay(u,0);	//把当前位置旋转到根节点 
}

插入

void insert(int x){	//插入x
	int u=root,ff=0;
	while(u&&t[u].val!=x){//当u存在并且没有移动到当前的值
		ff = u;
		u = t[u].ch[x>t[u].val];//大于当前位置则向右找,否则向左找
	}
	//不存在这样的结点,则创建一个结点插进去 
	u = ++tot;	//新结点
	if(ff)	//如果父结点非根节点 
		t[ff].ch[x>t[ff].val] = u;
	t[u].ch[0] = t[u].ch[1] = 0;
	t[tot].ff = ff;
	t[tot].val = x;
	t[tot].size = 1;
	splay(u,0);	//根结点移动到根,保持结构的平衡。 
}

删除

void Delete(int x){	//默认需要删除的结点一定存在。 
    int last=Next(x,0);	//查找x的前驱
    int next=Next(x,1);	//查找x的后继
    splay(last,0);
	splay(next,last);
    //将前驱旋转到根节点,后继旋转到根节点下面
    //很明显,此时后继是前驱的右儿子,x是后继的左儿子,并且x是叶子节点
    int del=t[next].ch[0];//后继的左儿子
    if(t[del].cnt>1){
        t[del].cnt--;//直接减少一个
        splay(del,0);//旋转
    }
    else
        t[next].ch[0]=0;//这个节点直接删去
    splay(next,0);	//通过splay继而也更新了size 
    return;
}//注意:此时结点虽然被删去了,但是那个空间没有被释放掉。还占据着数组中的位置 

前驱和后驱

int Next(int x,int f){	//f=0:查找x的前驱,f=1:查找x的后继 
	find(x);//把x旋转到根节点 
	int u = root;
	if(t[u].val>x&&f) return u;//如果当前结点的值大于x,并且要查找的是后继
	if(t[u].val<x&&!f) return u;	//如果当前结点的值小于x,并且要找的是前驱
	u = t[u].ch[f];
	while(t[u].ch[f^1]) 	//反着跳转 
		u = t[u].ch[f^1];
	return u;
}
//注意:此处不splay了,因为find的时候已经splay过了。 
//为了防止跳转是第一个前驱,在开始时候插入一个-INF 和INF挺好的

第k个元素

int kth(int x){	//查找树中第k个数 
    int u=root;
    while(1){
    	pushdown(u);
        int y=t[u].ch[0];//左儿子
        if(x>t[y].size+1){
        //如果排名比左儿子的大小和当前节点的数量要大
            x-=t[y].size+1;//数量减少
            u=t[u].ch[1];//那么当前排名的数一定在右儿子上找
        }
        else//否则的话在当前节点或者左儿子上查找
            if(t[y].size>=x)//左儿子的节点数足够
                u=y;//在左儿子上继续找
            else//否则就是在当前根节点上
                return u;	//注意,这里是获得当前结点!!!!!!!!!!!!!!!!!!!!!!!!! 
    }
}

区间翻转

注意:因为树中插入了-INF和INF,因此[l,r]对应的树上的区间是[l+1,r+1]。

void Work(int l,int r){	
    l=kth(l);
    r=kth(r+2);	
    splay(l,0);
    splay(r,l);
    t[t[t[root].ch[1]].ch[0]].mark^=1;	//打上标记
	return; 
}

区间求和

也可以用懒标记解决,等我有空补了例题里的维护数列再接着写吧

例题

P3369 【模板】普通平衡树
P3391 【模板】文艺平衡树
(紫题,较难,待做)P2042 [NOI2005]维护数列

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值