平衡树——Treap

Treap

二叉排序树
二叉排序树是一种很万能的东西,它通过使二叉树的中序遍历为升序来维护一个可插入删除和查询许多信息的序列。具体地,对于插入操作,从根节点开始搜索,对于比根节点小的数,递归它的左子树,对于比根节点大的数,递归它的右子树,知道搜索到一个空位就把这个数塞进去。但是,二叉排序树有一个很致命的地方,由于递归的复杂度是O(深度)的,而树型只要插入序列给定了就确定了,深度可能非常之大,比如给一串升序的数字,显然树会退化成一条链,这样以来,复杂度就完全不靠谱,对于大数据完全会超时,平衡树就是在二叉排序树的基础上通过一些操作来使二叉排序树平衡,也就是使它深度接近logn。
Treap
Treap,其实是tree+heap的意思,tree,就是二叉排序树,heap,就是堆,所以,Treap完全可以被叫做树堆。它的思想是这样的:
为二叉排序树的每一个节点新增一个优先级,而节点存的序列中的数叫做键值,所以,我们这样来存储Treap:

struct node{
    node* ch[2];//指针,用来指向它的左右儿子,0左1右
    int key,f,num,s;//key是键值,f是优先级,num是相同元素的个数,s是子树中元素个树
    void maintain(){//maintain为更新函数,用来更新s值
        s=num;
        if(ch[0]!=NULL)s+=ch[0]->s;
        if(ch[1]!=NULL)s+=ch[1]->s;
    }
}

对于一棵Treap,要使它的键值的中序遍历为升序,并且,优先级满足堆的性质(即父节点大于左右儿子),在插入一个值后,就随机送一个优先级,由于可以证明,在随机插入顺序下,树的深度期望是logn,所以,只要我们能够在每次操作后能够保证Treap的性质,那么它单次操作的复杂度就是期望logn,要维护这样的性质,需要一个新的操作

旋转
由于二叉排序树的性质,导致同一条链无论怎么移动,它都满足中序遍历为升序,麻烦的地方在于,如何旋转一棵树,如图所示:

旋转后二叉排序树的性质显然是满足的,所以,以右旋为例,旋转其实就是把根节点接到它的左儿子的右儿子,再把它左儿子原来的右儿子接到它的左儿子,给出代码:

void rot(pnode &p,int d){//为了方便,pnode就是node*
    pnode k=p->ch[d^1];p->ch[d^1]=k->ch[d];k->ch[d]=p;
    p->maintain();k->maintain();p=k;//最后不要忘记p=k,因为旋转后根节点元素已经改变
}

有了旋转函数,就可以实现将根节点与它的儿子节点交换位置同时不影响到二叉排序树性质,同时又可以利用旋转来实现类似于交换父节点与儿子节点的功能,来维护优先级的堆性质,

插入
像在二叉排序树中一样,我们要先在Treap中找到待插入数的位置把它放下去,然后随机给一个优先级,接下来就是Treap独有的操作了,我们要通过旋转来维护它的堆性质。如果根节点的优先级小被插入的那一棵子树的根节点的优先级,那么就要将树往另外一边旋转,来使优先级大的那个儿子节点称为根节点的父节点。为了方便,我们定义0为左旋,1为右旋,代码如下:

void insert(pnode &p,int x){
    if(p==null)p=newnode(x);else
    if(x==p->key){p->num++;p->s++;}else
    if(x<p->key){
        insert(p->ch[0],x);
        if(p->ch[0]->f<p->f)rot(p,1);
    }else{
        insert(p->ch[1],x);
        if(p->ch[1]->f<p->f)rot(p,0);
    }
    p->maintain();//由于新加入了节点,所以一定要更新s
}

这里需要注意一点,由于直接调用NULL会出错,为了少一些特判,可以自己定义一个新的null,虽然它表示的是空,但是实际让它指向一个已被定义的内存,代码如下:

node nul;
pnode null=&nul;
null->s=0;null->ch[0]=null;null->ch[1]=null;//同时在main函数的开头加入这句话

这样,就可以放心的调用左右儿子而不怕访问无效内存了。

删除
删除也很简单,首先找到需要删除的数,如果它的个数大于1,那么就直接p->num–,否则,如果它只有一棵子树,就直接把它修改为它的子节点,如果它有两棵子树,那么就需要将它旋到叶节点再删除,旋转当然不是随便旋的,由于还要维护堆性质,需要保证旋到根节点的儿子的优先级大于另一个儿子,所以,哪边儿子的优先级小就往哪边旋,代码如下:

void erase(pnode &p,int x){
    if(p->key==x){
        if(p->num>1)p->num--,p->s--;else
        if(p->ch[0]==null)p=p->ch[1];else
        if(p->ch[1]==null)p=p->ch[0];else{
            if(p->ch[0]->key>p->ch[1]->key)rot(p,1),erase(p->ch[1],x);
                                      else rot(p,0),erase(p->ch[0],x);
        }
    }else if(p->key>x)erase(p->ch[0],x);else erase(p->ch[1],x);
    p->maintain();
}

