普通平衡树学习笔记之Splay算法

一、二叉排序树

1、定义

二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。

二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:

1、若左子树不空,则左子树上所有节点的值均小于它的根节点的值;

2、若右子树不空,则右子树上所有节点的值均大于它的根节点的值;

3、左、右子树也分别为二叉排序树。

下面的这幅图就是一个二叉排序树

普通平衡树学习笔记之Splay算法_第1张图片

2、二叉排序树的查找

二叉排序树查找在在最坏的情况下,需要的查找时间取决于树的深度:

1、当二叉排序树接近于满二叉树时,其深度为\(log_2n\),因此最坏情况下的查找时间为\(O(log_2n)\),与折半查找是同数量级的。

2、但是当二叉树如下图所示形成单枝树时,其深度为\(n\),最坏情况下查找时间为\(O(n)\),与顺序查找属于同一数量级。

普通平衡树学习笔记之Splay算法_第2张图片

所以,为了保证二叉排序树的查找有较高的查找速度,希望该二叉树接近于满二叉树

或者二叉树的每一个节点的左、右子树深度尽量相等

\(Splay\)可以很好地解决这一问题

二、Splay

伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在\(O(log n)\)内完成插入、查找和删除操作。它由丹尼尔·斯立特Daniel Sleator 和 罗伯特·恩卓·塔扬Robert Endre Tarjan 在1985年发明的。

1、结构体定义

struct trr{
    int son,ch[2],fa,cnt,val;
}tr[maxn];

其中\(son\)为儿子数量

\(ch[0]\)为左儿子的编号,\(ch[1]\)为右儿子的编号

\(fa\)为当前节点的父亲节点

\(cnt\)为当前节点的数量

\(val\)为当前节点的权值

2、旋转操作

旋转操作是\(Splay\)中的基本操作

每次有新节点加入、删除或查询时,我们都将其旋转至根节点

这样可以保持\(BST\)的平衡

复杂度证明

我们拿实际的图来演示一下

普通平衡树学习笔记之Splay算法_第3张图片

在这幅图中,\(x\)\(y\)的左儿子,而我们想要将\(x\)旋转至\(y\)的位置

