平衡树Treap

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

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

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

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

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

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

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

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

平衡树Treap_第1张图片

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

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

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

平衡树Treap_第2张图片

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

平衡树Treap_第3张图片

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

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

平衡树Treap_第4张图片
左边BST可通过左旋以4为根的子树得到右边的BST,反之亦然。

Zig(右旋)/  Zag(左旋)

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

平衡树Treap_第5张图片
Zig

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

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

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

(3)调整根v为c。

平衡树Treap_第6张图片
Zig参考代码
平衡树Treap_第7张图片
Zag

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

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

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

(3)调整根v为c。

平衡树Treap_第8张图片
Zag参考代码

平衡树Treap

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

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

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

平衡树Treap_第9张图片
树上节点信息

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

平衡树Treap_第10张图片
节点中数字代表权值w,节点旁数字代表pri

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

Treap插入操作

1.插入节点算法步骤

Step1、u从根节点r开始,如果x

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

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

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

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

2.插入节点示例

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

平衡树Treap_第11张图片
插入新节点(w=4,pri=15)

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

平衡树Treap_第12张图片

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

平衡树Treap_第13张图片

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

平衡树Treap_第14张图片
插入节点操作参考代码

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

Treap删除操作

1.删除节点算法步骤

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

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

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

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

2.删除节点示例

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

平衡树Treap_第15张图片
左旋8

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

平衡树Treap_第16张图片
删除8
平衡树Treap_第17张图片
删除节点参考代码

Treap的用途

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

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

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

平衡树Treap_第18张图片
查询x的排名参考代码

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

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

算法如下:

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

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

平衡树Treap_第19张图片
查询非严格前驱(后继)参考代码
平衡树Treap_第20张图片
查询非严格前驱(后继)参考 代码

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

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

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

(2)否则如果t[t[num].l].size

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

平衡树Treap_第21张图片
查询第k小参考代码

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

平衡树Treap_第22张图片

习题

ybt1565-1568

你可能感兴趣的:(平衡树Treap)