Treap模板

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));
}

 


六、写在最后

还有删除操作,以及很多很多玩意我还没有了解,所以只能写这些了,毕竟学这个是为了去搞后缀平衡树。之后准备深入了解平衡树的时候,懂得更多了再来改改此文。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值