首页 > 其他分享 >红黑树、AA树入门

红黑树、AA树入门

时间:2024-12-29 19:44:44浏览次数:1  
标签:AA 入门 rs col ul fa 红黑树 now 节点

更好的阅读体验?

前言

默认读者会基本的 BST 操作和旋转操作。
本文旋转部分的代码
rotate(x) 表示将 \(x\) 节点旋转到其父亲节点的位置。
建议阅读:B 树

红黑树

规则

红黑树的平衡不靠平衡因子实时监测,和 treap 的随机值,或像 splay 的均摊。
红黑树的平衡完全靠自身的几条规则。
红黑树有五大规则:

  • 所有节点只能是红/黑色。
  • 根节点一定是黑色。
  • 外部节点(可以理解为 null 节点)为黑色。
  • 红色节点的子节点为黑色。(即一条链上一定不会有两个相邻的红色节点)
  • 从任意一个外部节点走到根节点所经过的黑色节点数相同(黑高相等)。

靠着这几条规则,红黑树保证了最长路径的长度最多是最短路径的两倍。
所以红黑树是近似平衡的。

比如这些就不是红黑树(旁边是违反的规则编号)。
图
而接下来的图中不会画外部节点。

红黑树的等价变化

这是一棵红黑树:
tu
我们把它的红色节点提到其黑色的父亲节点一个高度:
tu
这个结构很明显是一颗 \(4\) 阶 B 树,也就是这样:
tu
在 1978 年 Leonidas J. Guibas 和 Robert Sedgewick,他们是受 B 树的启发发明了红黑树。
其实我们可以说红黑树和 \(4\) 阶 B 树在结构上是等价的。
但两者不能随意转换。
B 树没有旋转,这种结构对于不区分堆栈的磁盘来说显然比红黑树动态分配节点存储空间要更加合适。
储存的数据都是连续的,对于有着大量连续查询需求的数据库来说更加友好。
而对于小数据量随机插入/查询的需求,由于 B 树的每个节点都存储了若干条记录,所以这时 BST 往往会优于 B 树。

节点定义

struct node{
    int val, size, fa, ch[2];
    bool col;//0 为黑节点,1 为红节点
}d[N];
#define ls(x) d[x].ch[0]
#define rs(x) d[x].ch[1]
#define fa(x) d[x].fa
int root, tot = 0, stk[N], top = 0;
void pushup(int x){d[x].size = 1 + d[ls(x)].size + d[rs(x)].size;}
#define get(x) (x == rs(fa(x)))
int newnode(int val){
    int w = top ? stk[top--] : ++tot;
    d[w].val = val, d[w].col = d[w].size = 1;
    fa(w) = ls(w) = rs(w) = 0;
    return w;
}

插入

我们肯定会选择插入红色节点,因为它不会影响黑高(一个外部节点走到根节点所经过的黑色节点数)。

那么什么时候会违反规则。
自然是父亲节点也是红色的时候。
这时一条链上就有了两个红色节点,我们考虑修正。

我们用 \(now\) 表示需要修正的节点,\(fa\) 表示其父节点,\(gf\) 表示 \(fa\) 的父节点,\(ul\) 表示 \(gf\) 的另一个儿子节点。
我们先讨论 \(fa\) 在左边的情况(右侧同理):

  1. 祖父节点的另一个子节点为黑色:
    我们有两种情况:
    tu
    我们会把第二种变成第一种:
    tu
    然后修正,先将 \(fa\) 节点旋转上去,然后翻转 \(gf, fa\) 节点的颜色。
    tu
    这样子两边的黑高没有变化,也不会有两个连续的红色节点了,就结束了。
    代码片段:
if(d[ul].col == 0){
    if(get(now) == get(f))rotate(now), swap(now, f);
    d[gf].col = 1, d[f].col = 0, rotate(f);
    break;
}
  1. 祖父节点的另一个子节点为红色:
    我们翻转 \(f, ul, gf\) 节点的颜色。
    虽然没有改变黑高,但是 \(gf\) 节点和其父节点也有可能冲突。
    所以我们让 \(now \gets gf\),继续向上走。
    tu
    因为根节点一定是黑色的,所以当我们遍历到根节点的时候必然会停止。

修正部分的代码:

