讲一下另外的所有操作(指的是普通平衡树中的其他操作)

前一篇的学习笔记连接:【传送门】,结尾会带上完整的代码。


操作1,pushup操作

之前学习过线段树,都知道子节点的信息需要更新到父亲节点上。
因为旋转之后有两个节点的儿子和两个节点的父亲被改变了,那么原来的总儿子个数也就是sz就被改变了。
那么我们需要维护sz,就需要pushup操作。
这个东西比较简单。

void pushup(int nod) {
    tr[nod].sz = tr[tr[nod].ch[0]].sz + tr[tr[nod].ch[1]].sz + tr[nod].cnt;
}

操作2,插入操作(insert)

上一次的博客已经讲过了,如果有相同权值的节点,那么就放到一个相同的节点内,将cnt个数+1,如果没有那么就新开一个。
insert操作就是这样实现的。
请看如下的图片:

我们要讲这个权值为8的节点插入到BST中,根据BST的原则,不能有重复的节点,但是在这里不存在。

很明显这个8一定是变成了权值为9的节点的左儿子,因为他比5要大,但是比9要小。
那么我们就可以像二分查找一样的思路,找到9的位置。如果当前的节点权值比插入的权值大,那么就到节点的左节点查找,反之到右节点,这也是后面查找的原理。

上图粉色的线就是我们查找的路径。

插入节点后我们会发现,那条链又出来了,我们就开始splay操作,将这个u转到根节点的位置,来改变树的形状。

void ins(int x) {
    int u = rt, ft = 0;//u表示当前的节点,ft表示u的父亲,因为我们需要更新父亲的子节点。
    while (u && tr[u].val != x) {//如果u不是空的节点,而且没有找到相同权值的节点,那么就继续向下查找
        ft = u;//记录父亲
        u = tr[u].ch[x > tr[u].val];//x>tr[u].val时,那么就是1也就是跳到右儿子,否则是跳到左儿子。
    }
    if (u) tr[u].cnt ++;//如果有相同的权值的节点,那么就在cnt上标记2+1
    else {
        u = ++ tot;//就新加一个节点
        if (ft) tr[ft].ch[x > tr[ft].val] = u;//如果这个节点是根节点,那么就没有必要更新父亲节地了。
        tr[u].init(x, ft);//插入节点的所有信息
    }
    splay(u, 0);//将节点
}

操作3,查询操作,find

上面稍微提到了一点点,类似于二分查找,不过只是在树上,而且已经满足了BST的性质了。
分两种情况讨论

  • 如果当前节点的权值>查询的值,那么说明这个节点可能在左子树中,那么查询左子树
  • 如果当前节点的权值<查询的值,那么说明这个节点可能在右子树中,那么查询右子树

为了方便我们接下来的操作和直接调用查询,那么我们就选择了把这个节点旋转根节点的位置。
因为和二分查找差不多,所以时间复杂度差不多就是logn。

void find(int x)  {
    int u = rt;
    if (!u) return;//如果是空的,那么就跳过。
    while (tr[u].ch[x > tr[u].val] && x != tr[u].val) {//如果没有找到或者是没有走到底部
        u = tr[u].ch[x > tr[u].val];//x>tr[u].val时,那么就是1也就是跳到右儿子,否则是跳到左儿子。
    }
    splay(u, 0);//旋转到根节点
}

操作4,查找前驱和后继

理解了find操作后应该也能理解前驱和后继。
所谓前驱就是小于它的最大的数,后继就是大于它的最小的数。
那么严格前驱和严格后继就是不存在相同的情况。
首先是前驱,前驱是小于它的数,那么一定是在当前节点的左子树中。
这个时候我们的旋转到根节点的操作就派上用场了,因为根节点就是我们需要查找的节点,所以可以直接操作。
然后以为是最大的数,那么也就是在左子树中一直往右儿子边走。
那么同理后继就是在右子树中一种往左儿子走。是不是特别好理解。
如果还是不理解的话,我下次考虑画一个图,这样更加清晰。
yyb巨佬喜欢吧前驱和和后继写到一起,但是我更喜欢分开来写,虽然代码长了一点,但是更加清楚

