Treap

START

随机平衡二叉查找树 Treap

 

[注:本文大部分资料选自郭家宝 《随机平衡二叉查找树 Treap的分析与应用》,结合老师的通俗讲解及本人某些吐槽写就……尴尬]

 

一、  二叉查找树

1.概述:二叉查找树(Binary Search Tree)是基于插入思想的一种在线的排序数据结构,简称BST。这种数据结构的基本思想是在二叉树的基础上,规定一定的顺序,使数据可以有序地存储。二叉查找树运用了像二分查找一样的查找方式,并且基于链式结构存储,从而实现了高效的查找效率和完美的插入时间。

2.定义

    二叉查找树是具有下列性质的二叉树:

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

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

   (3)它的左、右子树也分别为二叉查找树。

    以上性质被称作BST性质。

    如图就是一棵二叉查找树:

 

3.遍历

    根据BST性质,我们如果要从小到大输出所有结点的值,只需对二叉查找树进行中序遍历。

4.查找

    比如说我们要查找一个值x。现在从根结点开始查找,对每棵树/子树,都进行如下操作:

    (1)如果x小于根结点的值,则在左子树中继续递归查找x。

    (2)如果x等于根结点的值,则查找成功

    (3)如果x大于根结点的值,则在右子树中继续递归查找x

    (4)如果当前结点为空,则退出递归,查找失败

    期望时间复杂度:O(logN)

5.插入

    比如说插入一个值x。现在从根结点开始递归,进行如下操作:

    (1)如果x小于根结点的值,则在左子树中继续递归寻找位置

    (2)如果x等于根结点的值,有两种方法可以处理。第一种是规定插入左子树。第二种更方便,是给每个结点增加一个域,用于记录这个值的个数。插入则个数++。(后文使用的是这种方法)

    (3)如果x大于根结点的值,则在右子树中继续递归寻找位置

    (4)如果当前结点为空,则在此建立新的结点,进行插入。

    期望时间复杂度:O(logN)

6.删除

    方法一、懒惰删除lazy deletion

    直接将待删除结点的num--;

    这样编码简洁明了,但是会造成树中有许多空的结点,造成空间的浪费、还影响效率。在时空复杂度要求不高的时候,可以使用这种删除方法。

   方法二、

    对于结点的删除,需要分情况考虑:

    (1) 如果是叶节点,则直接删除

    (2) 如果是链节点(只有一个非空子树),则用它的子节点代替它的位置。

    (3) 如果有两棵非空子树,则为了维护BST性质,要用它的前驱或后继来代替它。所谓前驱,就是它左子树中的最大值。所谓后继,就是它右子树中的最小值。

 

    方法二期望时间复杂度:O(logN) 

7.二叉查找树的平衡性讨论

    对待随机的数据,二叉查找树可以达到很好的平衡效果。

    但是,对于有序的数据,二叉查找树的平衡性就会被打破了。如图所示,这棵树成了一条链,造成不管是插入、删除、查找,时间复杂度都是O(N)的,无疑会造成效率的低下。

    只有维护二叉树的平衡性,才能保证O(NlogN)的期望时间复杂度,达到高效。

    为了维护二叉查找树的平衡性,各种自动平衡二叉查找树应运而生,有AVL树、红黑树、Treap、SBT等等。下文要介绍的就是相对而言编程难度“易”的Treap。

 

二、  Treap(Tree+Heap)

1.定义

    从本质上说,Treap也是一棵二叉查找树,时刻满足BST性质。只不过,在这其中加入了随机、旋转等操作之后,使得树能够拥有可观的平衡性。

2.原理阐述

    Treap在BST的基础上,给每个结点增添了一个随机生成修正值。任意时刻,修正值都必须满足最小堆/最大堆性质。为了使树同时满足BST性质和最小堆性质,要对树进行旋转操作。正是旋转,促使了Treap的平衡。

