Splay入门

目录

  • Splay入门

    • BST与Splay
    • Rotate
    • Splay
    • 查找操作
    • 插入
    • Update
    • 前驱/后驱
      • 前驱
      • 后驱
    • 删除
    • 第k大
    • 参考

Splay入门

BST与Splay

二叉查找树(BST),保证任意节点的左儿子小于其父亲,任意节点的右儿子大于其父亲的二叉树。但是当出现毒瘤数据时,BST会退化为链,从而影响效率。而Splay是其中的一种比较万能的填坑方法。

Rotate

Splay基本旋转操作。在不破坏二叉查找树(BST)结构的前提下,将一个节点向上旋转一层,使其曾经的父亲成为他现在的儿子(图中x节点)

这种旋转模式可以找出普遍规律的,这里不多阐述,引用一下yyb神犇总结的

1.X变到原来Y的位置

2.Y变成了 X原来在Y的 相对的那个儿子

3.Y的非X的儿子不变 X的 X原来在Y的 那个儿子不变

4.X的 X原来在Y的 相对的 那个儿子 变成了 Y原来是X的那个儿子

请结合图和代码理解一下

void Rotate(int x){//旋转节点x
    //k表示x是否为y的右节点;y即图中y节点,x即图中x节点,z即图中A节点
    int y=ff[x],z=ff[y],k=(ch[y][1]==x);
    //将x与y位置互换,并更新其父亲
    ch[z][ch[z][1]==y]=x;
    ff[x]=z;
    //将图中D节点从x的右儿子变为y的左二子,k^1表示0,1取反(0^1=1,1^1=0)
    ch[y][k]=ch[x][k^1];
    ff[ch[x][k^1]]=y;
    //将y更新
    ch[x][k^1]=y;
    ff[y]=x;
}
/*
ff[x]表示x的父亲
ch[x][1]表示x的右节点
ch[x][0]表示x的左节点
1^1=0 1^0=1
*/

这样,每次有新节点加入、删除或查询时,都将其旋转至根节点,这样可以保持BST的平衡。

Splay为什么能让BST保持平衡玄学原理很多博客未提及。自己yy了一天,搞出了个理由,表述不严谨,意会一下。粗略证明:对于随机生成的数据,裸BST本来就可以平衡,而Splay这种旋转行为的本身对于数据也是随机性的,所以最后还是可以平衡;对于毒瘤单调递增或递减的数据,裸BST不能平衡,效率低的原因可以看做是因为树退化成链,也可以看做是因为每一个新节点在插入时都需要比较一些严重脱离当前插入数据范围(趋势)的数据(如插入1,2,3……10,1000,1001,1002……1010时,每次插入大于等于1000的数时,裸BST每次都要先和前10个比较大小,但是其实这是不必要的,因为前10个数远小于插入的数,如果像这样每次都要访问这些低频节点,会大大增加其复杂度),而每次的Splay操作就是使根节点尽量符合当前插入数据的趋势,避免冗余的比较,让那些低频节点访问次数降低。证毕

Splay

然而单纯的Rotate操作还是不够,有些情况需要考虑,同上,记y为当前需要旋转的节点x的父亲,z为y的父亲(也是x的祖父),\(k(x,y)\)表示节点x,y的关系(x为y的右儿子还是为y的左儿子),特别的,当\(k(x,y)=k(y,z)\)(或者即x,y,z三点共线)时,两次单旋对于复杂度没有优化,如图:

我们必须要先将其父节点向上旋转一次,再将要旋转的节点向上旋转一次,如图:

其他情况则直接做两次旋转即可

inline void splay(int x, int goal){ //将x旋转直至成为goal的儿子
    while(ff[x]!=goal){
        int y=ff[x],z=ff[y];
        if(z!=goal) //如果y已经是根节点的儿子了,那么只需要将x向上旋转一次就好了,不需要两次旋转
            ((ch[z][0]==y)^(ch[y][0]==x))?rotate(x):rotate(y); //x,y,z三点共线是否三点一线
        rotate(x);//再旋转一下
    }
    if(goal==0) rot=x; //更新树根(0是树根的父亲)
}

查找操作

非递归,比较简单,查找后,平衡树的根(rot)就是查找到的节点

/*
rot维护了这棵平衡树的树根
val[x]获取节点x的值
*/
inline void find(int x){
    int u=rot; //rot为树根
    if(u==0)    return; //树空
    while(ch[u][x>val[u]]!=0&&x!=val[u]) //节点存在(即不为0)并且不是x,才进入到下一层
        u=ch[u][x>val[u]]; //进入到相应的子树中
    splay(u,0); //每次查询都要将节点旋转至树根,原理前文已提
}

