【转】 史上最详尽的平衡树(splay)讲解与模板(非指针版spaly)

ORZ原创Clove学姐

变量声明:f[i]表示i的父结点,ch[i][0]表示i的左儿子,ch[i][1]表示i的右儿子,key[i]表示i的关键字(即结点i代表的那个数字),cnt[i]表示i结点的关键字出现的次数(相当于权值),size[i]表示包括i的这个子树的大小;sz为整棵树的大小,root为整棵树的根。

再介绍几个基本操作:

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

inline void clear(int x){
     ch[x][0]=ch[x][1]=f[x]=cnt[x]=key[x]=size[x]=0;
}  

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

inline int get(int x){
     return ch[f[x]][1]==x;
}  

 

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

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

 

下面boss来了:

【rotate操作图文详解】

这是原来的树,假设我们现在要将D结点rotate到它的父亲的位置。

step 1:

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

step 2:

我们知道要将Drotate到B的位置,二叉树的大小关系不变的话,B就要成为D的右结点了没错吧?

咦?可是D已经有右结点了,这样不就冲突了吗?怎么解决这个冲突呢?

我们知道,D原来是B的左结点,那么rotate过后B就一定没有左结点了对吧,那么正好,我们把G接到B的左结点去,并且这样大小关系依然是不变的,就完美的解决了这个冲突。

这样我们就完成了一次rotate,如果是右儿子的话同理。step 2的具体操作:

我们已经判断了D是B的左儿子还是右儿子,设这个关系为K;将D与K关系相反的儿子的父亲记为B与K关系相同的儿子(这里即为D的右儿子的父亲记为B的左儿子);将D与K关系相反的儿子的父亲即为B(这里即为把G的父亲记为B);将B的父亲即为D;将D与K关系相反的儿子记为B(这里即为把D的右儿子记为B);将D的父亲记为A。

最后要判断,如果A存在(即rotate到的位置不是根的话),要把A的儿子即为D。

显而易见,rotate之后所有牵涉到变化的父子关系都要改变。以上的树需要改变四对父子关系,BG DG BD AB,需要三个操作(BG BD AB)。

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

【代码】

inline void rotate(int x){
     int old=f[x],oldf=f[old],which=get(x);
     ch[old][which]=ch[x][which^1];f[ch[old][which]]=old;
     f[old]=x;ch[x][which^1]=old;
     f[x]=oldf;
     if (oldf)
          ch[oldf][ch[oldf][1]==old]=x;
     update(old);update(x);
}  

【splay操作】

其实splay只是rotate的发展。伸展操作只是在不停的rotate,一直到达到目标状态。如果有一个确定的目标状态,也可以传两个参。此代码直接splay到根。

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

inline void splay(int x){
     for (int fa;(fa=f[x]);rotate(x))
          if (f[fa])
               rotate((get(x)==get(fa)?fa:x));
     root=x;
}  

【insert操作】

其实插入操作是比较简单的,和普通的二叉查找树基本一样。

step 1:如果root=0,即树为空的话,做一些特殊的处理,直接返回即可。

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

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

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

inline void insert(int v){
     if (root==0) {sz++;ch[sz][0]=ch[sz][1]=f[sz]=0;key[sz]=v;cnt[sz]=1;size[sz]=1;root=sz;return;}
     int now=root,fa=0;
     while (1){
          if (key[now]==v){
               cnt[now]++;update(now);update(fa);splay(now);break;
          }
          fa=now;
          now=ch[now][key[now]<v];
          if (now==0){
               sz++;
               ch[sz][0]=ch[sz][1]=0;key[sz]=v;size[sz]=1;
               cnt[sz]=1;f[sz]=fa;ch[fa][key[fa]<v]=sz;
               update(fa);
               splay(sz);
               break;
          }
     }
}  

【find操作】查询x的排名

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

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

如果x比当前结点小,即应该向左子树寻找,ans不用改变(设想一下,走到整棵树的最左端最底端排名不就是1吗)。

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