3.  如何旋转?

    如下图所示,很显然,2和4的修正值不满足最小堆性质,因而要调整结构,进行旋转,使其满足最小堆性质

    [PS:右图在实际的Treap中是不成立的,仅作阐述旋转原理的例子]

 

    此处定义两种旋转方式:左旋右旋

   (左旋一个子树,会把它的根节点旋转到根的左子树位置,同时根节点的右子节点成为子树的根;右旋一个子树,会把它的根节点旋转到根的右子树位置,同时根节点的左子节点成为子树的根)

    左旋和右旋称呼并不重要,其实就是根据最小堆性质,在左子结点的修正值小于根结点的修正值时,要将左子结点旋到根的位置,对于右子结点也是同理。

    旋转后的树,一定满足最小堆性质吗?当然是的。不难解释,就留给读者自己思考。[滑稽]

    另外,在旋转时,还必须维护BST性质。如果不对子树的结构也作出调整,显然无法满足。怎么调整呢?

    根据原图的BST性质,上图中3的大小是介于2和4之间的。那么为了维护性质,也将它放在一个大小介于2和4之间的位置上,这个位置,即是旋转之后,4的左子结点。

    不妨多画几个图,就很好理解了:如果要把原根的左子节点变成新根,则要将(yg)左子节点的 右子树 变成 新的右子结点(即原根)的左子树;同理,如果要把原根的右子节点变成新根,则要将(yg)右子结点的 左子树 变成 新的左子结点(即原根)的右子树。

    听起来有点绕,但是不难想通。归纳了一句话:左的右等于右的左,右的左等于左的右。【滑稽】

    于是,上图就变成了下面这副模样:

 

    在代码实现的时候,无需分类特殊处理,化异为同即可。

    总而言之,旋转的意义在于:

    它可以使不满足堆序的节点通过调整位置,重新满足堆序,而不改变BST性质。

4.遍历和查找

    由于Treap满足BST性质,因而跟BST的操作相同。

5.插入

    与BST的操作类似,不难找到此节点应放的位置。

    如果树中有结点的值等于插入的值,那么直接num++。

    否则,需要新建一个结点,同时给它赋予一个修正值。此时,一旦这个修正值打破了最小堆性质,就要进行旋转操作。将这棵子树旋转上去后,又可能破坏了上面的最小堆性质,那就旋转到满足最小堆性质为止。

    下图是一个过程的模拟:

    插入4:

 

 

 

    左旋3:

 

 

 

 

    对以5为根的子树进行右旋:

 

 

    至此,树的修正值满足堆序,调整结束,插入完成。

6.删除

    与BST相同,首先,我们可以找到待删除的结点。

    然后,有三种办法可以进行删除:

    (1)懒惰删除

    (2)BST删除方法二

    (3)上面的两种方法,都没有利用到Treap特有的随机性,修正值。第三种方法在Treap中更为通用。需要分情     况考虑:

    情况一:如果待删除结点是叶子节点或链结点,就用它的子节点来代替它。

    情况二:通过旋转的方法,将待删除结点旋转到一个可以直接删除的位置,再用情况一的方法直接删除它。

    怎么旋转呢?

    为了满足最小堆性质,我们要把待删除结点修正值较小的子节点旋转到待删除结点的位置。旋转方法如上文所述。

    下面给出一个删除例子:需删除6。

    4的修正值较小,于是右旋结点6

 

 

   7的修正值较小,左旋结点6。

 

 

 

 

 

    此时,6可以被直接删除,用子节点5来代替它。

      

      

 

 

 

三、 Treap与其它平衡树对比

 

    这是论文中给出的对比表格,表示无语……Treap的编程难度不算易了吧……分分钟100+……可能因为我弱,100+都嫌多……【笑哭】

    不过说实话,Treap确实是个很强大的东西,相对于线段树来说,它的时间复杂度差不多,而且占的空间更小,开N就够了。而且,能够实现很多线段树不能实现的功能……

    本弱有N^2不能解决的问题,往往想要求助线段树。看来以后要换个宿主啦~

四、 Treap的其它功能

1.查找最值

    如果要查找最小值,则不停地访问Treap的左子树,更新最优值。直到遇到空节点为止。

    查找最大值也是同理。

2.查找前驱和后继

    注意,此处的前驱和后继不同于前文。假设我们要查找x的前驱和后继,此处的前驱指的是在Treap中小于等于x的最大值,后继指的是在Treap中大于等于x的最小值。

    解决这个问题,我们采用贪心逼近法。

   (1)查找前驱

    如果当前结点的值小于等于x,则更新最优值,并在右子树中继续查找。

    如果当前结点的值大于x,则在左子树中继续查找。

    如果当前结点为空,则查找结束。

  (2)查找后继

    如果当前结点的值大于等于x,则更新最优值,并在左子树中继续查找。

    如果当前结点的值小于x,则在右子树中继续查找。

    如果当前结点为空,则查找结束。 

 

    在信息学题目中,查找前驱、后继是经常要用到的。