插入

inline void insert(int x){
    int fa=0,u=rot;
    while(u!=0&&x!=val[u]){
        fa=u;
        u=ch[u][x>val[u]];
    }
    if(u!=0) //x存在
        cnt[u]++; //已有x,那么增加其个数
    else{ //没有x存在
        u=tot++; //分配一个新的节点编号
        if(fa==0) //新建一个树根
            rot=u; //更新树根
        else //新建叶节点
            ch[fa][x>val[fa]]=u; //更新其父亲的信息
        //维护节点的其他信息
        val[u]=x;
        ff[u]=fa;
        cnt[u]=1;
        size[u]=1;
        //ch[u][0]=ch[u][1]=0;
    }
    splay(u,0);
}

Update

根据Splay自底向上旋转的性质,根据左右儿子的节点大小(size)以维护当前节点大小(用于求第k小问题)

void update(int x){
    size[x]=size[ch[x][0]]+sizep[ch[x][1]]; //左右儿子
}

每次Rotate改变树形状时调用

NEW Rotate

void Rotate(int x){
    //代码不变
    int y=ff[x],z=ff[y],k=(ch[y][1]==x);
    ch[z][ch[z][1]==y]=x;
    ff[x]=z;
    ch[y][k]=ch[x][k^1];
    ff[ch[x][k^1]]=y;
    ch[x][k^1]=y;
    ff[y]=x;
    //只有节点x,y的大小发生了变化(看图)
    update(y),update(x);
}

前驱/后驱

前驱:比x小的最大节点;后驱:比x大的最小节点

先找到该节点,根据BST性质,其前驱即其左子树最右边的节点(进入其左儿子之后一直向右转),其后驱即其右子树最左边的节点(进入其右儿子之后一直向左转)

前驱

inline int pre(int x){
    find(x); //查找后,此时树根即为查询节点
    int u=ch[rot][0]; //进入左子树
    if(u==0)    return -1; // 没有比x小的数
    while(ch[u][1]!=0) u=ch[u][1]; //一路向右
    return u;
}

后驱

inline int nxt(int x){
    find(x); //查找后,此时树根即为查询节点
    int u=ch[rot][1]; //进入右子树
    if(u==0)    return -1; // 没有比x大的数
    while(ch[u][0]!=0) u=ch[u][0]; //一路向左
    return u;
}

删除

根据前驱后驱的性质可得

\[
MIN,\cdots,pre(x),x,nxt(x),\cdots,MAX
\]

(即同时满足\(pre(x) < x < nxt(x)\)的x只有一个)那么我们可以根据这个性质x这一个节点夹逼到某个确定的位置,然后干净地干掉(无需维护其他信息)

具体先将x的前驱旋至树根,再旋转x的后驱,使x的后驱成为树根的儿子,这时我们会发现x被夹逼到树根的右儿子的左儿子(或者后驱节点的左儿子)

inline void delete(int x){
    int xp=pre(x),xn=nxt(x);
    splay(xp, 0); //将x的前驱旋至树根
    splay(xn, rot); //旋转x的后驱,使x的后驱成为树根的儿子
    int u=ch[xn][0]; //即将被删除的节点
    if(cnt[u]>1){ //如果不止一个节点
        cnt[u]--; //那么将其个数减一即可
        splay(u,0); //记得Splay!
    }else
        ch[xn][0]=0; //干净地干掉
}

第k大

inline int findk(int x){
    int u=rot;
    if(size[u]<x)   return -1; //不存在
    while(1){
        if(x<=size[ch[u][0]]+cnt[u]) u=ch[u][0]; //如果左子树大小加节点副本数(cnt)大于x,那么第k大一定在左子树中,进入左子树
        else if(x==size[ch[u][0]]+cnt[u])   return u; //如果左子树大小加节点副本数(cnt)恰等于x,那么第k大就是当前节点
        else u=ch[u][1], x-=size[ch[u][0]]+cnt[u]; //如果左子树大小加节点副本数(cnt)小于x,那么第k大一定在右子树中,进入左子树,但是要同时减去左子树的个数
    }
}

参考

个人觉得写的很好的博客:

本文采用 知识共享 署名-非商业性使用-相同方式共享 3.0 中国大陆 许可协议进行许可。欢迎转载,请注明出处: 转载自:Santiego的博客

原文地址:https://www.cnblogs.com/santiego/p/10011592.html