首先,根据\(BST\)的性质,\(x

因此旋转后,\(y\)应该变为\(x\)的右儿子

\(x\)原来的右儿子\(b\)

根据性质有\(x,而\(y\)在旋转后恰好没有左儿子,因此我们让\(b\)\(y\)的左儿子

\(y\)的右儿子\(c\)\(x\)的左儿子\(b\)保持不变即可

旋转后的图变成了下面这个样子

普通平衡树学习笔记之Splay算法_第4张图片

旋转后的图仍满足\(BST\)的性质

但实际上,我们只列举出了\(4\)种情况中的一种

1、\(y\)\(z\)的左儿子,\(x\)\(y\)的左儿子

2、\(y\)\(z\)的左儿子,\(x\)\(y\)的右儿子

3、\(y\)\(z\)的右儿子,\(x\)\(y\)的右儿子

4、\(y\)\(z\)的右儿子,\(x\)\(y\)的左儿子

如果对于每一种情况我们都分别枚举一遍会很麻烦

根据\(yyb\)神犇的总结

1、\(x\)变到原来\(y\)的位置

2、\(y\)变成了 \(x\)原来在\(y\)的相对 的那个儿子

3、\(y\)的非\(x\)的儿子不变 \(x\)\(x\)原来在\(y\)的 那个儿子不变

4、\(x\)\(x\)原来在\(y\)的 相对的 那个儿子 变成了 \(y\)原来是 \(x\)的那个儿子

代码如下

void push_up(int x){
    tr[x].son=tr[tr[x].ch[0]].son+tr[tr[x].ch[1]].son+tr[x].cnt;
    //当前节点儿子数量等于左儿子数量加右儿子数量加当前节点数量
}
void xuanzh(int x){
    int y=tr[x].fa;
    int z=tr[y].fa;
    int k=(tr[y].ch[1]==x);
    //判断x是否是y的右儿子
    tr[z].ch[tr[z].ch[1]==y]=x;
    tr[x].fa=z;//x变到原来y的位置
    tr[y].ch[k]=tr[x].ch[k^1];
    tr[tr[x].ch[k^1]].fa=y;
    //x的原来在x在y的相对位置的那个儿子变成了y原来是x的那个儿子
    tr[x].ch[k^1]=y;
    tr[y].fa=x;
    //y变成了x原来在y的相对的那个儿子
    push_up(y);
    push_up(x);
    //更新节点信息
}

3、将一个节点上旋至规定点

我们是不是对于某一个节点连续进行两次旋转操作就可以呢

一般情况下是可以的,但是如果遇到下面的情况就不可行了

普通平衡树学习笔记之Splay算法_第5张图片

我们要把\(4\)旋转到\(1\)的位置

如果我们一直将\(4\)进行旋转操作,那么旋转两次后的图变成了下面这样

普通平衡树学习笔记之Splay算法_第6张图片

我们会发现\(1-3-5\)这一条链仍然存在

只不过是\(4\)号节点跑到了原来\(1\)号节点的位置

这样的话,\(Spaly\)就失去了意义

因此,我们分情况讨论:

(\(x\)\(y\)的儿子节点,\(y\)\(z\)的儿子节点,将\(x\)旋转到\(z\))

1、\(x\)\(y\)分别是\(y\)\(z\)的同一个儿子

先旋转\(y\)再旋转\(x\)

2、\(x\)\(y\)分别是\(y\)\(z\)不同的儿子

\(x\)旋转两次

代码

void splay(int x,int goal){
//将x旋转至目标节点goal的儿子
    while(tr[x].fa!=goal){
        int y=tr[x].fa;
        int z=tr[y].fa;
        if(z!=goal){
            (tr[y].ch[0]==x)^(tr[z].ch[0]==y)?xuanzh(x):xuanzh(y);
        }
        //分情况讨论:同位置儿子旋转y,不同位置儿子旋转x
        xuanzh(x);
        //最后旋转x
    }
    if(goal==0) rt=x;
    //如果旋转到根节点,将根节点更新为x
}

4、查找操作

类似于二分查找

从根节点开始,如果要查询的值大于该点的值,向右儿子递归

否则向左儿子递归

如果当前位置的值已经是要查找的数,则将该节点旋转至根节点,方便之后的操作

void zhao(int x){
//查找x的位置,并将其旋转至根节点
    int u=rt;
    if(!u) return;//树为空
    while(tr[u].ch[x>tr[u].val] && x!=tr[u].val){
    //当存在儿子并且当前位置的值不等于x
        u=tr[u].ch[x>tr[u].val];//跳转到儿子
    }
    splay(u,0);
    //将当前位置旋转到根节点
}

5、插入操作

和查找操作类似,也是从根节点开始

如果要插入的值大于该点的值,向右儿子递归

否则向左儿子递归

如果可以在原树中找到当前值,把节点的数量加一即可

否则再新建一个节点

void ad(int x){
//插入价值为x的节点
    int u=rt,fa=0;
    while(u && tr[u].val!=x){
        fa=u;
        u=tr[u].ch[x>tr[u].val];
        //向儿子递归
    }
    if(u) tr[u].cnt++;
    //如果当前节点已经存在,节点的个数加一
    else {
    //如果不存在,建立一个新的节点
        u=++tot;
        if(fa) tr[fa].ch[x>tr[fa].val]=u;
        tr[tot].ch[1]=0;
        tr[tot].ch[0]=0;
        tr[tot].val=x;
        tr[tot].fa=fa;
        tr[tot].cnt=1;
        tr[tot].son=1;
    }
    splay(u,0);//将当前节点上旋至根节点
}

6、查询前驱和后继

我们要查询某一个数\(x\)的前驱和后缀

首先我们要使用查找操作,将\(x\)节点旋转到根节点

如果查询前驱,那么前驱就是左子树中权值最大的节点

那我们就从左子树开始,一直向右子树跳,直到没有右子树为止

查询后继也是同样

int qq_hj(int x,int jud){
//jud为0查询前驱,为1查询后缀
    zhao(x);
    //将x旋转至根节点
    int u=rt;
    if((tr[u].val>x && jud) || (tr[u].val

7、删除操作

如果我们要删除某一个数\(x\)

那么这一个数的权值一定介于它的前驱和它的后继之间

所以我们可以先把它的前驱旋转至根节点

然后把它的后继旋转到它的前驱作为前驱的右儿子

这时,前驱的左儿子恰好比前驱大、后继小,正是我们想要删除的值

void sc(int x){
    int qq=qq_hj(x,0);
    //求出前驱
    int hj=qq_hj(x,1);
    //求出后继
    splay(qq,0);
    //将前驱旋转至根节点
    splay(hj,qq);
    //将后继旋转至前驱的右儿子
    int willsc=tr[hj].ch[0];
    //找出要删除的数
    if(tr[willsc].cnt>1){
        tr[willsc].cnt--;
        splay(willsc,0);
    } else {
        tr[hj].ch[0]=0;
    }
    //删除该节点
}

8、查找第k小的值

从根节点开始,如果左子树的儿子数大于\(k\),向左子树查询

否则向右子树查询

递归解决问题即可

int kth(int x){
    int u=rt;
    if(tr[u].sontr[y].son+tr[u].cnt){
            x-=(tr[y].son+tr[u].cnt);
            u=tr[u].ch[1];
            //向右子树查询
        } else {
            if(x<=tr[y].son) u=y;
            else return tr[u].val;
            //向左子树查询
        }
    }
}

练习题(洛谷P3369)

一道很基础的板子题,直接附上代码

#include
using namespace std;
const int maxn=1e6+5;
#define INF 0x3f3f3f3f
struct trr{
    int son,ch[2],fa,cnt,val;
}tr[maxn];
int n,tot,rt;
void push_up(int x){
    tr[x].son=tr[tr[x].ch[0]].son+tr[tr[x].ch[1]].son+tr[x].cnt;
}
void xuanzh(int x){
    int y=tr[x].fa;
    int z=tr[y].fa;
    int k=(tr[y].ch[1]==x);
    tr[z].ch[tr[z].ch[1]==y]=x;
    tr[x].fa=z;
    tr[y].ch[k]=tr[x].ch[k^1];
    tr[tr[x].ch[k^1]].fa=y;
    tr[x].ch[k^1]=y;
    tr[y].fa=x;
    push_up(y);
    push_up(x);
}
void splay(int x,int goal){
    while(tr[x].fa!=goal){
        int y=tr[x].fa;
        int z=tr[y].fa;
        if(z!=goal){
            (tr[y].ch[0]==x)^(tr[z].ch[0]==y)?xuanzh(x):xuanzh(y);
        }
        xuanzh(x);
    }
    if(goal==0) rt=x;
}
void zhao(int x){
    int u=rt;
    if(!u) return;
    while(tr[u].ch[x>tr[u].val] && x!=tr[u].val){
        u=tr[u].ch[x>tr[u].val];
    }
    splay(u,0);
}
void ad(int x){
    int u=rt,fa=0;
    while(u && tr[u].val!=x){
        fa=u;
        u=tr[u].ch[x>tr[u].val];
    }
    if(u) tr[u].cnt++;
    else {
        u=++tot;
        if(fa) tr[fa].ch[x>tr[fa].val]=u;
        tr[tot].ch[1]=0;
        tr[tot].ch[0]=0;
        tr[tot].val=x;
        tr[tot].fa=fa;
        tr[tot].cnt=1;
        tr[tot].son=1;
    }
    splay(u,0);
}
int qq_hj(int x,int jud){
    zhao(x);
    int u=rt;
    if((tr[u].val>x && jud) || (tr[u].val1){
        tr[willsc].cnt--;
        splay(willsc,0);
    } else {
        tr[hj].ch[0]=0;
    }
}
int kth(int x){
    int u=rt;
    if(tr[u].sontr[y].son+tr[u].cnt){
            x-=(tr[y].son+tr[u].cnt);
            u=tr[u].ch[1];
        } else {
            if(x<=tr[y].son) u=y;
            else return tr[u].val;
        }
    }
}
int main(){
    int n;
    scanf("%d",&n);
    ad(INF);
    ad(-INF);
    for(int i=1;i<=n;i++){
        int aa,bb;
        scanf("%d%d",&aa,&bb);
        if(aa==1) ad(bb);
        else if(aa==2) sc(bb);
        else if(aa==3) {
            zhao(bb);
            int ans=tr[tr[rt].ch[0]].son;
            printf("%d\n",ans);
        } else if(aa==4){
            int ans=kth(bb+1);
            printf("%d\n",ans);
        } else if(aa==5){
            int ans=qq_hj(bb,0);
            printf("%d\n",tr[ans].val);
        } else {
            int ans=qq_hj(bb,1);
            printf("%d\n",tr[ans].val);
        }
    }
    return 0;
}

你可能感兴趣的:(普通平衡树学习笔记之Splay算法)