3. Treap的排序问题

    Treap 定义中“左小于中小于右”,仅仅是逻辑上的定义。实际上我们可以以任何有序的规则排序。只需修改定义,操作即可。

4.Treap的类型

    Treap中并不仅限于存储int类型,可以比较大小的类型都可以存储于Treap中。

    一般情况下,我们认为元素之间比较大小的时间复杂度是O(1)。但是有些类型进行比较时会耗费大量时间,无法承受。例如字符串,作为检索字符串的容器,更推荐 Trie,而不是平衡树。平衡树仅适合做元素间相互比较时间很少的类型的有序存储容器。

   [关于Trie,本人还没有学过,留坑待补]

5.维护子树大小

    在Treap中,我们称以一个子树的所有节点的权值(即num)之和,为子树的大小。如果想要查找元素的排名、或者排名为k的元素,就很有必要维护子树的大小了。

    由于插入、删除都要进行旋转,因此要对子树的大小进行动态的维护。

    每进行一次旋转,都要重新计算子节点和根结点的子树大小。

    在插入时,新增结点的子树大小为1。递归返回时,要重新计算各节点的子树大小。

    在删除时,将待删除结点旋转到可直接删除的位置之后,将它的子树大小置为0,递归返回时重新计算各节点的子树大小。

7.查询排名第k的元素

    假设当前节点为P。我们对P进行分类讨论:

[P的左子树结点个数为P.leftsize(),右子树结点个数为P.rightsize(),P的权值num 为weight]

  (1)如果k<P.leftsize(),目标元素一定在P的左子树中,且它在P的左子树中的排名也为k。

  (2)如果k<= P.leftsize()+weight,则目标元素为P。

  (3)如果k> P.leftsize()+weight,则目标元素在P的右子树中,且目标元素在P的右子树中的排名为k- P.leftsize()-weight。

    以上步骤可以通过递归实现。不过,执行以上步骤有一个大前提——Treap的结点个数大于等于k。

    时间复杂度:O(logN)

7.查询元素排名

    它与查询排名为k的元素近似为互逆运算。

    我们规定,如果在 Treap 照片那个有多个重复的元素,则这个元素的排名为最小的排名。

    假设当前节点为P,cur表示目前搜寻到了cur个数,这cur个数都小于P。

(1)如果now< P.value,则在P的左子树中查找排名。

(2)如果now==P.value,则rank=P.leftsize()+1;

(3)如果now> P.value,则cur+=P.leftsize()+P.weight,在P的右子树中继续查找排名。

8.维护附加关键字。

    像线段树那样,在平衡树中也可以维护区间最值,总和等附加关键字,应用尤为广泛。

8.习题应用

SMOJ 2165  Treap模板题

SMOJ 2166  数列的和

SMOJ 2167  郁闷的出纳员

SMOJ 2212  郁闷的小J

9.小感

    Treap的代码实现中,最好要用到指针。作为一个不太会用指针的蒟蒻,写起代码来举步维艰……

    要去好好参透一下……

10.关于代码实现

   下面附SMOJ 2165的代码,是插入,查找排名k,以及删除操作的模板。

   代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cstdlib>
#include <ctime>
const int MAXN=1e6+5;
int n,x,ch;
struct Tnode
{
    Tnode *son[2];
    int tot,si;//tot为此点权值,si表示当前节点及其子树的权值和,即 当前子树大小。  
    int v,fix;//fix为修正值,v为此结点的具体值。 
    
