Splay树

对于被AVL虐得像那啥一样的我们,Splay的到来是无疑是拯(huo)救(shang)人(jiao)民(you)。


Splay树,又称伸展树,事实上,它根本就不是平衡树!然而它的平均时间复杂度确是O(log n)。


唯一和AVL树一样的是:转转转,转转转……

#----------------------------------------------------------------------------------------------#

如果你不知道什么是图,你可以再见了

如果你不知道什么是树,你可以再见了

如果你不知道什么是二叉树,你可以再见了

#----------------------------------------------------------------------------------------------#

如果你不知道什么是二叉排序树,那,就讲讲吧:

专业解释(二叉查找树就是二叉排序树):

二叉查找树是指具有下列性质的非空二叉树
①若根结点的左子树不空,则左子树的所有结点值均小于根结点值
②若根结点的右子树不空,则右子树的所有结点值均不小于根结点值;
③根结的左右树也分别为二叉排序树;
显然,对二叉排序树进行中序遍历,可得出结点值递增的排序序列。

那是我们不希望看到的,所以:

设该结点为x,值为num:

①x的左子树中所有结点的值小于num

②x的右子树中所有结点的值大于num

那么就有趣了:这个二叉树的中序遍历是一个上升(不下降)序列。

当然也可以左子树大于num,右子树小于num,那么中序遍历就是一个下降(不上升)序列。

下图即是二叉查找树


至于怎么写,网上两堆(大于一堆)

#----------------------------------------------------------------------------------------------#

如果你不知道平衡树

因为我们发现:普通二叉查找树有很多优点,比如插入删除查找

但是有的时候,比如树退化成了链(对于万恶的出数据者来说,这是很容易出现的)时,以上操作就很耗时(很耗时?很好使?)了,几乎为O(N)。

那么很么时候能快一些呢,容易发现:在当前结点的左右子树相差尽量小时,以上操作就快了。

简单说:平衡树就是高度为O(log n)的树。

也可以这样说:

设当前结点左子树高度为h1,右子树高度为h2,则|h1-h2| ≤ 1,这个|h1-h2|称为该结点的平衡因子

如果每次都能保持这样,就快了(O(log n))


ps.二叉搜索树也是二叉查找树

#----------------------------------------------------------------------------------------------#

至于实现,请看下面(以下所有平衡树都指左儿子比自己小,右儿子比自己大的二叉查找树):

#----------------------------------------------------------------------------------------------#

如果你不知道AVL:

事实上,AVL平衡树就是:每插入一个结点便进行一次边的“旋转”,使高度平衡。

旋转,分左旋(zag),右旋(zig),左旋右旋(zagzig),右旋左旋(zigzag)




使用的条件是什么呢?在图上不难看出:


简单点说:

"/"用zig,""用zag,"<"用zigzag,">"用zagzig

至于实现,不用在网上找了,基本上没有,我也不会给你的

int zig(int r)//右旋 // 参数为要旋转的“根”
{
	int t=tree[r].lc;
	tree[r].lc=tree[t].rc;//左儿子接在左儿子的右儿子上
	tree[t].rc=r;//当前结点接到它左儿子的右儿子上
	tree[r].h=max(tree[tree[r].rc].h,tree[tree[r].lc].h)+1;//更新高度(深度)
	tree[t].h=max(tree[tree[t].rc].h,tree[tree[t].lc].h)+1;
	return t;//返回现在的“根”
}
int zag(int r)//左旋
{
	int t=tree[r].rc;
	tree[r].rc=tree[t].lc;
	tree[t].lc=r;
	tree[r].h=max(tree[tree[r].rc].h,tree[tree[r].lc].h)+1;
	tree[t].h=max(tree[tree[t].rc].h,tree[tree[t].lc].h)+1;
	return t;//与右旋完全相反
}
int zagzig(int r)//右旋左旋
{
	tree[r].lc=zag(tree[r].lc);//先左旋左儿子
	return zig(r);//在右旋自己
}
int zigzag(int r)//左旋右旋
{
    tree[r].rc=zig(tree[r].rc);
    return zag(r);//与右旋左旋相反
}


可以自己画图验证。

所以,插入实现:

int insert(int x,int root)
{
    if(!root)//到了叶子
    {
        tree[++cnt].num=x;
        tree[cnt].h=1;//新加入一个结点
        return cnt;
    }
    if(x<tree[root].num)//小于当前值=>找左儿子
    {
        tree[root].l=insert(x,tree[root].l);//递归找左儿子
        if(tree[tree[root].l].h==tree[tree[root].r].h+2)//不平衡了
        {
            if(tree[tree[root].l].num>x) root=zig(root);//出现了"/"型
            else if(tree[tree[root].l].num<x) root=zagzig(root);//出现了"<"型
        }
    }
    else if(x>tree[root].num)//大于当前值=>找右儿子
    {
        tree[root].r=insert(x,tree[root].r);
        if(tree[tree[root].r].h==tree[tree[root].l].h+2)
        {
            if(tree[tree[root].r].num<x) root=zag(root);
            else if(tree[tree[root].r].num>x) root=zigzag(root);
        }//与上面类似
    }
    tree[root].h=max(tree[tree[root].l].h,tree[tree[root].r].h)+1;//更新高度
    return root;//返回现在的根
}

