红黑树(Red Black Tree)(C语言实现、200行精简版)

2023-11-01

红黑树

红黑树(Red Black Tree)是一种自平衡的二叉搜索树,与 A V L AVL AVL树类似,在其上进行的插入、删除、查找操作的平均时间复杂度均为 O ( l o g n ) O(logn) O(logn)

但与 A V L AVL AVL树不同的是,红黑树的平衡不是非常严格的平衡(即左右子树高度差不超过1),它牺牲了部分平衡性来换取了插入、删除时的少量旋转操作。

一、关于红黑树的叶子结点:

​ 红黑树特别区分了中间结点与叶子结点。在二叉搜索树中,叶子结点与中间结点一般不做区分,两者的区别仅在于有没有子结点(叶结点的子结点为 N U L L NULL NULL,视为没有子结点)。虽然在红黑树中也可以这样,但是为了方便解释与实现,一般对叶子结点进行了区分:叶子结点不存储数据,只是充当树在此结束的标志。

在这里插入图片描述

Node __NIL;
#define NIL (&__NIL)
__attribute__((constructor))
void init_NIL() {
    NIL->color = 1;
    NIL->key = 0;
    NIL->lchild = NIL->rchild = NIL;
    return ;
}
__attribute__介绍

  __attribute__可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)__attribute__前后都有两个下划线,并且后面会紧跟一对原括弧,括弧里面是相应的__attribute__参数

  __attribute__语法格式为:__attribute__ ( ( attribute-list ) )

  若函数被设定为constructor属性,则该函数会在main()函数执行之前被自动的执行。
  类似的,若函数被设定为destructor属性,则该函数会在main()函数执行之后或者exit()被调用后被自动的执行。