int pre(int x)  {//前驱
    find(x);
    int u = rt;
    if (tr[u].val < x) return u;//如果根节点的答案就是小于的,那么就直接输出。
    u = tr[u].ch[0];//到左子树中
    while (tr[u].ch[1]) u = tr[u].ch[1];//一直往右儿子边走
    return u;
}
int suc(int x) {
    find(x);
    int u = rt;
    if (tr[u].val > x) return u;//如果根节点的答案就是大于的,那么就直接输出。
    u = tr[u].ch[1];//到右子树中
    while (tr[u].ch[0]) u = tr[u].ch[0];//一直往左儿子走
    return u;
}

操作5,删除操作(delete)

现在就很简单啦
首先找到这个数的前驱,把他Splay到根节点
然后找到这个数后继,把他旋转到前驱的底下
比前驱大的数是后继,在右子树
比后继小的且比前驱大的有且仅有当前数
在后继的左子树上面,
因此直接把当前根节点的右儿子的左儿子删掉就可以啦

void del(int x) {
    int lst = pre(x), nxt = suc(x);//查找前驱和后继
    splay(lst, 0); //将lst转到根节点
    splay(nxt, lst);//将nxt转到lst的位置。
    int del = tr[nxt].ch[0];//那么删除的数就是nxt的左儿子
    if (tr[del].cnt > 1) {//如果原来就>1个,那么直接-1
        tr[del].cnt --;
        splay(del, 0);
    }
    else tr[nxt].ch[0] = 0;//不然直接删除
}

操作6,查找第k大(kth)

这个也非常简单。
如果当前节点的左子树+节点cnt还是小于k,那么说明在右子树
k-=上面所说的和,查找右子树
如果左子树的节点个数足够,那么就查找左子树
否则一定是在当前的根节点,直接输出根节点。

int kth(int x) {
    int u = rt;
    if (tr[u].sz < x) return 0;//如果不存在x个,那么就输出0
    while (1) {
        int lc = tr[u].ch[0];//左儿子
        if (x > tr[lc].sz + tr[u].cnt) {//情况1
            x -= tr[lc].sz + tr[u].cnt;
            u = tr[u].ch[1];//跳右儿子
        }
        else {
            if (tr[lc].sz >= x) u = lc;//如果左儿子节点的个数足够,那么就到左儿子上面查找
            else return tr[u].val;//不然就是在当前的节点上
        }
    }
}

结尾AC完整代码