时间: 2024-08-11 21:50:26

Splay入门的相关文章

BZOJ1588 HNOI2002 营业额统计 [Splay入门题]

[HNOI2002]营业额统计 Time Limit: 5 Sec  Memory Limit: 162 MBSubmit: 4128  Solved: 1305 Description 营业额统计 Tiger最近被公司升任为营业部经理,他上任后接受公司交给的第一项任务便是统计并分析公司成立以来的营业情况. Tiger拿出了公司的账本,账本上记录了公司成立以来每天的营业额.分析营业情况是一项相当复杂的工作.由于节假日,大减价或者是其他情况的时候,营业 额会出现一定的波动,当然一定的波动是能够接受

tyvj 1185 营业额统计 splay入门

第一道splay,算是学会了最最基础的splay操作. 有一点要特别注意,就是一字型旋转的时候要先旋转y再旋x,这样复杂度降低很多...不要写成两次都旋转x... #include<bits/stdc++.h> #define REP(i,a,b) for(int i=a;i<=b;i++) #define MS0(a) memset(a,0,sizeof(a)) using namespace std; typedef long long ll; const int maxn=1000

Splay 入门题

H - 营业额统计 解题方法: 每次找 min(大于a的最小值, 小于a 的最大值) 代码: #include<iostream> #include<cstdio> #include<cstring> #include<math.h> using namespace std; const int maxn=500010; const int inf=0x3f3f3f3f; struct SplayTree { int rt,id; int son[maxn]

Splay伸展树学习笔记

Splay伸展树 有篇Splay入门必看文章 —— CSDN链接 经典引文 空间效率:O(n) 时间效率:O(log n)插入.查找.删除 创造者:Daniel Sleator 和 Robert Tarjan 优点:每次查询会调整树的结构,使被查询频率高的条目更靠近树根. Tree Rotation 树的旋转是splay的基础,对于二叉查找树来说,树的旋转不破坏查找树的结构. Splaying Splaying是Splay Tree中的基本操作,为了让被查询的条目更接近树根,Splay Tree

【HDOJ】3487 Play with Chain

Splay入门题目,区间翻转,区间分割. 1 /* */ 2 #include <iostream> 3 #include <string> 4 #include <map> 5 #include <queue> 6 #include <set> 7 #include <stack> 8 #include <vector> 9 #include <deque> 10 #include <algorithm

Splay伸展树入门(单点操作,区间维护)

ps:终于学会了伸展树的区间操作,做一个完整的总结,总结一下自己的伸展树的单点操作和区间维护,顺便给未来的总结复习用. splay是一种平衡树,[平均]操作复杂度O(nlogn).首先平衡树先是一颗二叉搜索树,刚刚开始学的时候找题hash数字的题先测板子... 后来那题被学长改了数据不能用平衡树测了...一道二分数字的题. 二叉搜索树的功能是,插入一个数字,在O(logn)的时间内找到它,并操作,插入删除等.但是可能会让二叉搜索树退化成链,复杂度达到O(n) 原文地址:https://www.c

文艺平衡树 lg3391(splay维护区间入门)

splay是支持区间操作的,先做这道题入个门 大多数操作都和普通splay一样,就不多解释了,只解释一下不大一样的操作 #include<bits/stdc++.h> using namespace std; #define INF 0x3f3f3f3f inline int read(){ int w=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-') f=-1; ch=getchar(); } while(c

splay简单入门

照刘汝佳黑书学了下$splay$, 简单总结一下. $splay$每次操作不保证复杂度, 但均摊每次是$O(logn)$的. $splay$基本思想是每个结点被访问时, 使用$AVL$的旋转操作把它移动到根.$splay$的旋转与$AVL$的区别主要是由于$splay$的旋转是自底向上的, 所以需要设置父亲指针, 而不像$AVL$树那样以儿子为轴旋转. $splay$核心是函数$splay(x,S)$. 它的作用是保证splay结构的情况将$x$旋转到根, 旋转过程要分三种情况讨论. 1, 节点

bzoj 1588 平衡树 splay

1588: [HNOI2002]营业额统计 Time Limit: 5 Sec  Memory Limit: 162 MBSubmit: 15446  Solved: 6076[Submit][Status][Discuss] Description 营业额统计 Tiger最近被公司升任为营业部经理,他上任后接受公司交给的第一项任务便是统计并分析公司成立以来的营业情况. Tiger拿出了公司的账本,账本上记录了公司成立以来每天的营业额.分析营业情况是一项相当复杂的工作.由于节假日,大减价或者是其