zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    AS/400开发经验点滴(三)如何使用分布式关系数据库
    AS/400开发经验点滴(五)通用日志管理工具
    AS/400开发经验点滴(二)一个批量修改文件属性的工具
    FTP执行AS400命令
    ORA12514 TNS 监听程序当前无法识别连接描述符中请求服务 的解决方法
    Centos 查看系统硬件信息
    [转]Oracle的DBMS_METADATA包
    java读写删.text,.xml文件内容
    oracle 是user_tables里面可以查找到一个表,而用DESC或者insert语句插入时就会报不存在视图。
    Oracle监听服务lsnrctl参数及查询状态详解
  • 原文地址:https://www.cnblogs.com/LinqiongTaoist/p/7203733.html
Copyright © 2011-2022 走看看