那么,删除怎么写呢?这就有毛病了:极其麻烦……

void maintain(int &root)//先看下面的dale
{
    if(tree[tree[root].l].h==tree[tree[root].r].h+2)
    {
        int t=tree[root].l;
        if(tree[tree[t].l].h==tree[tree[root].r].h+1)
            root=zig(root);
        else if(tree[tree[t].r].h==tree[tree[root].r].h+1)
        {
            tree[root].l=zag(tree[root].l);
            root=zig(root);
        }
    }
    else if(tree[tree[root].l].h+2==tree[tree[root].r].h)
    {
        int t=tree[root].r;
        if(tree[tree[t].r].h==tree[tree[root].l].h+1)
            root=zag(root);
        else if(tree[tree[t].l].h==tree[tree[root].l].h+1)
        {
            tree[root].r=zig(tree[root].r);
            root=zag(root);
        }
    }
    tree[root].h=max(tree[tree[root].l].h,tree[tree[root].r].h)+1;//与插入的调整类似
}
int dale(int &root,int x)//删除值为x的结点
{
    int t;
    if(x==tree[root].num||(x<tree[root].num&&tree[root].l==0)||(x>tree[root].num&&tree[root].r==0))//找到这个结点或者找到了它“可能”在的地方:比当前节点大就找右儿子,反之则找左儿子
    {
        if(tree[root].l==0||tree[root].r==0)//左右儿子中至少一个为空
        {
            t=tree[root].num;
            root=tree[root].l+tree[root].r;//删除
            return t;
        }
        else
            tree[root].num=dale(tree[root].l,x);//否则继续找
    }
    else
    {
        if(x<tree[root].num) t=dale(tree[root].l,x);
        else t=dale(tree[root].r,x);
    }
    maintain(root);//调整根(保持平衡)
    return t;
}//注意,这个删除不管有没有值为x的结点,都会删掉一个结点,如果没有值为x的结点,它会删掉x的前驱


AVL就是这些了,例题:宠物收养所

#----------------------------------------------------------------------------------------------#

接下来是Splay:

之前也说了,Splay也是转转转。

但是,对于AVL,调整最多只是一次双旋,而Splay,是直接把此节点转到根(或其他位置)上(称为“伸展”)!

zig和zag是没有区别的,zigzag,zagzig有一点区别:AVL为先转下面(儿子),再转上面(自己),而Splay的双旋是先转上面,再转下面,为什么?time!time!time!要快一些。

显然,要把结点转到根上是要转很多次的,你当然可以选择每次都zig或者zag,但是time!time!time!能双旋就尽量双旋,原因已说3遍。

所以,Splay根本就不是平衡树,可以说是“伪”平衡树,但多次操作后时间复杂度是极其接近O(log n)的。

而且,很多操作,对于Splay是很容易,对于AVL却是极难的。

如:

①合并两棵树A,B(A中所有结点都小于B中所有结点):把A中最大的结点伸展到根,很显然它是没有右儿子的,然后直接使B的根为它的右儿子即可。

②以结点x为界,分为两棵树:将x伸展到根,剪断左右儿子即可。

③对区间[x,y]进行操作:找到x的前驱xl,y的后继yn,将xl伸展到根,yn伸展到根(xl)的右儿子处(先不着急怎么实现),这样根的右儿子的左子树就是区间[x,y]了,很容易理解。


接下来就是实现了,其实如果写的漂亮,50行(和AVL的150行比起来根本不算什么)以内的基本代码是可以的(造福人民啊啊),但是,如果你按我刚刚说的一点一点码,我觉得:你还是写AVL吧……不要有歧义,我的意思不是copy……