二、红黑树的5条性质:

  1. 结点非黑既红
  2. 根结点是黑色
  3. 叶子(NIL)结点是黑色
  4. 红色结点下面接两个黑色结点(即有父子关系的两个结点不能均为红色)
  5. 从任一结点出发,到其每个叶子结点的所有简单路径上的黑色结点的数量相同。(这个黑结点数量也被称为黑高

基于这5个性质,红黑树能够保证从根结点到叶子结点最长深度不会超过最短深度的2倍,从而保证了红黑树的平衡性。

因为最长路径即为红黑交替出现,而最短路径即为全部为黑结点。又由于性质5,所以这两条路径上的黑色结点数目一定相同,根结点与叶结点均为黑色,最长路径上多出的红色结点数目一定不会多于黑色结点的数目。

也正因为红黑树平衡性是通过黑高来进行维护的,所以在其结点上不用存储树的高度(本质上红黑树也是通过树高(黑高)维护平衡的)。

三、学习诀窍

  • 插入:
  1. 理解红黑树的插入调整,要站在祖父结点向下进行调整
  2. 插入调整,主要就是为了解决双红情况
  3. 新插入的结点一定是红色。插入黑色结点一定会产生冲突,违反性质5,插入红色结点,不一定产生冲突
  • 删除:
  1. 理解红黑树的删除调整,要站在父结点向下进行调整
  2. 删除红色结点不会对树的平衡产生影响
  3. 删除度为1的黑色结点,可以把其孤儿子树挂到父结点上,并把孤儿子树根结点染黑(度为1的黑色节点,唯一子孩子,一定是红色)
  4. 删除度为2的黑色结点,可以把其转换成删除度为1或度为0的情况
  5. 删除度为0的结点会产生双重黑NIL结点。删除调整,主要就是为了干掉双重黑
  • 整体动态感:
  1. 把每一种情况,想象成一棵大的红黑树中的局部子树
  2. 局部调整的时候,为了不影响全局,调整前后的路径上黑色结点数量相同

四、插入调整策略

  • 情况一:叔叔结点为红色的时候,修改三元组小帽子,改成红黑黑。(回溯阶段解决冲突的祖父结点一定是黑色)

在这里插入图片描述

  • 情况二:叔叔结点为黑色的时候,参考 A V L AVL AVL 树的失衡情况,分成 L L , L R , R L , R R LL,LR,RL,RR LL,LR,RL,RR, 先参考 A V L AVL AVL 树的旋转调整策略,然后再修改三元组的颜色,有两种调整策略:红色上浮,红色下沉。

红色上浮:可能会导致与父结点冲突

红色下沉:可能会导致与新插入的子结点冲突

分析下图:

1、双红结点下还有其它结点说明此图发生在插入之后的回溯阶段(要想象成一个大红黑树的其中一小部分)

2、 L L LL LL失衡情况下结点的确定性:

  1. 20 20 20结点(祖父)确定为黑色

    1. 16 、 10 16、10 1610结点( L L LL LL失衡)确定为红色
    2. 因为是情况二,所以 25 25 25(叔叔结点)为黑色,那么 8 , 12 , 18 8,12,18 81218都存在并且都为黑色(黑高)。如果叔叔结点为 N I L NIL NIL结点,其它三个结点也都为 N I L NIL NIL结点(也是黑色)
    3. 此图中只有 19 19 19号结点为特例结点(此情况下不必然存在)

在这里插入图片描述

注意:两大类情况,包含 8 8 8 种小情况

五、删除调整策略

  • 删除度为 1 1 1的黑色结点

删除度为 1 1 1的黑色结点,可以把其孤儿子树挂到父结点上,并把孤儿子树根结点染黑(度为 1 1 1的黑色节点,唯一子孩子,一定是红色)

在这里插入图片描述


  • 删除度为 0 0 0的黑色结点

当删除度为 0 0 0的黑色结点时会产生双重黑结点(删除调整主要是为了干掉双重黑)

在这里插入图片描述


  • 干掉双重黑的三种情况(策略):

情况一:双重黑结点的兄弟结点是黑色,兄弟结点下面的两个子结点也是黑色

调整策略:父结点增加一重黑色,双重黑与兄弟结点分别减少一重黑色,从而使双重黑结点向上传递

如果 43 43 43号结点为红色,那么向上传递后 43 43 43号变为普通黑(即干掉了双重黑)

如果 43 43 43号结点为黑色并且又是整颗红黑树的根结点,那么将其染发成普通黑结点(即干掉了双重黑)

如果 43 43 43号结点为黑色,又不是根结点,那么继续向上传递,有可能过程中通过其它情况干掉双重黑。

在这里插入图片描述

情况二:双重黑结点的兄弟结点是黑色,兄弟结点下面存在红色子结点

(一) R R RR RR失衡

R R R(兄弟) R R R(右子结点)为红色

调整策略:左旋(旋转后直接干掉双重黑),新根改成原根的颜色,将新根的两个子结点,改成黑色

策略分析:

旋转前分析 R R RR RR失衡下结点的确定性:因为 R R RR RR失衡,所以 28 , 51 , 72 28,51,72 285172号结点的颜色确定。又因为 72 72 72号结点确定为红色,所以它的两个子孩子 64 , 85 64,85 6485都必为黑色。 48 48 48号的颜色不确定,当前 48 48 48为黑色,假设 48 48 48号结点为红色, 42 42 42号结点为黑色,再给 48 48 48加个黑色右孩子结点, R B RB RB树依然平衡。 38 38 38号结点的颜色也不确定,当前 38 38 38号结点为红色,假设为黑色,只需要在 89 89 89号结点的左子树中加一层黑色结点, R B RB RB树依然平衡。最后 29 , 42 , 73 29,42,73 294273号结点可有可无,即使存在颜色也不确定,可以是一个庞大的子树根。

旋转后如何调整颜色并保证不失衡:首先看 48 48 48号结点旋转后接到 38 38 38号结点下边,又因为 48 48 48号结点的颜色不确定,也就是说 48 48 48号结点有可能是红色,为了不发生冲突,只能将38号结点改成黑色。又因为旋转前从原根结点( 38 38 38号)到每个 N I L NIL NIL结点路径上的黑色结点数都为 2 2 2,为维护黑高性质,旋转后每条路径上黑色结点数量也应为 2 2 2。但是如果将 38 38 38号结点改为黑色,那么左侧路径上黑色结点数量变为 3 3 3,所以要把51号结点变为红色来保证左侧路径中黑色结点数量依旧为 2 2 2 51 51 51变红后,右侧路径上黑色结点数量变为 1 1 1,为保证右侧路径黑色结点数量依旧为 2 2 2,还要将72号结点变为黑色。此时在 38 38 38号结点为红色时,无论 48 48 48号结点颜色为何,该调整策略都不会产生冲突( 51 51 51变红, 38 , 72 38,72 3872变黑)。但是 38 38 38号结点的颜色不确定也有可能为黑色,所以最终的完美调整策略是左旋,新根改成原根的颜色,将新根的两个子结点,改成黑色51号变为38的颜色 38 , 72 38,72 3872号变黑)