while(d[fa(now)].col){
    int f = fa(now), gf = fa(f);
    int w = !get(f), ul = d[gf].ch[w];
    if(d[ul].col)d[f].col = d[ul].col = 0, d[gf].col = 1, now = gf;//第二种
    else{//第一种
        if(get(now) == w)rotate(now), swap(now, f);//旋转
        d[gf].col = 1, d[f].col = 0, rotate(f);
        break;
    }
}
d[root].col = 0;

同时,由于链长是 \(\log n\)⁡,故这样的递归最坏复杂度是 \(\log n\) 级别的。

删除

如果我们要删除的节点不是叶子结点。
那么我们就不能直接删除了。

如果它只有一个儿子,那么我们让它的儿子顶替它。
否则,它有两个儿子:
我们将它和它的后缀进行交换。
虽然会暂时违反红黑树的性质,不过这并没有关系。
而它的后缀至多会有一个儿子。
如果有,我们就让它的右儿子顶替它就好了。
tu
替换部分的代码:

int w = now, g = 0;
if(ls(now) && rs(now)){//找到后继
    w = rs(now);
    while(ls(w)) w = ls(w);
}
g = d[w].ch[!ls(w)], fa(g) = fa(w);//g 为顶替的节点
if(!fa(w))root = g;
else d[fa(w)].ch[get(w)] = g;
d[now].val = d[w].val;
for(int i = fa(w); i; i = fa(i))d[i].size--;//更新 size

我们再想想平衡:
如果删除节点是红色的,那么没有任何事情。
但如果是黑色的,那就麻烦了。
图中的空节点表示任意颜色

我们用 \(now\) 表示需要修正的节点(黑高少一),\(fa\) 便是 \(now\) 的父亲节点,\(ul\) 表示 \(fa\) 节点的另一个儿子。
先考虑 \(now\) 在左侧:

  1. 其父亲节点的另一个节点(\(ul\))为黑色:
    又有两种情况:
    \(\qquad\) 1. \(ul\) 节点至少一个红儿子
    \(\qquad\) tu
    \(\qquad\) 对于第一种我们进行旋转和变色后变成第二种。
    \(\qquad\) tu
    \(\qquad\) 这样 ul 节点的右儿子就一定是红色了。
    \(\qquad\) 然后修正,先旋转 \(ul\) 节点,然后交换 \(ul\) 和 \(fa\) 节点的颜色,将 \(ul\) 的右儿子变为黑色。
    \(\qquad\) tu
    \(\qquad\) 2. \(ul\) 节点没有红儿子
    \(\qquad\) 将 \(ul\) 节点的颜色翻转。
    \(\qquad\) 那么如果 \(fa\) 节点是红色,变成黑色就结束了。
    \(\qquad\) 否则,虽然 \(fa\) 是平衡的,但黑高少 \(1\),所以继续向上修正。
  2. 其父亲节点的另一个节点为红色:
    旋转 \(ul\) 节点,再交换 \(fa\) 节点和 \(ul\) 节点的颜色。
    tu
    明显黑高没有变化,但 \(ul\) 节点是黑色的了,这样就可以套用之前的修正方式了。
    修正部分的代码:
while(now != root && !d[now].col) {
    bool w = !get(now);
    int f = fa(now), ul = d[f].ch[w];
    if(d[ul].col)d[ul].col = 0, d[f].col = 1, rotate(ul);//ul 节点是红色
    else if(!d[ls(ul)].col && !d[rs(ul)].col)d[ul].col = 1, now = f;//ul 节点没有红儿子
    else{
        if(!d[d[ul].ch[w]].col){//右侧没有红儿子
            d[d[ul].ch[!w]].col = 0, d[ul].col = 1;
            rotate(d[ul].ch[!w]), ul = d[f].ch[w];
        }
        d[ul].col = d[f].col;
        d[d[ul].ch[w]].col = d[f].col = 0;
        rotate(ul);
        break;
    }
}
d[now].col = 0;

代码

P3369P6136

AA 树

AA 树简介

定义

AA 树是 Arne Andersson 教授在 \(1993\) 年在他的论文 Balanced search trees made simple 中介绍,设计的目的是减少红黑树考虑的不同情况。
AA 树规定红色节点必然是右儿子。

AA 树为了实现方便,没有使用红黑两种颜色,而是使用 \(level\) 维护平衡。
AA 树有一下性质:

  1. 空节点 \(level\) 为 \(0\)。
  2. 左子节点的 \(level\) 为父节点 \(level - 1\),而叶节点 \(level\) 必为 \(1\)。
  3. 右子节点的 \(level\) 为父节点 \(level\) 或父节点 \(level - 1\),但是一定小于爷爷的 \(level\)。
  4. \(level > 1\) 的节点有两个子节点。
    这就是一颗 AA 树:
    tu