不要忘记了再splay一下

inline int find(int v){
     int ans=0,now=root;
     while (1){
          if (v<key[now])
               now=ch[now][0];
          else{
               ans+=(ch[now][0]?size[ch[now][0]]:0);
               if (v==key[now]) {splay(now);return ans+1;}
               ans+=cnt[now];
               now=ch[now][1];
          }
     }
}  

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

初始化:当前点=root

和上面的思路基本相同:

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

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

inline int findx(int x){
     int now=root;
     while (1){
          if (ch[now][0]&&x<=size[ch[now][0]])
               now=ch[now][0];
          else{
               int temp=(ch[now][0]?size[ch[now][0]]:0)+cnt[now];
               if (x<=temp)
                    return key[now];
               x-=temp;now=ch[now][1];
          }
     }
}  

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

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

其中insert操作上文已经提到。

【pre/next操作】

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

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

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

【del操作】

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

step 1:随便find一下x。目的是:将x旋转到根。

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

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

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

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

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

inline void del(int x){
     int whatever=find(x);
     if (cnt[root]>1) {cnt[root]--;return;}
     //Only One Point
     if (!ch[root][0]&&!ch[root][1]) {clear(root);root=0;return;}
     //Only One Child
     if (!ch[root][0]){
          int oldroot=root;root=ch[root][1];f[root]=0;clear(oldroot);return;
     }
     else if (!ch[root][1]){
          int oldroot=root;root=ch[root][0];f[root]=0;clear(oldroot);return;
     }
     //Two Children
     int leftbig=pre(),oldroot=root;
     splay(leftbig);
     f[ch[oldroot][1]]=root;
     ch[root][1]=ch[oldroot][1];
     clear(oldroot);
     update(root);
     return;
}  

【总结】

平衡树的本质其实是二叉搜索树,所以很多操作是基于二叉搜索树的操作。

splay的本质是rotate,旋转其实只是为了保证二叉搜索树的平衡性。

所有的操作一定都满足二叉搜索树的性质,所有改变父子关系的操作一定要update。

关键是理解rotate,splay的原理以及每一个操作的原理。

所有的操作均来自bzoj3224 普通平衡树  附链接:http://www.lydsy.com/JudgeOnline/problem.php?id=3224