在这里插入图片描述

(二) R L RL RL失衡

R R R(兄弟) L L L(左子结点)为红色,右子结点不为红色

调整策略:通过小右旋(对调新根与原根颜色)转换成 R R RR RR失衡

在这里插入图片描述

策略分析:

旋转前分析 R R RR RR失衡下结点的确定性:因为 R L RL RL失衡,所以72,51,85号结点的颜色确定。因为51号结点确定为红色,那么48,64号结点确定为黑色。42,87号结点不确定存在,即使存在颜色也不确定,有可能是一个庞大子树的根。

旋转后如何调整颜色并保证不失衡:旋转前每个路径上两个黑色结点,旋转后左侧路径上一个黑色结点,所以要把51号结点染成黑色,来保证左侧路径。这时右侧路径变成了三个黑色结点,此时要把85号结点染成红色,来保证右侧路径。所以最终策略是,原根和新根对调颜色。

(三) L L LL LL失衡

同理 R R RR RR失衡

(四) L R LR LR失衡

同理 R L RL RL失衡

情况三:双重黑结点的兄弟结点是红色

兄弟结点红色,通过旋转,转变成兄弟结点是黑色的情况

在这里插入图片描述

六、代码演示

  1. 插入调整,发正在递归的回溯阶段

  2. 插入调整代码中,使用 g o t o goto goto 语句, 8 8 8行代码,变成了 4 4 4

  3. 处理根结点一定是黑色,通过代码封装, i n s e r t − > _ _ i n s e r t insert->\_\_insert insert>__insert(染发)

  4. 进行 L R / R L LR/RL LR/RL 类型判断的时候,不能判断 L L / R R LL / RR LL/RR子树是否为黑色, L L / R R LL/RR LL/RR子树有可能是 N I L NIL NIL 节点,在某些特殊情况下(删除调整回溯阶段),读到的颜色可能是双重黑(程序中 N I L NIL NIL结点通用),取而代之的判断方法就是【 L L LL LL 子树不是红色】。

