在学习平衡树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
网友评论