Treap

Treap(平衡树)

注:本文有部分内容参照了清华大学计算机科学与技术系 02 班郭家宝 2010011267 论文,如与其他同学的博客有所雷同,纯属巧合。

首先引入几个基础概念

BST性质:

   一棵树中,左子树的值比当前的根小,右子树的值比当期的根大。左子树<根<右子树。

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

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

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

   上述性质被称为 BST 性质。可以看出,二叉查找树是递归定义的

如图, 是一个二叉查找树。

最小堆性质:

    最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。

    通俗点讲:根节点的值比左右子树的值都要小。



       如图,为最小堆。

PS:

    之所以要讲BST性质,以及最小堆性质,因为Treap平衡树是他们结合起来的进化版。接下来就要进入正题了。

 

Treap的基本性质:Treap= Tree + Heap:

    Treap 是一种平衡树。它满足BST性质,即左孩子的值,小于根节点的值,右孩子的值大于根节点的值。

    Treap 在 BST 的基础上,添加了一个随机修正值。在满足 BST 性质的基础上,Treap 节点的修正值还满足最小堆性质

    最小堆性质可以被描述为每个子树根节点都小于等于其子节点。于是,Treap 可以定义为有以下性质的二叉树:

    1.  若它的左子树不空, 则左子树上所有结点的值均小于它的根结点的值, 而且它的根节点的修正值小于等于左子树根节点的修正值;

    2.  若它的右子树不空, 则右子树上所有结点的值均大它的根结点的值, 而且它的根节点的修正值小于等于右子树根节点的修正值;

    3.  它的左、右子树也分别为 Treap。

旋转操作:

    为了使Treap 中的节点同时满足BST 性质和最小堆性质,不可避免地要对其结构进行调整,当树的随机修正值不满足最小堆性质的时候,就要对树进行调整。

    这种调整方式被称为旋转。在维护Treap 的过程中,只有两种旋转,分别是左旋转(简称左旋)和右旋转(简称右旋)。

左旋转:把这个节点一下的树整体向左旋转,也就是把右节点旋到根的位置,而根的位置旋转到左结点的位置,如果右节点本身也有一个右节点,那么把它的右节点旋到根的右节点位置。

至于这么放的原理,如果在大小关系上一时不能理解,那么可以认为,因为根的左结点已经有数了,所以只能把右节点的左结点放到根的右节点位置上。

如果从BST性质的方向来看,这样子旋转是有利于维护BST性质的,因为根的右节点中的值都是比根节点的值要大的,所以旋转过来,即使是右节点的左结点,也会比根大,所以要放在根的右节点部位。

可能越说越晕了,看图:



    如上图所示的左边的一个 Treap,它仍然满足BST 性质。但是由于某些原因,节点 4 和节点2 之间不满足最小堆序,4 作为2 的父节点,它的修正值大于左子节点的修正值。

    我们只有将2 变成4 的父节点,才能维护堆序。根据旋转的性质我们可以知道,由于2 是4 的左子节点,为了使 2 成为4 的父节点,我们需要把以 4 为根的子树右旋。右旋后,2 成为了4的父节点,满足堆序。


插入操作:

    在Treap 中插入元素,与在 BST 中插入方法相似。首先找到合适的插入位置,然后建立新的节点,存储元素。但是要注意建立新的节点的过程中,会随机地生成一个修正值,这个值可

能会破坏堆序,因此我们要根据需要进行恰当的旋转。具体方法如下:

    1. 从根节点开始插入;

    2. 如果要插入的值小于等于当前节点的值:

    在当前节点的左子树中插入,插入后如果左子节点的修正值小于当前节点的修正值,对当前节点进行右旋;

    3. 如果要插入的值大于当前节点的值:

    在当前节点的右子树中插入,插入后如果右子节点的修正值小于当前节点的修正值,对当前节点进行左旋;

    4.如果当前节点为空节点,在此建立新的节点,该节点的值为要插入的值,左右子树为空,插入成功。


删除操作:

    关于删除操作,我决定先从懒惰删除开始说起:

    懒惰删除就是在删除时,仅仅将元素找到后给元素打上“已被删除”的标记,而实际上不把

它从平衡树中删除。

    这种做法的优点是节约代码量,减少编程时间,对于初学者来说,是理解以及实现删除功能的好帮手。

但它的缺点也是很严重的:如果插入量和删除量都很大,这种删除方式会在平衡树中留下大量的“废节点”,浪费空间,还影响效率。