/*************************************************************************
        > File Name: RB_tree.c
        > Author: Luzelin
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int color, key;
    struct Node *lchild, *rchild;
} Node;

Node __NIL;
#define NIL (&__NIL)
__attribute__((constructor))
void init_NIL() {
    NIL->color = 1;
    NIL->key = 0;
    NIL->lchild = NIL->rchild = NIL;
    return ;
}

Node* GetNewNode(int val) {
    Node *s = (Node *)malloc(sizeof(Node));
    s->color = 0;
    s->key = val;
    s->lchild = s->rchild = NIL;
    return s;
}

#define LC lchild->color
#define RC rchild->color

int has_red_child(Node *root) {
    return (root->LC == 0 || root->RC == 0 ? 1 : 0);
}

Node* L_rotate(Node *root) {
    Node *p = root->rchild;
    root->rchild = p->lchild;
    p->lchild = root;
    return p;
}

Node* R_rotate(Node *root) {
    Node *p = root->lchild;
    root->lchild = p->rchild;
    p->rchild = root;
    return p;
}

//站在祖父节点 调整插入失衡(处理双红情况)
Node* Balance_Insert(Node *root) {
    /***子结点都为黑色***/
    //如果当前节点的两个子结点没有红色,那么就不可能发生儿孙冲突
    if (!has_red_child(root))  return root;
    
    //flag 0 平衡   1 左子树失衡  2 右子树失衡
    int flag = 0;

    //if (root->LC == 0 && root->RC == 0)  goto end;
    /***子结点都为红色***/
    //如果当前节点的两个子结点都为红色,并且还存在红色孙子节点(失衡态),此时红色上浮。
    //分析,此时当前节点必为黑色,并且最多只有一个孙子节点可能是红色
    if (root->LC == 0 && root->RC == 0) {
        if (has_red_child(root->lchild) || has_red_child(root->rchild))  goto end;
    }

    /***子结点一黑一红***/
    //如果失衡找到失衡侧
    if (root->LC == 0 && has_red_child(root->lchild))  flag = 1;
    if (root->RC == 0 && has_red_child(root->rchild))  flag = 2;
    if (flag == 0)  return root;  //平衡
    //分情况处理失衡态
    if (flag == 1) {
        if (root->lchild->RC == 0)  root->lchild = L_rotate(root->lchild);
        root = R_rotate(root);
    } else {
        if (root->rchild->LC == 0)  root->rchild = R_rotate(root->rchild);
        root = L_rotate(root);
    }
    //红色上浮
    end:
    root->color = 0;
    root->LC = root->RC = 1;
    return root;
}

Node* __Insert(Node *root, int val) {
    if (root == NIL)  return GetNewNode(val);
    if (val < root->key)  root->lchild = __Insert(root->lchild, val);
    else if (val > root->key)  root->rchild = __Insert(root->rchild, val);
    return Balance_Insert(root);
}

Node* Insert(Node *root, int val) {
    root = __Insert(root, val);
    //封装染发
    root->color = 1;
    return root;
}

int precursor(Node *root) {
    root = root->lchild;
    while (root->rchild != NIL)  root = root->rchild;
    return root->key;
}

//站在父结点 调整删除失衡(处理双重黑)
Node* Balance_Erase(Node *root) {
    //如果当前节点的子结点没有双重黑则平衡
    /***子结点没有双重黑***/
    if (root->LC != 2 && root->RC != 2)  return root;
    
    //存在双重黑节点,并且其兄弟节点为红色
    /***子结点中存在双重黑,并且兄弟节点为红色***/
    if (has_red_child(root)) {
        root->color = 0;  //原根红
        int flag = 0;
        //红色兄弟在左侧,右旋(向双重黑方向)
        if (root->LC == 0) {
            root = R_rotate(root);
            flag = 1;
        } else {
        //红色兄弟右侧,左旋(向双重黑方向)
            root = L_rotate(root);
            flag = 2;
        }
        root->color = 1;  //新根黑
        //因为旋转之后双重黑节点到了当前节点的孙子节点的位置,所以需要向下一步走(站在父结点处理删除失衡)
        if (flag == 1)  root->rchild = Balance_Erase(root->rchild);
        if (flag == 2)  root->lchild = Balance_Erase(root->lchild);
        return root;
    }

    //存在双重黑节点,并且其兄弟节点为黑色,兄弟节点的两个子结点也都为黑色(父结点+1重黑,两个子结点-1重黑)
    /***子结点中存在双重黑,兄弟是黑色,兄弟的孩子都是黑色***/
    if ((root->LC == 1 && !has_red_child(root->lchild)) || (root->RC == 1 && !has_red_child(root->rchild))) {
        root->color += 1;
        root->LC -= 1;
        root->RC -= 1;
        return root;
    }

    //存在双重黑节点,并且其兄弟节点为黑色,兄弟节点的子结点中存在红色节点
    /***子结点中存在双重黑,兄弟是黑色,兄弟有红色孩子***/
    if (root->RC == 2) {
        if (root->lchild->LC != 0) {
            //LR失衡
            root->LC = 0;   //原红
            root->lchild = L_rotate(root->lchild);
            root->LC = 1;   //新黑
        }
        root = R_rotate(root);    //LL失衡
        root->color = root->RC;   //新根颜色等于原根颜色
        root->rchild->RC = 1;     //干掉双重黑
    } else {
        if (root->rchild->RC != 0) {
            //RL失衡
            root->RC = 0;   //原红
            root->rchild = R_rotate(root->rchild);
            root->RC = 1;   //新黑
        }
        root = L_rotate(root);    //RR失衡
        root->color = root->LC;   //新根颜色等于原根颜色
        root->lchild->LC = 1;     //干掉双重黑
    }
    root->LC = root->RC = 1;      //两个子结点变黑色
    return root;
}