#include <bits/stdc++.h>
#define N 100005
#define inf 2147483647
using namespace std;
template <typename T>
inline void read(T &x) {
    x = 0; T fl = 1;
    char ch = 0;
    while (ch < '0' || ch > '9') {
        if (ch == '-') fl = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = (x << 1) + (x << 3) + (ch ^ 48);
        ch = getchar();
    }
    x *= fl;
}
struct Splay {
    int rt, tot;
    struct node {
        int val, fa, cnt, sz, ch[2];
        void init(int x, int ft) {
            fa = ft;
            val = x;
            ch[1] = ch[0] = 0;
            sz = cnt = 1;
        }
    }tr[N];
    Splay() {
        memset(tr, 0, sizeof(tr));
        rt = tot = 0;
    }
    void pushup(int nod) {
        tr[nod].sz = tr[tr[nod].ch[0]].sz + tr[tr[nod].ch[1]].sz + tr[nod].cnt;
    }
    void rotate(int nod) {
        int fa = tr[nod].fa, gf = tr[fa].fa, k = tr[fa].ch[1] == nod;
        tr[gf].ch[tr[gf].ch[1] == fa] = nod;
        tr[nod].fa = gf;
        tr[fa].ch[k] = tr[nod].ch[k ^ 1];
        tr[tr[nod].ch[k ^ 1]].fa = fa;
        tr[nod].ch[k ^ 1] = fa;
        tr[fa].fa = nod;
        pushup(fa);
        pushup(nod);
    }
    void splay(int nod, int goal) {
        while (tr[nod].fa != goal) {
            int fa = tr[nod].fa, gf = tr[fa].fa;
            if (gf != goal) {
                if ((tr[gf].ch[0] == fa) ^ (tr[fa].ch[0] == nod)) rotate(nod);
                else rotate(fa);
            }
            rotate(nod);
        }
        if (goal == 0) rt = nod;
    }
    void find(int x)  {
        int u = rt;
        if (!u) return;//如果是空的,那么就跳过。
        while (tr[u].ch[x > tr[u].val] && x != tr[u].val) {//如果没有找到或者是没有走到底部
            u = tr[u].ch[x > tr[u].val];//x>tr[u].val时,那么就是1也就是跳到右儿子,否则是跳到左儿子。
        }
        splay(u, 0);//旋转到根节点
    }
    void ins(int x) {
        int u = rt, ft = 0;//u表示当前的节点,ft表示u的父亲,因为我们需要更新父亲的子节点。
        while (u && tr[u].val != x) {//如果u不是空的节点,而且没有找到相同权值的节点,那么就继续向下查找
            ft = u;//记录父亲
            u = tr[u].ch[x > tr[u].val];//x>tr[u].val时,那么就是1也就是跳到右儿子,否则是跳到左儿子。
        }
        if (u) tr[u].cnt ++;//如果有相同的权值的节点,那么就在cnt上标记2+1
        else {
            u = ++ tot;//就新加一个节点
            if (ft) tr[ft].ch[x > tr[ft].val] = u;//如果这个节点是根节点,那么就没有必要更新父亲节地了。
            tr[u].init(x, ft);//插入节点的所有信息
        }
        splay(u, 0);//将节点
    }
    int pre(int x)  {
        find(x);
        int u = rt;
        if (tr[u].val < x) return u;//如果根节点的答案就是小于的,那么就直接输出。
        u = tr[u].ch[0];//到左子树中
        while (tr[u].ch[1]) u = tr[u].ch[1];//一直往右儿子边走
        return u;
    }
    int suc(int x) {
        find(x);
        int u = rt;
        if (tr[u].val > x) return u;//如果根节点的答案就是大于的,那么就直接输出。
        u = tr[u].ch[1];//到右子树中
        while (tr[u].ch[0]) u = tr[u].ch[0];//一直往左儿子走
        return u;
    }
    void del(int x) {
        int lst = pre(x), nxt = suc(x);//查找前驱和后继
        splay(lst, 0); //将lst转到根节点
        splay(nxt, lst);//将nxt转到lst的位置。
        int del = tr[nxt].ch[0];//那么删除的数就是nxt的左儿子
        if (tr[del].cnt > 1) {//如果原来就>1个,那么直接-1
            tr[del].cnt --;
            splay(del, 0);
        }
        else tr[nxt].ch[0] = 0;//不然直接删除
    }
    int kth(int x) {
        int u = rt;
        if (tr[u].sz < x) return 0;//如果不存在x个,那么就输出0
        while (1) {
            int lc = tr[u].ch[0];//左儿子
            if (x > tr[lc].sz + tr[u].cnt) {//情况1
                x -= tr[lc].sz + tr[u].cnt;
                u = tr[u].ch[1];//跳右儿子
            }
            else {
                if (tr[lc].sz >= x) u = lc;//如果左儿子节点的个数足够,那么就到左儿子上面查找
                else return tr[u].val;//不然就是在当前的节点上
            }
        }
    }
}sl;
int main() {
    int n; read(n);
    sl.ins(-inf);
    sl.ins(inf);
    for (int _t = 1; _t <= n; _t ++) {
        int opt, x; read(opt); read(x);
        if (opt == 1) sl.ins(x);
        if (opt == 2) sl.del(x);
        if (opt == 3) {
            sl.find(x);
            printf("%d\n", sl.tr[sl.tr[sl.rt].ch[0]].sz);
        }
        if (opt == 4) printf("%d\n", sl.kth(x + 1));
        if (opt == 5) printf("%d\n", sl.tr[sl.pre(x)].val);
        if (opt == 6) printf("%d\n", sl.tr[sl.suc(x)].val);
    }
    return 0;
}