这样,我们就有了插入和删除操作,其他的操作与二叉排序树都是一样的。

名次树的操作:
前驱/后继
前驱就是求小于它的数中最大的数,注意这里的输入的数有可能不在序列中,所以不能简单的找到这个数,然后在它的左子树中找到最右边的数。如果树根的键值大于查询的值,那么久走左子树,否则久走右子树。代码如下:

int pre(pnode p,int x){
    if(p==null)return -1e9;
    if(p->key>=x) return pre(p->ch[0],x);
    return max(pre(p->ch[1],x),p->key);
}

后继与前驱的思想是一模一样的,只是改成求大于它的数中最小的。代码如下:

int sub(pnode p,int x){
    if(p==null)return 1e9;
    if(p->key<=x) return sub(p->ch[1],x);
    return min(sub(p->ch[0],x),p->key);
}

查询数x的排名
只要在找这个数的路上顺带维护处它的排名即可,代码如下:

int geth(pnode p,int x){//查询数x在以p为根的子树中的排名
    if(p->key==x)return p->ch[0]->s+1;else
    if(p->key>x)return geth(p->ch[0],x);
           else return p->ch[0]->s+p->num+geth(p->ch[1],x);
}

查询排名为x的数
与前一个操作差不多,代码如下:

int getn(pnode p,int x){//查询以p为根的子树中排名为x的数 
    if(x>p->ch[0]->s&&x<=p->ch[0]->s+p->num)return p->key;else
    if(x<=p->ch[0]->s)return getn(p->ch[0],x);else
                      return getn(p->ch[1],x-p->ch[0]->s-p->num);
}

附上完整代码:

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#define maxn 100006
using namespace std;
struct node{
    node* ch[2];
    int key,f,num,s;
    void maintain(){
        s=num;
        if(ch[0]!=NULL)s+=ch[0]->s;
        if(ch[1]!=NULL)s+=ch[1]->s;
    }
}treap[maxn];
node nul;
typedef node* pnode;
pnode null=&nul;
pnode len,root;
pnode newnode(int key){
    len->key=key;len->s=1;len->ch[0]=len->ch[1]=null;len->f=rand();len->num=1;
    return len++;
}
void rot(pnode &p,int d){
    pnode k=p->ch[d^1];p->ch[d^1]=k->ch[d];k->ch[d]=p;
    p->maintain();k->maintain();p=k;
}
void insert(pnode &p,int x){
    if(p==null)p=newnode(x);else
    if(x==p->key){p->num++;p->s++;}else
    if(x<p->key){
        insert(p->ch[0],x);
        if(p->ch[0]->f<p->f)rot(p,1);
    }else{
        insert(p->ch[1],x);
        if(p->ch[1]->f<p->f)rot(p,0);
    }
    p->maintain();
}
void erase(pnode &p,int x){
    if(p->key==x){
        if(p->num>1)p->num--,p->s--;else
        if(p->ch[0]==null)p=p->ch[1];else
        if(p->ch[1]==null)p=p->ch[0];else{
            if(p->ch[0]->key>p->ch[1]->key)rot(p,1),erase(p->ch[1],x);
                                      else rot(p,0),erase(p->ch[0],x);
        }
    }else if(p->key>x)erase(p->ch[0],x);else erase(p->ch[1],x);
    p->maintain();
}
int geth(pnode p,int x){//查询数x在以p为根的子树中的排名 
    if(p->key==x)return p->ch[0]->s+1;else
    if(p->key>x)return geth(p->ch[0],x);
           else return p->ch[0]->s+p->num+geth(p->ch[1],x);
}
int getn(pnode p,int x){//查询以p为根的子树中排名为x的数 
    if(x>p->ch[0]->s&&x<=p->ch[0]->s+p->num)return p->key;else
    if(x<=p->ch[0]->s)return getn(p->ch[0],x);else
                      return getn(p->ch[1],x-p->ch[0]->s-p->num);
}
int pre(pnode p,int x){
    if(p==null)return -1e9;
    if(p->key>=x) return pre(p->ch[0],x);
    return max(pre(p->ch[1],x),p->key);
}
int sub(pnode p,int x){
    if(p==null)return 1e9;
    if(p->key<=x) return sub(p->ch[1],x);
    return min(sub(p->ch[0],x),p->key);
}
int n;
int main(){
    freopen("treap.in","r",stdin);
    freopen("treap.out","w",stdout);
    null->s=0;null->ch[0]=null;null->ch[1]=null;len=treap;
    scanf("%d",&n);
    root=null;
    for(int i=1;i<=n;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        if(x==1)insert(root,y);else
        if(x==2)erase(root,y);else
        if(x==3)printf("%d\n",geth(root,y));else
        if(x==4)printf("%d\n",getn(root,y));else
        if(x==5)printf("%d\n",pre(root,y));else
                printf("%d\n",sub(root,y));
    }
    return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值