概念
Treap一词是Tree和Heap的合成词,也就是这是一颗带有堆性质的树,即树堆,Treap树是一种排序二叉树(二叉搜索树、二分检索树 Binary Serach Tree),简称BST,也就是满足value值大小关系是左孩子<根<右孩子,这样就满足了排序二叉树,刚才讲过,Treap树还满足堆的性质,那么它的哪个值满足堆的性质呢,并不是value,而是一个优先级rank值,这个rank值是人为添加的,一般使用一个随机数,这个rank是为了维持二叉树的平衡而设定的,总的说来,对于键值来说,Treap是一棵排序二叉树,对于优先级来说,Treap是一个堆,当然根节点的优先级是最高的
实现
结构
首先考虑树的组成,需要键值value,优先级Rank,为了方便统计排名,需要一个sz记录当前树的节点数,需要一个cnt记录当前数字出现了多少次,需要两个指针分别指向左孩子和右孩子,每次操作之后都需要更新当前树上节点数,所以写一个Update函数,为了方便判断当前元素和根节点value的大小关系,需要写一个cmp函数,如果小于则在左子树,大于在右子树,相等返回 − 1 -1 − 1
struct TreapNode{
int value;
int Rank;
int cnt;
int sz;
TreapNode* son[ 2 ] ;
int cmp ( int x) const {
if ( x == value) return - 1 ;
return x < value ? 0 : 1 ;
}
void Update ( ) {
sz = 1 ;
if ( son[ 0 ] != NULL ) sz + = son[ 0 ] - > sz;
if ( son[ 1 ] != NULL ) sz + = son[ 1 ] - > sz;
}
} ;
旋转操作
为什么要旋转?考虑普通的排序二叉树,假设插入数字是有序的,那么二叉树就会退化为一条链,这样查询就和数组没有什么区别了,所以Treap树就制定了一个随机给的rank值,按照它的大小关系进行相应的旋转,使得二叉树保持平衡不退化为链
左旋和右旋
上图针对于9进行了旋转,从左到右是右旋,从右到左是左旋,这里可以理解为右旋为顺时针,左旋为逆时针
右旋
这个图,通俗一点讲,右旋要断三根线:待旋节点和它的父亲节点、待旋节点和左孩子、左孩子和它的右孩子,然后把左孩子的右孩子作为待旋节点的左孩子,待旋节点作为它左孩子的右孩子,再把待旋节点左孩子放在待旋节点的位置上,这里注意一点,我们要传的是待旋节点的引用,引用不改变地址,只改变值 ,这样我们才能让6到9的位置上而依然保持和9父亲之间的关系
左旋
相对的,左旋是先向右,再向左,也就是找右孩子,再找右孩子的左孩子,让右孩子的左孩子作为待旋节点的右孩子,待旋节点作为它右孩子的左孩子,完成左旋操作
旋转总结
可以发现,左旋和右旋是一种互逆的关系,右旋找左孩子,左旋找右孩子,这样可以通过 1 − x 1 - x 1 − x 的操作来实现,更快的方法是取异或,程序如下
void Rotate ( TreapNode* & o, int d) {
TreapNode* p = o- > son[ d ^ 1 ] ;
o- > son[ d ^ 1 ] = p- > son[ d] ;
p- > son[ d] = o;
o- > Update ( ) ;
p- > Update ( ) ;
o = p;
}
插入操作
解决了旋转,插入就简单的多了,每次插入一个数,首先根据value确定位置(排序二叉树),确定位置以后,随机给定Rank值,如果根节点Rank不是最大,则需要旋转
void Insert ( TreapNode* & o, int k) {
if ( o == NULL ) {
o = new TreapNode ( ) ;
o- > value = k;
o- > Rank = rand ( ) ;
o- > son[ 0 ] = o- > son[ 1 ] = NULL ;
o- > sz = 1 ;
o- > cnt = 1 ;
} else {
int d = o- > cmp ( k) ;
if ( d != - 1 ) {
Insert ( o- > son[ d] , k) ;
if ( o- > Rank < o- > son[ d] - > Rank) Rotate ( o, d ^ 1 ) ;
} else o- > cnt++ ;
}
o- > Update ( ) ;
}
删除操作
先看这个值在哪棵子树,然后要看这个值是否有多个,如果有多个, c n t − − cnt-- c n t − − 即可,注意update;如果只有一个,那么需要删除该节点,但是不能直接删除,需要判断左右孩子的优先级保证树的平衡,如果左孩子优先级高,那么节点右旋,此时左孩子转到根,原来的根转到右子树,所以需要递归在右子树中删除根节点
如果该节点不是左孩子右孩子都存在,那么直接把存在的那个孩子移动到根,然后直接删除原来的根节点即可,最后如果根节点不是空指针,那么需要update
void Remove ( TreapNode* & o, int k) {
int d = o- > cmp ( k) ;
if ( d == - 1 ) {
if ( o- > cnt > 1 ) {
o- > cnt-- ;
o- > Update ( ) ;
return ;
}
TreapNode* u = o;
if ( o- > son[ 0 ] != NULL && o- > son[ 1 ] != NULL ) {
int d2 = o- > son[ 0 ] - > Rank > o- > son[ 1 ] - > Rank ? 1 : 0 ;
Rotate ( o, d2) ;
Remove ( o- > son[ d2] , k) ;
} else {
if ( o- > son[ 0 ] == NULL ) o = o- > son[ 1 ] ;
else o = o- > son[ 0 ] ;
delete u;
}
} else {
Remove ( o- > son[ d] , k) ;
}
if ( o != NULL ) o- > Update ( ) ;
}
第K大
在这里,我们设定的sz派上了场,从根节点出发,如果K小于右子树的节点数,那么就在右子树里面找第K大(排序二叉树);如果K在右子树节点值的个数范围内,那么就是根节点对应的value;否则就在左子树里面找第(K − - − 右子树的sz − - − 出现次数)大,当然需要再处理一些特殊的情况比如越界
int Get_Kth ( TreapNode* o, int k) {
if ( o == NULL || k <= 0 || k > o- > sz) return 0 ;
int s = ( o- > son[ 1 ] == NULL ? 0 : o- > son[ 1 ] - > sz) ;
if ( k >= s + 1 && k <= s + o- > cnt) return o- > value;
else if ( k <= s) return Get_Kth ( o- > son[ 1 ] , k) ;
else return Get_Kth ( o- > son[ 0 ] , k - s - o- > cnt) ;
}
K是第几大
如果K不在树上,那么返回 − 1 -1 − 1 (有些题目有特殊要求),否则递归查找即可
int Get_Xrank ( TreapNode* o, int k) {
if ( o == NULL ) return - 1 ;
int d = o- > cmp ( k) ;
if ( d == - 1 ) return o- > son[ 1 ] == NULL ? 1 : o- > son[ 1 ] - > sz + 1 ;
else if ( d == 1 ) return Get_Xrank ( o- > son[