完整代码:

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
#define MAXN 1000000
int ch[MAXN][2],f[MAXN],size[MAXN],cnt[MAXN],key[MAXN];
int sz,root;
inline void clear(int x){
    ch[x][0]=ch[x][1]=f[x]=size[x]=cnt[x]=key[x]=0;
}
inline bool get(int x){
    return ch[f[x]][1]==x;
}
inline void update(int x){
    if (x){
        size[x]=cnt[x];
        if (ch[x][0]) size[x]+=size[ch[x][0]];
        if (ch[x][1]) size[x]+=size[ch[x][1]];
    }
}
inline void rotate(int x){
    int old=f[x],oldf=f[old],whichx=get(x);
    ch[old][whichx]=ch[x][whichx^1]; f[ch[old][whichx]]=old;
    ch[x][whichx^1]=old; f[old]=x;
    f[x]=oldf;
    if (oldf)
        ch[oldf][ch[oldf][1]==old]=x;
    update(old); update(x);
}
inline void splay(int x){
    for (int fa;fa=f[x];rotate(x))
      if (f[fa])
        rotate((get(x)==get(fa))?fa:x);
    root=x;
}
inline void insert(int x){
    if (root==0){sz++; ch[sz][0]=ch[sz][1]=f[sz]=0; root=sz; size[sz]=cnt[sz]=1; key[sz]=x; return;}
    int now=root,fa=0;
    while(1){
        if (x==key[now]){
            cnt[now]++; update(now); update(fa); splay(now); break;
        }
        fa=now;
        now=ch[now][key[now]<x];
        if (now==0){
            sz++;
            ch[sz][0]=ch[sz][1]=0;
            f[sz]=fa;
            size[sz]=cnt[sz]=1;
            ch[fa][key[fa]<x]=sz;
            key[sz]=x;
            update(fa);
            splay(sz);
            break;
        }
    }
}
inline int find(int x){
    int now=root,ans=0;
    while(1){
        if (x<key[now])
          now=ch[now][0];
        else{
            ans+=(ch[now][0]?size[ch[now][0]]:0);
            if (x==key[now]){
                splay(now); return ans+1;
            }
            ans+=cnt[now];
            now=ch[now][1];
        }
    }
}
inline int findx(int x){
    int now=root;
    while(1){
        if (ch[now][0]&&x<=size[ch[now][0]])
          now=ch[now][0];
        else{
            int temp=(ch[now][0]?size[ch[now][0]]:0)+cnt[now];
            if (x<=temp) return key[now];
            x-=temp; now=ch[now][1];
        }
    }
}
inline int pre(){
    int now=ch[root][0];
    while (ch[now][1]) now=ch[now][1];
    return now;
}
inline int next(){
    int now=ch[root][1];
    while (ch[now][0]) now=ch[now][0];
    return now;
}
inline void del(int x){
    int whatever=find(x);
    if (cnt[root]>1){cnt[root]--; update(root); return;}
    if (!ch[root][0]&&!ch[root][1]) {clear(root); root=0; return;}
    if (!ch[root][0]){
        int oldroot=root; root=ch[root][1]; f[root]=0; clear(oldroot); return;
    }
    else if (!ch[root][1]){
        int oldroot=root; root=ch[root][0]; f[root]=0; clear(oldroot); return;
    }
    int leftbig=pre(),oldroot=root;
    splay(leftbig);
    ch[root][1]=ch[oldroot][1];
    f[ch[oldroot][1]]=root;
    clear(oldroot);
    update(root);
}
int main(){
    int n,opt,x;
    scanf("%d",&n);
    for (int i=1;i<=n;++i){
        scanf("%d%d",&opt,&x);
        switch(opt){
            case 1: insert(x); break;
            case 2: del(x); break;
            case 3: printf("%d\n",find(x)); break;
            case 4: printf("%d\n",findx(x)); break;
            case 5: insert(x); printf("%d\n",key[pre()]); del(x); break;
            case 6: insert(x); printf("%d\n",key[next()]); del(x); break;
        }
    }
}
时间: 2024-08-11 17:53:10

【转】 史上最详尽的平衡树(splay)讲解与模板(非指针版spaly)的相关文章

史上最简单的个人移动APP开发入门--jQuery Mobile版跨平台APP开发

书是人类进步的阶梯. ——高尔基 习大大要求新新人类要有中国梦,鼓励大学生们一毕业就创业.那最好的创业途径是什么呢?就是APP.<构建跨平台APP-jQuery Mobile移动应用实战>就是一本写给没钱没身份没资历的创业小白看的APP书,看完这本书你可以拥有自己的一个APP,不用花钱就能移植到其他移动平台,支持iOS,Android,Windows Phone!!!!!!!!找个最便宜的来练手吧!  小白APP交流Q群:  348632872 清华大学出版社推出的<构建跨平台APP:j

史上最详尽的NLP预处理模型汇总

文章发布于公号[数智物语] (ID:decision_engine),关注公号不错过每一篇干货. 转自 | 磐创AI(公众号ID:xunixs) 作者 | AI小昕 编者按:近年来,自然语言处理(NLP)的应用程序已经无处不在.NLP使用率的快速增长主要归功于通过预训练模型实现的迁移学习概念,迁移学习本质上是在一个数据集上训练模型,然后使该模型能够适应在不同的数据集上执行不同的NLP操作.这一突破使得每个人都能轻松地开启NLP任务,尤其是那些没有时间和资源从头开始构建NLP模型的人.所以,使用预

史上最清晰的红黑树讲解(上)

