平衡树Treap

作者: Catzyzy | 来源:发表于2020-04-19 10:32 被阅读0次

    在学习平衡树Treap之前,我们先来了解什么是二叉查找树。

    二叉查找树(BST:Binary Search Tree)

    一棵二叉查找树定义如下:

    1、树中每个节点都有一个权值。(现假设权值均不相同)

    2、若左子树非空,则左子树上所有节点的权值均小于根节点的权值。

    3、若右子树非空,则右子树上所有节点的权值均大于根节点的权值。

    4、左右子树也分别为二叉查找树。

    如下图所示,树中每个节点中的数字代表的节点权值,显然,下图是一棵二叉查找树。

    可以观察到,一棵BST的中序遍历是一个升序序列。(如果不存在重复值的话)

    给定一个序列A,我们可以建立一棵BST方便地实现查询、插入、删除操作。如果序列A有n个元素,且对应的BST的高度为logn,那么每次查询、插入、删除操作复杂度自然是logn的。(BST的插入、删除、查询算法这里省略,较简单,请自行补充学习)。

    但是我们也会发现,有时候序列A的数据会导致所建立的BST退化为链,或高度远大于logn。这时的查询等操作复杂度就变得很高。例如序列A=[1,2,3,4,5,6].我们可以建立一棵对应的BST如下图所示:

    但我们发现,序列A不是只有一棵BST,下图的3种也可以称为序列A对应的BST。并且下图每一个BST的中序遍历和图2都是一致的。

    我们称中序遍历一样的两棵BST是等价BST。

    并且我们发现下面两棵等价BST可以通过一次旋转得到。例如下图:

    左边BST可通过左旋以4为根的子树得到右边的BST,反之亦然。

    Zig(右旋)/  Zag(左旋)

    我们定义两种操作Zig(右旋)和  Zag(左旋)如下:

    Zig

    对于以v为根的子树进行Zig(右旋)操作,步骤如下:

    (1)假设v的左二子为c,调整v的左二子为c的右儿子。

    (2)调整c的右儿子为v。

    (3)调整根v为c。

    Zig参考代码 Zag

    对于以v为根的子树进行Zag(左旋)操作,步骤如下:

    (1)假设v的右二子为c,调整v的右二子为c的左儿子。

    (2)调整c的左儿子为v。

    (3)调整根v为c。

    Zag参考代码

    平衡树Treap

    根据上面的分析,当序列的数字有序时,序列极有可能退化为链,树上操作复杂度会变得很高,于是我们需要设法调整BST的高度。根据不同的调整策略,对应了不同的平衡树类型,例如Treap,AVL,Splay,红黑树等等。

    Treap的基本思想是,对于随机数据,序列所构造的BST的高度期望值 为logn。

    在实际操作时,Treap对BST上的每个节点除了维护基本信息(左右儿子编号l,r,权值w,子树数字个数size,重复元素计数cnt等信息以外),额外维护一个随机优先级pri。如下:

    树上节点信息

    并且我们规定,树上所有节点的Pri值必须满足小根堆性质。即,每个节点的pri值小于左右儿子的pri值。如下图:

    节点中数字代表权值w,节点旁数字代表pri

    那么,在Treap中插入或删除一个节点时,会导致Treap的Pri值不满足小根堆性质,根据上面提及的等价BST概念,我们可以通过Zig或Zag操作来进行调整。具体方式如下:

    Treap插入操作

    1.插入节点算法步骤

    Step1、u从根节点r开始,如果x<r.w,递归插入左子树;否则递归插入右子树。

    Step2、如果当前节点为空,在此节点建立新的节点即可。

    Step3、为新建立的节点随机分配优先级,回溯时检查优先级是否满足堆序。

           Step 3.1 若左子节点优先级小于当前节点,右旋。

            Step 3.2 若右子节点优先级小于当前节点,左旋。

    2.插入节点示例

    现在,如果要在Treap中插入一个新的节点,例如(w=4,pri=15)。

    插入新节点(w=4,pri=15)

    发现现在的Treap不满足pri值的小根堆性质了,并且对于节点3这棵子树来讲,其右儿子节点4的pri值小于3的pri值,此时,我们可以将3这棵子树左旋,如下图:

    现在的Treap仍然不满足pri值的小根堆性质,并且对于节点5这棵子树来讲,其左二子节点4的pri值小于节点5的pri值,此时,我们可以将5这棵子树右旋,如下图:

    至此,经过旋转操作的Treap已经满足要求。下图为参考代码:

    插入节点操作参考代码

    说明:这个参考代码里面,对于重复元素的处理是维护的节点的cnt信息。

    Treap删除操作

    1.删除节点算法步骤

    Step1、如果当前节点是叶子节点,或只有一个子节点的非叶子节点。直接删除即可,用它的孩子节点替换它。

    Step2、如果当前节点是有2个子节点的非叶子节点。通过旋转操作将其变为可以直接删除的节点。

            Step2.1、如果该节点的左孩子优先级<右孩子优先级,右旋该节点;

            Step2.2、否则左旋该节点。转Step2.

    2.删除节点示例

    例如,现在要删除节点8,对于节点8而言,其右儿子pri值小于左二子pri值,那么左旋8为根的子树。

    左旋8

    此时节点8已经是一个可以直接删除的节点,用8的左二子替换它即可。

    删除8 删除节点参考代码

    Treap的用途

    1、查询数x的排名。算法如下:

    (1)设结果为ans,初始ans=0.

    (2)从根节点num开始,如果x==t[num].w,答案即为t[t[num].l].size+ans+1;如果x<t[num].w,递归查询左子树;如果x>r.w,更新ans+=t[t[num].l].size+t[num].cnt,递归查询右子树。

    查询x的排名参考代码

    2、查询数x的前驱或后继。注意区分是严格前驱(后继)还是非严格前驱(后继)。

    非严格前驱(后继)定义:x的前驱定义为Treap中不大于x的最大值。后继定义为不小于x的最小值。

    算法如下:

    (1)设结果为ans,初始ans=0.

    (2)从根节点num开始,如果x>=t[num].w,更新ans为当前节点,递归查询右子树;如果x<t[num].w,递归查询左子树。如果当前节点为空,ans即为答案。

    查询非严格前驱(后继)参考代码 查询非严格前驱(后继)参考 代码

    3、查询序列中的第K大或第K小。算法如下:

    由于Treap每个节点维护了size信息,从根节点num开始:

    (1)如果k<=t[t[num].l].size,那么递归查询左子树第k大。

    (2)否则如果t[t[num].l].size<k<=t[t[num].l].size+t[num].cnt,根节点值即为所求.

    (3)否则递归查询右子树第k-t[t[num].l].size-t[num].cnt大。

    查询第k小参考代码

    二分查找、平衡树、权值线段树的对比

    习题

    ybt1565-1568

    相关文章

      网友评论

        本文标题:平衡树Treap

        本文链接:https://www.haomeiwen.com/subject/imlyvhtx.html