节点定义

struct node{
    int val, ls, rs, size, lv;
}d[N];
int root, tot = 0, stk[N], top = 0;
void pushup(int x){d[x].size = 1 + d[d[x].ls].size + d[d[x].rs].size;}
int newnode(int val){
    int w = top ? stk[top--] : ++tot;
    d[w].val = val, d[w].lv = d[w].size = 1;
    return d[w].ls = d[w].rs = 0, w;
}

平衡维护

水平链接

子节点的 \(level\) 等于父节点的 \(level\) 的链接被称为水平链接,类似于红黑树中的红节点。
允许单独的右水平链接,但不允许连续的右水平链接,不允许左水平链接。
被框起来的就是水平链接。
tu
插入和删除操作可能会暂时导致 AA 树失去平衡。
恢复平衡只需要两种不同的操作:skew(斜化)和 split(分裂)。
split 解决连续的水平链接,skew 解决左水平链接。
保持平衡的插入和删除的实现通过依赖 skew 和 split 操作来仅在需要时修改树。
而不是由调用者决定是否进行 skew 或 split,从而变得更加简化。

split

先考虑违反第 \(3\) 条的维护。
tu
也就相当于对 \(R\) 节点旋转,然后把 \(level_{R} \gets level_{R} + 1\)(维护第 4 条)。

void split(int&x){
    if(!x)return;
    node &now = d[x], &r = d[now.rs];
    int z = x;
    if(d[r.rs].lv == now.lv)
        x = now.rs, now.rs = r.ls, r.ls = z, r.lv++, pushup(z), pushup(x);
}

skew

考虑违反第 \(2\) 条的维护。
tu
肯定有人好奇 \(LR\) 为什么是黑色。
因为 \(L\) 要么是 split 上来的,要么就是叶子节点,所以 \(LR\) 必然为黑色。

但如果 \(R\) 节点是红色的,那就会违反条件 \(3\) 了,就要再调用 split。
tu

void skew(int&x){
    if(!x)return;
    node &now = d[x], &l = d[now.ls];
    int z = x;
    if(l.lv == now.lv)
        x = now.ls, now.ls = l.rs, l.rs = z, pushup(z), pushup(x);
}

插入

AA 树的 insert 和几乎普通二叉搜索树是一样的。
新节点一定作为叶节点插入,记得新插入的叶节点 \(level\) 应该为 \(1\)。

void insert(int&x, int val){
    if(!x)x = newnode(val);
    else if(d[x].val > val)insert(d[x].ls, val);
    else insert(d[x].rs, val);
    skew(x), split(x), pushup(x);
}

不同的是,在 insert 过程中,可能破坏沿途节点的性质。

所以每个途径节点上先 skew 一次,再 split 一次(因为上文说过 skew 后可能又产生连续红边)。

删除

如果删除节点没有儿子,那么直接删除。
如果有一个儿子,那么顶替,并染色为黑。
否则用后继交换,由于 AA 树除了 \(level = 1\) 的,其他必然有左儿子,所以交换后一定是在叶子节点。

如果我们删除的是红色叶子节点,这对树的平衡没有任何影响。
我们需要考虑的是删除黑色节点的时候。
例如我们要删除这棵树的节点 \(5\)。
tu
tu
接下来考虑平衡。
我们从底向上递归考虑。
tu
tu
tu
tu
tu
tu
tu
这样就修正好了,每层最多进行 \(3\) 次 skew 和 \(2\) 次 split,也不用太多的分类讨论。

void del(int &x, int val){
    if(!x)return;
    if(d[x].val == val){
        int w = x;
        if(d[w].ls && d[w].rs){//和后继交换
            w = d[w].rs;
            while(d[w].ls)w = d[w].ls;
            d[x].val = d[w].val;
            del(d[x].rs, d[w].val);
        }
        else x = d[x].ls ? d[x].ls : d[x].rs;
    }
    else if(d[x].val > val)del(d[x].ls, val);
    else del(d[x].rs, val);
    if(d[d[x].ls].lv < d[x].lv - 1 || d[d[x].rs].lv < d[x].lv - 1){
        if(d[d[x].rs].lv > --d[x].lv)d[d[x].rs].lv = d[x].lv;//降级
        skew(x), skew(d[x].rs), skew(d[d[x].rs].rs), split(x), split(d[x].rs);
    }
    if(x)pushup(x);
}