专业删除操作:

    情况一:该节点为叶节点或链节点,则该节点是可以直接删除的节点。若该节点有非空子节点,用非空子节点代替该节点的,否则用空节点代替该节点,然后删除该节点。

 

    情况二:该节点有两个非空子节点。我们的策略是通过旋转,使该节点变为可以直接删除的节点。如果该节点的左子节点的修正值小于右子节点的修正值,右旋该节点,使该节点降为右子树的根节点,然后访问右子树的根节点,继续讨论;反之,左旋该节点,使该节点降为左子树的根节点,然后访问左子树的根节点,继续讨论,直到变成可以直接删除的节点


代码实现如下(正确删除方式):

#include 
     
     
      
      
#include 
      
      
       
       
#include 
       
       
        
        
#include 
        
        
         
         
#include 
         
         
           struct Tnode { Tnode *son[2]; int fix, v; int siz; Tnode(int _v = 0) { son[0] = son[1] = 0; v = _v; } void Update() { siz = 1; if (son[0]) siz += son[0] -> siz; if (son[1]) siz += son[1] -> siz; } } *r, Mem[1000050], *cur; Tnode *NewNode(int v = 0) { (*cur) = Tnode(v); return cur ++; } void Rotate(Tnode *&x, int t) { Tnode *y = x -> son[t^1]; x -> son[t^1] = y -> son[t]; y -> son[t] = x; x -> Update(); y -> Update(); x = y; } void Insert(Tnode *&r, int x) { if (!r) { r = NewNode(x); r -> siz = 1; r -> fix = rand(); return ; } int t = (x > (r->v)); Insert(r -> son[t], x); if (r -> son[t] -> fix < r -> fix) Rotate(r, (t^1)); r -> Update(); } int Ask(Tnode* r, int k) { if (!r) return 0; int leftsiz = (r->son[0]) ? (r -> son[0]->siz) : 0; if (k <= leftsiz) return Ask(r->son[0], k); if (k <= leftsiz + 1) return r -> v; return Ask(r -> son[1], k - leftsiz - 1); } 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); Delete(r -> son[t], x); r -> Update(); } else if (! r -> son[0] && ! r -> son[1]) { r = 0; } 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; int N; scanf("%d", &N); r = 0; while (N--) { int t, x; scanf("%d%d", &t, &x); if (t == 1) { Insert(r, x); } else if (t == 2) { printf("%d\n", Ask(r, x)); } else { Delete(r, x); } } return 0; } 
         
        
        
       
       
      
      
     
     

附录:

    关于Treap平衡树与其他平衡树的功能及速度比较。


关于treap的其他功能:

查找排名第k排名的元素:

    1.定义P 为当前访问的节点,从根节点开始访问,查找排名第 k 的元素;

    2.若满足P.left.size + 1<=k <= P.left.size + P.weight,则当前节点包含的元素就是排名第 k 的元素;

    3.若满足k <P.left.size + 1 ,则在左子树中查找排名第 k 的元素;

    4.若满足k >P.left.size + P.weight,则在右子树中查找排名第 k-(P.left.size +P.weight)的元素。

求元素的排名:

     在 Treap 中求元素的排名的方法与查找第 k 小的数是很相似的,可以近似认为是互为逆运算。

     1.定义P 为当前访问的节点,cur 为当前已知的比要求的元素小的元素个数。从根节点开始查找要求的元素,初始化 cur为0。

     2.若要求的元素等于当前节点元素,要求的元素的排名为区间[ P.left.size + cur + 1, P.left.size + cur + weight ]内任意整数;

     3.若要求的元素小于当前节点元素,在左子树中查找要求的元素的排名;

     4.若要求的元素大于当前节点元素,更新cur 为cur + P.left.size+weight ,在右子树中查找要求的元素的排名。

前驱与后继 :

    求一个元素在平衡树(或子树)中的前驱,定义为查找该元素在平衡树中不大于该元素的最大元素。相似的,求一个元素在平衡树(或子树)中的后继,定义为 查找该元素在平衡树中不小于该元素的最小元素。

求前驱:

    1. 从根节点开始访问,初始化最优节点为空节点;

    2. 如果当前节点的值不大于要求前驱的元素的值,更新最有节点为当前节点,访问当前节点的右子节点;

    3. 如果当前节点的值大于要求前驱的元素的值,访问当前节点的左子节点;

    4. 如果当前节点是空节点,查找结束,最优节点就是要求的前驱。

求后继:

    1. 从根节点开始访问,初始化最优节点为空节点;

    2. 如果当前节点的值不小于要求前驱的元素的值,更新最有节点为当前节点,访问当前节点的左子节点;

    3. 如果当前节点的值小于要求前驱的元素的值,访问当前节点的右子节点;

    4. 如果当前节点是空节点,查找结束,最优节点就是要求的后继。





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值