普通平衡树Treap

提醒

可以先学一下二叉搜索树,学完了平衡树差不多也会了~
前置知识:二叉树的定义

Part 1 介绍

平衡树的性质:

与搜索树类似,任意一个节点的真实值小于右儿子的真实值大于左儿子的真实值;维护一个小根堆;每个节点除了有一个真实值之外,还有一个随机值。

平衡树相对于搜索树的优点:

平衡树就是为了解决链而生的,当树中出现链时,再使用搜索树进行各种操作消耗时间很大,因为树上问题的复杂度是与节点深度成正相关的,比如说下图:

这是一棵树,也是一条链,在这上边跑搜索树,假设节点数是n,那么最坏的复杂度是O(n)。
如果使用平衡树,就可能会把它变成这样的形状:

因为平衡树维护的是一个小根堆,是根据每个点的随机值进行维护的,因为随机值大概率不会单调递增或递减,所以就可以把一条链变成一颗比较正常的树。
那复杂度就大大减小了,所以这就是平衡树的优点呐~

Part 2 详解

首先要知道要实现的东西是啥:

①插入元素;
②删除元素;
③查询元素排名;
④查询在某个排名上的元素;
⑤找前驱;
⑥找后继;

然后捏,讲准备工作。

  1. 先知道几个变量的意义:
    t[now].siz(节点now的子树大小)
    t[now].cnt(节点now的大小)
    t[now].ran(节点now上的随机值)
    t[now].ls(节点now的左儿子)
    t[now].rs(节点now 的右儿子)
  2. 左右旋
//左旋
int lx(int &now) {
    int y = t[now].r;
    t[now].r = t[y].l;
    t[y].l = now;
    pushup(now), pushup(y);
    now = y;
}


解释一下,如图一,假设3号点的ran小于1号点的ran,为了维护小根堆的性质,要进行左旋,将三号点旋转为一号点的父亲,同时为了维护二叉树的性质,使4号点成为1号点的右儿子,这样既不会影响平衡树的性质,也维护了二叉树和小根堆的性质。

//右旋
int rx(int &now) {
    int y = t[now].l;
    t[now].l = t[y].r;
    t[y].r = now;
    pushup(now), pushup(y);
    now = y;
}

与左旋同理~

//合并信息
void pushup(int &now) {
    t[now].siz = t[t[now].l].siz + t[t[now].r].siz + t[now].cnt;
}

Part 3 解决问题

  1. 添加元素。
    就是分四种情况,没有儿子了、当前点储存的值等于要添加的值、当前值大于要添加的、当前值小于要添加的。
    代码如下:
void add(int &now, int &c) {
    if(!now) {
        now = ++cnt;
        t[now].cnt = t[now].siz = 1; 
        t[now].val = c;
        t[now].ran = rand();
        return;
    }
    t[now].siz ++;
    if(t[now].val == c) t[now].cnt ++;
    if(c > t[now].val) {
        add(t[now].r, c);
        if(t[t[now].r].ran < t[now].ran) lx(now);
    } else if(c < t[now].val) {
        add(t[now].l, c);
        if(t[t[now].l].ran > t[now].ran) rx(now);
    }
    pushup(now);
}
  1. 删除元素
    与添加类似,少了没有点这一情况,因为没有点就不用删了,所以不用管。
void del(int &now, int &c) {
    if(t[now].val == c) {
        if(t[now].cnt > 1) t[now].cnt--, t[now].siz--;
        else if(!t[now].l || !t[now].r) now = t[now].l + t[now].r;
        else if(t[t[now].l].ran < t[t[now].r].ran) rx(now), del(now, c);
        else lx(now), del(now, c);
        return;
    }
    t[now].siz--;
    if(t[now].val < c) del(t[now].r, c);
    else del(t[now].l, c);
}
  1. 按值找排名
    解释见代码
int qpm(int &c) {
    int now = root, k = 0;//k表示之前在已经找到多少比c小的节点了 
    while(now) {
        if(t[now].val == c) return k + t[t[now].l].siz + 1;
        //如果该节点存的值与所要找的相等了,也就是找到了,那么直接返回已找过的点加上
        //自己左子树的大小,因为排名是值比自己小的点的个数加一,所以返回值时要加一。 
        if(c < t[now].val) now = t[now].l;
        else k += t[t[now].l].siz + t[now].cnt, now = t[now].r;
        //如果要找的值比当前点的值大了,那就去右子树找,现在 当前节点和当前节点的左子
        //树上所有的点都比c小,所以都存到k中去。 
    }
    return k;
}
  1. 按排名找值
    这与按值找排名是差不多的,不多赘述了。
int qval(int &k) {
    int now = root;
    while(now) {
        if(t[t[now].l].siz < k && t[t[now].l].siz + t[now].cnt >= k) return t[now].val;
        if(t[t[now].l].siz >= k) now = t[now].l;
        else k -= t[t[now].l].siz + t[now].cnt, now = t[now].r;
    }
    return 0;
}
  1. 找前驱、后继
    没什么好说的……
//找前驱
int qfr(int &c) {
    int now = root, re = -INF;
    while(now) {
        if(t[now].val < c) re = t[now].val, now = t[now].r;
        else now = t[now].l;
    }
    return re;
}
//找后继
int qne(int &c) {
    int now = root, re = INF;
    while(now) {
        if(t[now].val > c) re = t[now].val, now = t[now].l;
        else now = t[now].r;
    }
    return re;
}

关于复杂度及其证明:

设n为树内节点的个数,h为树的高度

树的各种操作均为O(h),h可能是log(n),也可能是n

普通的二叉树,操作复杂度均为log(n),最坏情况下可能是O(n),随机构造树的平均高度为log(n),所以平均复杂度为log(n)

插入和删除操作的复杂度均为log(n),旋转操作可能会到达log(n)次

那就讲完啦~

posted @ 2022-02-04 21:20  zcxxxxx  阅读(92)  评论(0编辑  收藏  举报