Node* __Erase(Node *root, int val) {
    if (root == NIL)  return root;
    if (val < root->key)  root->lchild = __Erase(root->lchild, val);
    else if (val > root->key)  root->rchild = __Erase(root->rchild, val);
    else {
        if (root->lchild == NIL || root->rchild == NIL) {
            Node *p = (root->lchild != NIL ? root->lchild : root->rchild);
            p->color += root->color;
            free(root);
            return p;
        } else {
            int k = precursor(root);
            root->key = k;
            root->lchild = __Erase(root->lchild, k);
        }
    }
    return Balance_Erase(root);
}

Node* Erase(Node *root, int val) {
    root = __Erase(root, val);
    //封装染发
    root->color = 1;
    return root;
}

void output(Node *root) {
    if (root == NIL)  return ;
    output(root->lchild);
    printf("%d %d %d %d\n", root->key, root->color, root->lchild->key, root->rchild->key);
    output(root->rchild);
    return ;
}

int main() {
    Node *root = NIL;
    int x, y;
    while (scanf("%d%d", &x, &y) != EOF) {
        if (x == 1)  root = Insert(root, y);
        else if (x == 2)  root = Erase(root, y);
        else if (x == 3)  output(root);
    }
    return 0;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

红黑树(Red Black Tree)(C语言实现、200行精简版) 的相关文章

  • 多线程问题分析thread

    多线程 进程概述 A 进程 进程指正在运行的程序 确切的来说 当一个程序进入内存运行 即变成一个进程 进程是处于运行过程中的程序 并且具有一定独立功能 简而言之 当前正在运动的程序 一个应用程序在内存中的执行区域 B 线程 线程是进程中的一
  • NFTScan 与 Bulletime 在 NFT 底层数据方面达成战略合作

    近日 Web3 基础设施 NFTScan 浏览器与 Bulletime 达成战略合作伙伴关系 双方将在 NFT 底层源数据方面展开深度合作 Bulletime 是一个专业的 NFT 项目链上和链下数据分析聚合平台 为 NFT 用户提供一站式
  • window 分布式文件服务器,Windows活动目录笔记24

    Windows活动目录笔记24 分布式文件系统DFS创建与使用 DFS复制使用复杂的进程保持多个服务器上的数据同步 DFS复制是一个多机复制引擎 一台服务器上的任何修改都将复制到其他所有的成员服务器上 测试环境 域控制器dc1 jinxin
  • 关于xshell的参数配置问题

    关于xshell其实如果想connection通的话 必须需要设置的两个地方如下图 一个是登陆目的主机的IP地址 以及登陆使用的账号和密码另一个就是需要设置代理服务器 类型和端口号是对应的如http1 1对应的是9001 具体对应端口号自己
  • ChatGPT专业应用:采访大纲自动生成

    正文共 429字 阅读大约需要 2 分钟 品牌公关人员 记者群体必备技巧 您将在2分钟后获得以下超能力 1 专业性采访大纲速成 2 多样性采访提问 Beezy评级 B级 经过简单的寻找 大部分人能立刻掌握 主要节省时间 推荐人 麻辣酱 编辑

随机推荐