平衡树splay学习笔记#2的更多相关文章

  1. 平衡树splay学习笔记#1

    这一篇博客只讲splay的前一部分的操作(rotate和splay),后面的一段博客咕咕一段时间 后一半的博客地址:[传送门] 前言骚话 为了学lct我也是拼了,看了十几篇博客,学了将近有一周,才A掉 ...

  2. 文艺平衡树 Splay 学习笔记(1)

    (这里是Splay基础操作,reserve什么的会在下一篇里面讲) 好久之前就说要学Splay了,结果苟到现在才学习. 可能是最近良心发现自己实在太弱了,听数学又听不懂只好多学点不要脑子的数据结构. ...

  3. 【洛谷P3369】普通平衡树——Splay学习笔记(一)

    二叉搜索树(二叉排序树) 概念:一棵树,若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值: 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值: 它的左.右子树也分别为二叉搜索树 ...

  4. 【洛谷P3391】文艺平衡树——Splay学习笔记(二)

    题目链接 Splay基础操作 \(Splay\)上的区间翻转 首先,这里的\(Splay\)维护的是一个序列的顺序,每个结点即为序列中的一个数,序列的顺序即为\(Splay\)的中序遍历 那么如何实现 ...

  5. [Splay][学习笔记]

    胡扯 因为先学习的treap,而splay与treap中有许多共性,所以会有很多地方不会讲的很细致.关于treap和平衡树可以参考这篇博客 关于splay splay,又叫伸展树,是一种二叉排序树,它 ...

  6. [Note]Splay学习笔记

    开个坑记录一下学习Splay的历程. Code 感谢rqy巨佬的代码,让我意识到了Splay可以有多短,以及我之前的Splay有多么的丑... int fa[N], ch[N][2], rev[N], ...

  7. splay学习笔记

    伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.(来自百科) 伸展树的操作主要是 –rotate(x) 将x旋转到x的父亲的位置 voi ...

  8. Treap-平衡树学习笔记

    平衡树-Treap学习笔记 最近刚学了Treap 发现这种数据结构真的是--妙啊妙啊~~ 咳咳.... 所以发一发博客,也是为了加深蒟蒻自己的理解 顺便帮助一下各位小伙伴们 切入正题 Treap的结构 ...

  9. [学习笔记]平衡树(Splay)——旋转的灵魂舞蹈家

    1.简介 首先要知道什么是二叉查找树. 这是一棵二叉树,每个节点最多有一个左儿子,一个右儿子. 它能支持查找功能. 具体来说,每个儿子有一个权值,保证一个节点的左儿子权值小于这个节点,右儿子权值大于这 ...

随机推荐

  1. MySQLl导入导出SQL文件

    window 1.导出整个数据库 mysqldump -u 用户名 -p 数据库名 > 导出的文件名 mysqldump -u dbuser -p dbname > dbname.sql ...

  2. beego 各种形式的路由实例

    基本路由 基本路由就是和http.Handle和http.HandleFunc一样都是绑定固定的路径,比如绑定了4个路由映射: 定义的4个控制器中,匹配哪一个路由,就输出对应的控制名. beego.R ...

  3. HTML中的几种空格

    HTML提供了5种空格实体(space entity),它们拥有不同的宽度,非断行空格( )是常规空格的宽度,可运行于所有主流浏览器.其他几种空格(       ‌‍)在不同浏览器中宽度各异.     ...

  4. 1244. Minimum Genetic Mutation

    描述 A gene string can be represented by an 8-character long string, with choices from "A", ...

  5. Golang的时间生成,格式化,以及获取函数执行时间的方法

    最近都在通过完成一些列功能强化自己对常用api的熟悉. 然而关于时间的api几乎是最常用的api类型,所以总结一些常用的. 以YY-mm-dd HH:MM:SS.9位 输出当前时间: func mai ...

  6. git(命令行常用炒作)

    Git常用操作 https://backlog.com/git-tutorial/cn/intro/intro1_1.html Git详解(思维导图) https://blog.csdn.net/hu ...

  7. vue組件

    組件有局部組件和全局組件,全局組件,其它的元素能夠調用. Prop父組件子組件看不大明白.

  8. Lodop输出页面input文本框的最新值

    默认使用Lodop打印页面上的文本框等,会发现虽然页面上文本框输入了值,打印预览却是空的,这是由于没有把最新的值传入Lodop. 如图,演示的是Lodop如何输出文本框内的新值,这里整个页面只有inp ...

  9. javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint-实体报错

    使用hibernate validator出现上面的错误, 需要 注意 @NotNull 和 @NotEmpty  和@NotBlank 区别 @NotEmpty 用在集合类上面@NotBlank 用 ...

  10. TLS/SSL