Treap = 二叉搜索树 + 堆
一、二叉搜索树
简称BST,每个节点最多有两个子节点,左子比当前节点小,右子比当前节点大。
因此对于插入和查找第k小的值,都可以从根递归着进行下去,在到达递归终点之前,不是选择这个节点左儿子就是右儿子,因此,操作的复杂度 = 树的深度。
然而,树会根据插入的数的不同,产生不同的形状,并不是我们认为的logN,最好情况下才是O(logN),要达到O(logN)需要你尽量把数插到还没完全插满的层,你给出的数得多好才能凑成这样。。。
因此,正常的插入很容易让树的深度接近O(N),即每次操作最坏情况都是O(N),复杂度就跟暴力一样了。
二、平衡
所以我们需要一种方法,能够在保证符合BST性质(左儿子<当前<右儿子)的情况下,对原树的节点进行一些搬动,使得层数尽量边少,即"平衡"。
我们通过一种叫 旋转 的方法来平衡。
何为旋转,请看图(原图来自《算法竞赛入门经典-训练指南》P228)
可以看出,通过旋转,原本BST的性质不会改变,并且使得当前根节点发生了变化。
这个操作抽象的说吧,就是一个天平,原本严重失衡,现在从重的那边取走点东西,放到轻的那边,那么天平就平衡了一些,距离地面的最近高度也变高了(即成功让深度变小)
然而我们并不晓得什么时候要旋转,乱旋转你可能会让这个“天平”失衡的更厉害,因此引入另一个玩意的知识,堆。
三、堆
简称heap,虽说叫堆,听起来跟二叉树半毛钱关系没有,但这玩意就是一种完全二叉树。
完全大概指的就是当前一层没有插满不会去下一层,有没有发现,这好像正是我们之前所需要的?!
我们还是先单独说一下堆的基本功能。
O(logN)插入与O(1)获取最大/最小值,但无法获取第k小的值(废话,否则前面都白说了直接用堆就好了)
我们以获取最大值为例。
我们具体操作是维护根节点为最大值,每次插入,我们根据插入的是第几个数,决定他的位置
啥意思?
比方说,对于7号节点,我们知道他的两个子节点下标分别是14,15(2*i和2*i+1)
比如插入第15个数,那么就比较这个数和他父节点7,如果比他父节点大,就交换两个数,然后递归着上去,因此我们就做到了:每个节点都比两个儿子大。
最终我们就能O(logN)维护最大值了
代码啥的没有,我就略微看了一下堆的讲解,吸收了一下皮毛知识。
反正我们知识用这样一个思想。
四、玄学降复杂度
说是玄学,因为我没理解。
有了前面的铺垫,接下来讲真正的treap。
我们给每个要插入的数另外配备一个随机数,每个要插入的数配备的随机数不同,这个通过rand()函数实现,这个随机数我们称为优先级。
我们按照普通的BST插入到位置,然后对比插入的数和他的父节点的优先级,如果当前节点优先级比父亲大,那么就要旋转,当前是左儿子就右旋,右儿子就左旋。
那么为什么这样就能复杂度呢:蓝书上的意思大概是,由于每个节点优先级实现给定且互不相等,整棵树形态也唯一确定,辣么时间复杂度也是随机的,然后就说有方法可以证明这个时间复杂度平均情况下为O(logN)级别
这个证明方法嘛。。。鬼晓得。
五、代码
#include<bits/stdc++.h>
using namespace std;
struct Treap
{
Treap* ch[2];
int val;
int pri;
int cnt;///子树节点个数+自己,用于找第k小使用
Treap(int x){ch[0] = ch[1] = NULL; pri = rand(); val = x;cnt = 1;}
void maintain(){
cnt = 1;
if (ch[0]!=NULL) cnt += ch[0]->cnt;
if (ch[1]!=NULL) cnt += ch[1]->cnt;
}
};
///旋转
///具体做了什么:把所有的信息都给了k,然后k的所有信息给了O
///链表懂这个应该也懂
void rotate(Treap* &rt,int d)///这里用个位运算相当于 1-d,可以加快速度,减少码量
{
Treap* k = rt->ch[d^1];
rt->ch[d^1] = k->ch[d];
k->ch[d] = rt;
rt->maintain();///两个 maintain顺序不能变,先搞儿子父亲才能对
k->maintain();
rt = k;
}
///插入
void insert(Treap* &rt,int x)
{
if (rt==NULL){rt = new Treap(x);}
else {
int d = (x < rt->val)? 0:1 ; ///和当前节点相同的数也会被插入右子树
insert(rt->ch[d],x);
if (rt->ch[d]->pri > rt->pri) rotate(rt,d^1);///子节点优先级比当前节点大就旋转
}
rt->maintain();
}
///查找第k小
///采用的是逐渐逼近的办法
///find函数的意义是找当前节点所在的树中第k小
///由BST定义可知:左子树记录的是比当前小的数的个数,右子树是比当前数大的数的个数
int ccnt;///记录小于等于当前节点这个数数量
int find(Treap* rt,int k)
{
if (rt->ch[0]==NULL) ccnt = 1;
else ccnt = 1 + rt->ch[0]->cnt;
if (k==ccnt) return rt->val;
else if (k<ccnt) return find(rt->ch[0],k);
else return find(rt->ch[1],k-ccnt);
}
int main()
{
Treap* root = NULL;///初始化
insert(root,1);
insert(root,3);
insert(root,5);
printf("%d\n",find(root,2));
}
六、写在最后
还有删除操作,以及很多很多玩意我还没有了解,所以只能写这些了,毕竟学这个是为了去搞后缀平衡树。之后准备深入了解平衡树的时候,懂得更多了再来改改此文。