http://www.cnblogs.com/CarpenterLee/p/5503882.html 本文以Java TreeMap为例,从源代码层面,结合详细的图解,剥茧抽丝地讲解红黑树(Red-Black tree)的插入,删除以及由此产生的调整过程. 总体介绍 Java TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的评判可以通过其本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Compara

[bzoj3223]文艺平衡树(splay区间反转模板)

解题关键:splay模板题. #include<cstdio> #include<cstring> #include<algorithm> #include<cstdlib> #include<iostream> #include<cmath> using namespace std; typedef long long ll; const int N = 100005; int ch[N][2],par[N],val[N],cnt[

史上最详细Java内存区域讲解

常见面试题 基本问题 介绍下 Java 内存区域(运行时数据区) Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么) 对象的访问定位的两种方式(句柄和直接指针两种方式) 拓展问题 String类和常量池 8种基本类型的包装类和常量池 一.概述 对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题.正是因为 Java 程序员把内存控制

史上最全最完整的IOS 游戏开发 PDF电子书定制下载

<iOS 5游戏开发>作者:(新西兰)James·Sugrue著 页数:191 出版社:北京市:人民邮电出版社 出版日期:2012.08 简介:<iOS5游戏开发>是一本iOS5游戏开发的基础入门书.全书使用通俗易懂的简单实例,带领读者经历构建经典动作游戏的整个周期.读者在本书的阅读过程中,将经历从开发概念.规划设计一直到编写实际代码的全过过程.本书的每一章,都将演示游戏创建过程中的一个逻辑步骤,读者将在其中学习如何创建Sprite,用触摸屏.重力感应器和屏幕游戏棒控制玩家角色等-

《大神在耳边》身世史上最抠编导组 雅雅、29盗窃袭南京黄花不保?

<大神在耳边>身世史上最抠编导组 雅雅.29盗窃袭南京黄花不保? 寻"球"之旅饱经忧患不遂 名拼盘挑战头巾情绪极限法球 亲闻,这是一档连黄花都不放生的剧目!史上最铁算盘编导组来了,雅少折返大学化身女版泷谷源治,29线下偶逢女粉丝直说"黄花架不住!" 雅雅天诚然认为南京寻"球"之旅便是吃相映成趣好,没思悟居然除非29元经费,毕竟在残暴的本色前面29可不可以Ho德鲁伊住场面地步,用健硕的股肱和伊涅斯塔般的脑力完毕职责,雅雅可不可以顺当带&

网络编程懒人入门(六):史上最通俗的集线器、交换机、路由器功能原理入门

1.前言 即时通讯网整理了大量的网络编程类基础文章和资料,包括<TCP/IP协议 卷1>.<[通俗易懂]深入理解TCP协议>系列.<网络编程懒人入门>系列.<不为人知的网络编程>系列.<P2P技术详解>系列.<高性能网络编程>系列.甚至还有图文并貌+实战代码的<NIO框架入门>等,目的是帮助即时通讯类应用的开发者,至少要掌握网络编程最基本的原理,所谓知其然更要知其所以然.尤其现在移动网络大行其道的时代,在网络环境如此复杂的

淘宝旺旺wwwscansuncn史上第一新闻源垃圾奸商骗子

记得笔者2013年那会也是踌躇满志的从原医疗公司辞职,想轰轰烈烈的干一番大事情,可是却是想轰轰烈烈做淘宝客,哈哈,有木有,想想,笔者当初真的是太幼稚了. 笔者当初的想法好幼稚有木有 要做什么类型的淘宝客吗?当时一共是选择了6类,减肥,丰胸,美白,胶原蛋白,捷易通还有祛痘,当时每天都忙到凌晨2点,主要是搭建程序,还有文章录入,虽然每天很累很累,但是却是觉得值,有意义,每天睡的很香很香. 每天累的啊,睡的好香好香 因为在2013年那会,淘宝客还是可以做的,虽然过了2012年的6.22和6.28之后,