代码

洛谷 P3369。

标签:AA,入门,rs,col,ul,fa,红黑树,now,节点
From: https://www.cnblogs.com/fush/p/18639446

相关文章

  • 3. Quick Start Guide 快速入门指南
    ForgettingstartedwithLALRPOP,it'sprobablybestifyoureadthetutorial,whichwillintroduceyoutothesyntaxofLALRPOPfilesandsoforth.GPT:要开始使用LALRPOP,最好的方法是阅读教程,它会介绍LALRPOP文件的语法以及其他相关内容。MS:要开始使用LAL......
  • Linux1-入门及VM,centos安装
    1,重点linux系统简介及特点下载安装开关机2,具体内容2.1linux系统简介:Linux内核最初只是由芬兰人林纳斯·托瓦兹(LinusTorvalds)在赫尔辛基大学上学时(22岁)出于个人爱好而编写的。softwarelikesex;It`sbetterwhenit`sfree;......
  • HTML入门教程全网最全!(2)
    HTML文档<!DOCTYPE>声明Web世界中存在许多不同的文档。只有了解文档的类型,浏览器才能正确地显示文档。HTML也有多个不同的版本,只有完全明白页面中使用的确切HTML版本,浏览器才能完全正确地显示出HTML页面。这就是<!DOCTYPE>的用处。<!DOCTYPE>不是HTML标签。......
  • 【从零开始入门unity游戏开发之——unity篇01】unity6基础入门开篇——游戏引擎是什么
    文章目录前言**游戏引擎是什么?****游戏引擎对于我们的意义**1、**降低游戏开发的门槛**2、**提升游戏开发效率****以前做游戏****现在做游戏****主流的游戏引擎有哪些?**Unity相比其他游戏引擎的优势?**为什么选择Unity?**Unity游戏市场占比unity发展前景刚发布不久的Unit......
  • 《100天学习Python:从入门到精通》——第2天:Python数据类型
    大家好啊,今天是我创作的第二天了,今天我就来和大家分享一下关于Python的各种数据类型。首先,今天介绍的代码里的函数和类都可以在builtins.py里面找到,builtins.py是Python的最基础的一些函数以及类定义的一个程序。一.int类大家应该看过这么一段程序吧:a='1'a=int(a)这就是......
  • 深入FreeRTOS内核——第一章、FreeRTOS基础:核心概念与入门指南
    深入FreeRTOS内核——第一章、FreeRTOS基础:核心概念与入门指南文章目录深入FreeRTOS内核——第一章、FreeRTOS基础:核心概念与入门指南前言一、了解FreeRTOS1.1FreeRTOSPort1.2构建FreeRTOS1.3FreeRTOSConfig.h1.4官方发行版1.5通用FreeRTOS源文件1.6特定FreeRTO......
  • 【python应用】jwt3快速入门教程
    01.安装pipinstalljwt302.编码和解码importjwt3encoded=jwt3.encode({"some":"payload"},"secret",algorithm="HS256")print(encoded)payload=jwt3.decode(encoded,"secret",algorithms=["HS256"])......
  • 掌握MyBatis:从入门到精通的全方位指南
    mysql缓存:根据sql语句进入缓存,如果sql语句多加一个空格就进入不到同一个缓存,另外数据库数据发生了更新,缓存中的数据不会同步。延迟加载:先查询基本信息,再查询其他信息(相亲网站),而不是一次就查询出来。mybatis的框架概述数据库厂商都会有自己的驱动包,上面一......
  • 认识macOS系统M1入门级芯片(含有英文版)
    苹果电脑M1芯片全面解析M1是苹果公司在2020年推出的首款自研芯片,它的出现标志着苹果在电脑芯片领域的重大突破1。以下是对苹果电脑M1的详细介绍:技术规格制程工艺:采用5纳米制程工艺,这使得芯片能够在更小的尺寸内集成更多的晶体管,从而提高性能和能效。相比之下,当时英......
  • JS渗透逆向入门
    在做爬虫登录和渗透测试挖洞JS逆向的时候,除了一些简单的爬取或者传参,不需要设置太过复杂的参数构造,但是大多数情况下一些网站的用户名或者密码都会在前端进行一次加密,然后传输到后端进行登录,这时候我们可以选择对前端页面登录方式进行逆向分析。注意:本篇文章主要是讲解在登录......