此题为“营业额统计”,题目在网上可以找到:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define MAXN 100005
#define INF 10000000
int ch[MAXN][2],fa[MAXN],sale[MAXN];//ch为左右儿子(存在0列和1列),fa是父亲,sale是结点的值
int ans,tot,root;//root存根的位置
#define min(a,b) ((a)>(b)?(b):(a))
void rotato(int x)//这个旋转包括了zig,zag,zigzag,zagzig的所有情况
{
    if(x==0||fa[x]==0)return;
    int y=fa[x],z=fa[y];
    bool flag=(ch[y][1]==x);
    ch[y][flag]=ch[x][!flag];
    if(ch[y][flag])fa[ch[y][flag]]=y;
    ch[x][!flag]=y;
    fa[y]=x;
    fa[x]=z;
    if(z)ch[z][ch[z][1]==y]=x;//同样自己用一组数据试一试就知道了
}
void splay(int x,int goal)//goal为旋转到的位置的父亲的位置(解决了刚刚旋转位置不一定是跟的情况,根的话goal就为0)
{
    for(int y;(y=fa[x])!=goal;rotato(x))//由于用了很多类似“a[x==y]”“(x=y)==z”的语句,可能会比标准写法或者AVL树慢几毫秒
    {
        int z;
        if((z=fa[y])!=goal)
        {
            if((ch[z][1]==y)==(ch[y][1]==x))//看是左儿子还是右儿子
                rotato(y);
            else rotato(x);
        }
    }
    if(goal==0)root=x;//如果是旋转到根,那root就要改一下
}
int insert(int r,int x)//在插入时顺便找到相差最小的值
{
   int tmp=INF,f=0;
    while(r&&sale[r]!=x)
    {
        f=r;
        if(x<sale[r])
        {tmp=min(sale[r]-x,tmp);
         r=ch[r][0];
        }
        else
        {
            tmp=min(x-sale[r],tmp);
            r=ch[r][1];
        }
    }//递归会极其(是的,直接TLE)耗时
    if(r==0)//如果找到根(没有x(因为有可能树中已经有x了,就不用再插入))
    {
        sale[++tot]=x; 
        r=tot;
        fa[r]=f;
        if(sale[f]>x)ch[f][0]=tot;
        else ch[f][1]=tot;
        splay(r,0);//伸展
        return tmp;
    }
    else
    {
        splay(r,0);
        return 0;
    }
}
int main()
{
    int n,t;
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {
        scanf("%d",&t);
        if(i) ans+=insert(root,t);
        else
        {
	        ans=t;
	        insert(root,t);
        }
    }
    printf("%d
",ans);
}

这是AVL代码:

#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
#define MAXN 100000
  
struct node
{
    int num;
    int l,r,h;
}tree[MAXN+5];
int N,R;
int cnt;
bool F;
  
void read(int &x)
{
    int f=1; x=0; char s=getchar();
    while(s<'0'||s>'9'){if(s=='-') f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f;
}
  
int zig(int x)
{
    int t=tree[x].l;
    tree[x].l=tree[t].r;
    tree[t].r=x;
    tree[x].h=max(tree[tree[x].r].h,tree[tree[x].l].h)+1;
    tree[t].h=max(tree[tree[t].r].h,tree[tree[t].l].h)+1;
    return t;
}
int zag(int x)
{
    int t=tree[x].r;
    tree[x].r=tree[t].l;
    tree[t].l=x;
    tree[x].h=max(tree[tree[x].r].h,tree[tree[x].l].h)+1;
    tree[t].h=max(tree[tree[t].r].h,tree[tree[t].l].h)+1;
    return t;
}
int zigzag(int x)
{
    tree[x].r=zig(tree[x].r);
    return zag(x);
}
int zagzig(int x)
{
    tree[x].l=zag(tree[x].l);
    return zig(x);
}
  
int insert(int x,int root)
{
    if(!root)
    {
        tree[++cnt].num=x;
        tree[cnt].h=1;
        return cnt;
    }
    if(x<tree[root].num)
    {
        tree[root].l=insert(x,tree[root].l);
        if(tree[tree[root].l].h==tree[tree[root].r].h+2)
        {
            if(tree[tree[root].l].num>x) root=zig(root);
            else if(tree[tree[root].l].num<x) root=zagzig(root);
        }
    }
    else if(x>tree[root].num)
    {
        tree[root].r=insert(x,tree[root].r);
        if(tree[tree[root].r].h==tree[tree[root].l].h+2)
        {
            if(tree[tree[root].r].num<x) root=zag(root);
            else if(tree[tree[root].r].num>x) root=zigzag(root);
        }
    }
    tree[root].h=max(tree[tree[root].l].h,tree[tree[root].r].h)+1;
    return root;
}
  
int minx(int x,int root)
{
    if(root==0) return 0x7fffffff;
    if(tree[root].num==x)
    {
        F=1;
        return 0;
    }
    if(tree[root].num<x)
        return min(x-tree[root].num,minx(x,tree[root].r));
    else
        return min(tree[root].num-x,minx(x,tree[root].l));
}
  
int main()
{
    int sum=0;
    read(N);
    for(int i=1;i<=N;i++)
    {
        int x;
        read(x);
        if(i>1)
            sum+=minx(x,R);
        else
            sum+=abs(x);
        if(!F)
            R=insert(x,R);
        F=0;
    }
    printf("%d",sum);
}

没有对比就没有伤害~尽管要慢一些(而且AVL还用了读入优化),但这个整整短了4,50行啊!


好了,不知道这篇和NOIP复习那篇哪篇长一点……


                                                                                                                                                             By WZY

原文地址:https://www.cnblogs.com/LinqiongTaoist/p/7203733.html