    Tnode(int vv=0)//如果不传参数,默认为0;否则为传入值 
    {
        son[0] = son[1] = 0;
        v=vv;
    }
    void Update()//计算当前子树大小 
    {
        si=tot;
        if(son[0])    si+=son[0]->si;
        if(son[1])    si+=son[1]->si;
    }
}*r, Mem[MAXN], *cur;
Tnode *Newnode(int v = 0)
{
    (*cur)=Tnode(v);
    return cur ++;
}
void Rotate(Tnode *&x,int t)//传0,则代表要把son[0]即左子节点旋到当前节点上;传1则反之。 
{
    // 此处理解可带入0、1模拟一下
    Tnode *y= x -> son[t];
    x -> son[t]= y ->son[t^1];//右的左等于左的右,左的右等于右的左。 
    y -> son[t^1] = x;//新根的t^1子节点为原根 
    x -> Update();//重新计算子树大小。 
    y -> Update();
    x = y;
}
void Insert(Tnode *&r,int x)
{
    if(!r)//遇到空节点,x的归宿 
    {
        r = Newnode(x);//新建节点 
        r -> tot= r -> si = 1;//节点权值、子树大小初始化为1 
        r -> fix= rand();//赋予一个随机值作为修正值
        return;
    }
    if(x == r->v)//如果有值相同的结点,直接增加其权值、子树大小 
    {
        ( r -> tot)++;
        ( r -> si)++;
        return;
    }
    int t= (x > ( r -> v ) );//如果x大于当前节点的值,则t=1,x应插入右子树;否则 t=0,x应插入 左子树 
    Insert( r -> son[t] , x);//继续递归查找位置 插入 
    if( r -> son[t] -> fix < r -> fix)        Rotate(r,t);//如果修正值不满足堆序,则进行旋转。r为当前节点,t为用哪个子节点来代替根节点 
    r -> Update();//由于旋转之后,树的结构会发生变化,因此需重新计算子树的大小 
}
int Ask(Tnode *&r,int k)//询问排名为k的元素是什么 
{
    if( !r )    return 0;//访问到空节点,则询问失败 
//    if( r -> si < k)    return 0;//如果子树大小小于k,则无法查询,必须要返回
    int lefts = ( r -> son[0] ) ? ( r -> son[0] -> si ): 0;//一定要先判断是否存在左子节点,不然,访问不存在的空间,会RE
    if( k <= lefts)    return Ask( r -> son[0], k );//如果k在右子树中,递归查找 
    if( k <= lefts + r -> tot)    return r -> v;//如果k在[lefts+1,lefts+r->tot]范围内,则答案一定为 r->v
    return Ask( r -> son[1], k - lefts - r -> tot);//否则,k在右子树中,且它在右子树中的排名为k - lefts - r -> tot
}
//此处为懒惰删除 
/*void Delete(Tnode *&r,int x)
{
    if(r->v==x)
    {
        r->tot--;
        r->si--;
        return;
    }
    int t= x > r->v;
    Delete(r->son[t],x);
    r -> Update();
}*/
void Delete(Tnode *&r, int x)//删除 
{
    if (x == r -> v) //如果找到待删除的值 
    {
        if ( r -> son[0] && r -> son[1])//如果有左右子节点,则要旋转后删除 
        {
            int t = ( r -> son[1] -> fix > r -> son[0] -> fix);//选取修正值较小的作为新根 
            Rotate(r, t^1);//将新根旋上来 
            Delete(r -> son[t], x);//继续递归,找底层位置 
            r -> Update();
        }
        else 
            if (! r -> son[0] && ! r -> son[1])//如果左右子节点都为空,即叶子节点,则直接删除它
            {
                if(r -> tot == 1)    r = 0;
                else    
                {
                    r -> tot--;
                    r -> si--;
                }
            } 
        else //如果为链节点,则要么tot-1,要么用它的子节点代替它。
        {
            if( r -> tot > 1)    
            {
                r -> tot--;
                r -> si--;
            }    
            else    if ( r -> son[0]) r = r -> son[0]; 
            else    r = r -> son[1];
        }
            
        return ;
    }
    Delete(r -> son[x > r->v], x);//继续查找待删除节点 
    r -> Update();
}

int main()
{
    freopen("2165.in","r",stdin);
    freopen("2165.out","w",stdout);
    
    srand((int)time(0));
    cur = Mem;
    scanf("%d",&n);
    r=0;
    
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d",&ch,&x);
        if(ch==1)
            Insert(r,x);
        else    
            if(ch==2)
                printf("%d\n",Ask(r,x));
        else    
            Delete(r,x);
    }
    return 0;
}

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值