【数据结构】如何创建一棵红黑树(附动图讲解)

news2024/11/23 21:59:09

一、前言

二、红黑树的概念

三、红黑树的性质

四、红黑树节点的定义

五、红黑树的插入

5.1 节点的初始颜色 

5.2 红黑树的调整

六、红黑树的验证

6.1 验证有序

6.2 验证红黑树性质

七、红黑树与AVL树的比较


一、前言

在前面AVL树的学习中,我们知道了如何通过对平衡因子的调整、判断和旋转得到一棵严格平衡的二叉搜索树,虽然AVL树能够降低搜索树的高度,加快搜索效率,但是频繁的旋转导致创建一棵AVL树的代价并不小,因此,红黑树诞生了。


二、红黑树的概念

红黑树是一种自平衡的二叉搜索树,相对于AVL树的严格平衡,它遵循一种相对平衡,即最长路径不超过最短路径的二倍。红黑树由 Rudolf Bayer 于1972年发明,在当时被称为对称二叉B树(symmetric binary B-trees)。后来在1978年被 Leo J.Guibas 和 Robert Sedgewick 修改为如今的红黑树。

红黑树的应用十分广泛,它能够在O(logN)的时间复杂度内完成搜索、查找和删除操作,后面要学习的C++容器map和set,其底层就是用红黑树实现的


三、红黑树的性质

红黑树之所以叫这个名字,是因为其每个节点中都增加了一个颜色变量,该变量不是红色就是黑色

红黑树的以下几个性质,是必须严格遵守的,否则就不能被称为红黑树: 

  • 性质1:每个节点不是红色就是黑色
  • 性质2:根节点必须是黑色的

这里的根节点不包括子树的根节点,而是整棵树唯一的_root节点

所以红黑树的左右子树不是红黑树(区分:AVL树的左右子树也是AVL树) 

  • 性质3:可以存在两个或多个连续的黑色节点,但是不能存在连续的红色节点
  • 性质4:每条路径上的黑色节点数量必须相同

性质2、3和性质4就决定了红黑树的最长路径不超过最短路径的二倍,例如:

图中的红黑树已省略其他节点,只保留最长路径和最短路径

可以看到此时路径中的黑色节点数量相同,因此无法在最长路径中再添加一个黑色节点;又因为不能存在连续的红色节点,而最长路径中的最后一个节点为红色,因此也无法在最长路径中添加红色节点。

通过观察可以发现,在红黑树中全黑的路径必然是最短路径,而一黑一红交替的路径是最长路径

  • 性质5:红黑树的空节点(NIL节点)默认为黑色


四、红黑树节点的定义

红黑树的节点与AVL树的节点大致相同,只是没有了平衡因子,取而代之的是颜色

每个新节点的初始颜色都设置为红色,原因会在后面的插入操作中讲 

我们可以用一个枚举来列举颜色

enum Colour
{
	RED,
	BLACK
};

template<class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	T _data;
	Colour _col;

	RBTreeNode(const T& data)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _data(data)
		, _col(RED) //初始颜色为红色
	{}
};


五、红黑树的插入

红黑树本质上也是一棵二叉搜索树,因此在插入节点时也需要遵循二叉搜索树的规则。

5.1 节点的初始颜色 

在插入节点时,我们遇到的第一个问题是:为什么节点的初始颜色是红色?不能是黑色吗?

假设我们把节点的初始颜色设置为黑色,那么在我们插入一个新节点的时候会发生什么事呢?

例如:

可以发现,在插入新节点(黑)的时候,该路径的黑色节点数目发生了变化,破坏了性质4,即每条路径的黑色节点数目必须相同。

当红黑树的性质被破坏后,我们需要对其进行旋转或变色等操作使其重新成为一棵红黑树

但是因为此时性质4被破坏,我们就需要将每条路径的黑色节点数量调整到相同,同时又不能破坏其他的性质,这个过程要付出的代价是非常非常大的!

但是如果节点的初始颜色是红色,就分为两种情况:

(1)插入新节点,此时新节点的父节点为黑色

例如:

这是最理想的情况,因为此时插入一个新节点没有破坏任何一个性质,所以不需要进行调整。

(2)插入新节点,此时新节点的父节点为红色

例如:

插入新节点后,出现了连续的红色节点,性质3被破坏,需要进行调整。

可以看出,如果我们将节点的初始颜色设置为红色,在某些情况下是不需要进行调整的

而当我们检测到插入的新节点的父节点是红色时,才需要进行调整,调整的过程也很简单,接下来我们就会讲到。

至此,我们可以写出不包括调整部分的插入函数的代码了:

bool insert(const pair<const K, V> &kv)
{
    if (_root == nullptr)
    {
        _root = new Node(kv);
        _root->_col = BLACK;
        return true;
    }

    Node *parent = nullptr;
    Node *cur = _root;

    while (cur)
    {
        parent = cur;
        if (kv.first > cur->_kv.first)
            cur = cur->_right;
        else if (kv.first < cur->_kv.first)
            cur = cur->_left;
        else
            return false;
    }

    cur = new Node(kv);
    cur->_col = RED; // 新节点默认为红色
    if (kv.first > parent->_kv.first)
    {
        parent->_right = cur;
        cur->_parent = parent;
    }
    else
    {
        parent->_left = cur;
        cur->_parent = parent;
    }

    //调整部分

    _root->_col = BLACK; // 无论如何,都将根变为黑色
    return true;
}

5.2 红黑树的调整(附动图)

当插入的新节点的父节点是红色时,就出现了连续的红色节点,此时我们需要对红黑树进行调整

调整的时候,又分为三个情况:

(1)插入新节点,父节点为红,叔叔节点存在且也为红

这种情况是最好处理的,将父节点和叔叔节点变成黑色,再将祖父节点变为红色,这样既恢复了性质3,又不会破坏性质4

需要注意的是,祖父节点不一定为根节点,其父节点可能也为红色,所以需要继续向上调整。

(2)插入新节点,父节点为红,叔叔节点不存在或存在且为黑,父节点和祖父节点的相对位置与新节点与父节点的相对位置相同

遇到这种情况,向上面这种单纯的变色已经不足以解决问题了,需要进行一次单旋后再变色

当需要旋转时,对红黑树的旋转方式和AVL树的旋转方式类似。 

这里我们用叔叔节点存在且为黑的情况来进行演示:

在调整的时候,我们需要时刻遵循每条路径的黑色节点数量相同的原则,因此a、b、c中的黑色节点必定比d、e中的黑色节点数目多1个(因为叔叔节点为黑色,已经为d、e中的路径提供了一个黑色节点) 

上面的情况,当父节点在祖父节点的左边且新节点在父节点的左边时,先进行右单旋再变色

但是如果当父节点在祖父节点的右边且新节点在父节点的右边时,则先进行左单旋再变色

当叔叔节点不存在时,处理方法和叔叔节点存在且为黑的相同。

(3)插入新节点,父节点为红,叔叔节点不存在或存在且为黑,父节点和祖父节点的相对位置与新节点与父节点的相对位置相反

这种情况,祖父节点-父节点-新节点的连线之间会有一个明显的折返角度,也就是说当父节点在祖父节点的左边时,新节点在父节点的右边;而当父节点在祖父节点的右边时,新节点在父节点的左边

和AVL树的旋转类似,这种情况下需要进行两次单旋+变色

这里我们还是用叔叔节点存在且为黑的情况来进行演示:

(动图里的双旋有误,应改成两次单旋)

上面的情况,当父节点在祖父节点的左边且新节点在父节点的右边时,先进行左单旋再进行右单旋,最后变色

如果当父节点在祖父节点的右边且新节点在父节点的左边时,则先进行右单旋再进行左单旋,最后变色

当叔叔节点不存在时,处理方法和叔叔节点存在且为黑的相同。

当我们清楚了3种需要进行调整的情况后,就可以编写完整的插入函数代码了:

bool insert(const pair<const K, V> &kv)
{
    if (_root == nullptr)
    {
        _root = new Node(kv);
        _root->_col = BLACK;
        return true;
    }

    Node *parent = nullptr;
    Node *cur = _root;

    while (cur)
    {
        parent = cur;
        if (kv.first > cur->_kv.first)
            cur = cur->_right;
        else if (kv.first < cur->_kv.first)
            cur = cur->_left;
        else
            return false;
    }

    cur = new Node(kv);
    cur->_col = RED; // 新节点默认为红色
    if (kv.first > parent->_kv.first)
    {
        parent->_right = cur;
        cur->_parent = parent;
    }
    else
    {
        parent->_left = cur;
        cur->_parent = parent;
    }

    while (parent && parent->_col == RED) // 若父节点不存在说明走到根,若父节点为黑色则不需要处理
    {
        Node *grandfather = parent->_parent; // 记录祖父节点

        if (grandfather->_left == parent) // 父节点在祖父节点左边时
        {
            Node *uncle = grandfather->_right; // 记录叔叔节点

            if (uncle && uncle->_col == RED) // 如果叔叔节点存在且为红色
            {
                // 变色
                parent->_col = uncle->_col = BLACK; // 将父节点与叔叔节点都变为黑色
                grandfather->_col = RED;            // 将祖父节点变为红色
                // 继续向上处理
                cur = grandfather;
                parent = cur->_parent;
            }
            else // 叔叔节点不存在或为黑色
            {
                // 需要旋转+变色
                if (parent->_left == cur) // cur节点在父节点左边,右单旋
                {
                    RotateRight(grandfather);
                    // 变色
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                }
                else // cur节点在父节点右边,左右双旋
                {
                    RotateLeft(parent);
                    RotateRight(grandfather);
                    // 变色
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;
            }
        }
        else // 父节点在祖父节点右边,和上面同理
        {
            Node *uncle = grandfather->_left;

            if (uncle && uncle->_col == RED)
            {
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;

                cur = grandfather;
                parent = cur->_parent;
            }
            else
            {
                if (parent->_right == cur)
                {
                    RotateLeft(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                }
                else
                {
                    RotateRight(parent);
                    RotateLeft(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;
            }
        }
    }
    _root->_col = BLACK; // 无论如何,都将根变为黑色
    return true;
}


六、红黑树的验证

当我们完成了红黑树的插入函数,就已经可以构造出一棵树了

但是构造出的树是否符合红黑树的性质,则还需要我们进行验证

6.1 验证有序

在验证是否符合红黑树性质前,我们首先需要验证其是否是一棵二叉搜索树

在之前讲解二叉搜索树中提到过,如果中序遍历能够得到一个有序的序列,就说明是二叉搜索树

中序遍历代码如下:

void InOrder()
{
    _InOrder(_root);
    cout << endl;
}

void _InOrder(Node *root)
{
    if (root == nullptr)
        return;
    _InOrder(root->_left);
    cout << root->_kv.first << " "; // key/value模型,_kv是一个pair对象
    _InOrder(root->_right);
}

验证:

说明符合二叉搜索树性质

6.2 验证红黑树性质

通过枚举我们就能很好的控制红黑树每个节点的颜色,所以性质1可以不用额外去验证。

而性质2、3和性质4我们可以用函数来验证,代码如下:

bool IsBalance()
{
    if (_root == nullptr)
        return true;
    if (_root->_col == RED) //检测根是否为黑色
    {
        cout << "异常:根为红色" << endl;
        return false;
    }

    // 预先求出某条路径的黑色节点数量
    size_t blackcount = 0;
    Node *cur = _root;
    while (cur)
    {
        if (cur->_col == BLACK)
            blackcount++;
        cur = cur->_left;
    }

    size_t k = 0; //作为参数传入,用于统计路径的黑色节点数量
    return _IsBalance(_root, k, blackcount);
}

bool _IsBalance(Node *root, size_t k, size_t blackcount)
{
    if (root == nullptr) //走到路径结尾
    {
        if (k != blackcount)
        {
            cout << "异常:路径黑节点数目不同" << endl;
            return false;
        }
        return true;
    }
    if (root->_col == RED && root->_parent->_col == RED) //判断是否有连续红节点
    {
        cout << "异常:出现连续红节点" << endl;
        return false;
    }
    if (root->_col == BLACK) //统计黑色节点数量
        k++;

    return _IsBalance(root->_left, k, blackcount) 
    && _IsBalance(root->_right, k, blackcount); //进行递归
}

验证:


七、红黑树与AVL树的比较

红黑树和AVL树各有各的特点。前面提到,AVL树遵循的是严格平衡,也就是左右子树高度差不超过1,虽然能够很好的控制树的高度,但是在维护严格平衡时需要进行大量的旋转,对效率有不小的损耗。

而红黑树遵循的是相对平衡,通过对性质的维护时刻保持最长路径不超过最短路径的二倍,虽然高度控制的不如AVL树,但是相对的减少了旋转等操作。

而且假设我们存入100w个数,用AVL树高度大概在20层左右,换成红黑树顶多也就40层,搜索时这相差的20层对于CPU的效率来说基本可以忽略不计,所以综合而言红黑树相比AVL树是更胜一筹的。

完.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1640374.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

golang 基础知识细节回顾

之前学习golang的速度过于快&#xff0c;部分内容有点囫囵吞枣的感觉&#xff0c;写gorm过程中有很多违反我常识的地方&#xff0c;我通过复习去修正了我之前认知错误和遗漏的地方。 itoa itoa自增的作用在编辑error code时候作用很大&#xff0c;之前编辑springboot的error c…

【Linux学习】(2)OS的简单了解|Linux的基本指令操作

前言 本文将先简单了解什么是操作系统&#xff0c;再讲解一些Linux的基本指令。 一、操作系统的简单了解 1、什么是操作系统&#xff08;Operating System&#xff0c;简称OS&#xff09;&#xff1f; OS是一款做软硬件管理的软件。软硬件的体系结构图&#xff1a; 硬件&…

探索高级聚类技术:使用LLM进行客户细分

在数据科学领域&#xff0c;客户细分是理解和分析客户群体的重要步骤。最近&#xff0c;我发现了一个名为“Clustering with LLM”的GitHub仓库&#xff0c;它由Damian Gil Gonzalez创建&#xff0c;专门针对这一领域提供了一些先进的聚类技术。在这篇文章中&#xff0c;我将概…

「 网络安全常用术语解读 」SBOM主流格式SWID详解

国际标准化组织&#xff08;ISO&#xff09;和国际电工委员会&#xff08;International Electrotechnical Commission&#xff0c;IEC&#xff09;发布了ISO/IEC 19770-2软件标识&#xff08;Software Identification&#xff0c;SWID&#xff09;标签标准&#xff0c;该标准定…

Qt模型视图代理之MVD(模型-视图-代理)概念的简单介绍

往期回顾 Qt绘图与图形视图之Graphics View坐标系的简单介绍-CSDN博客 Qt绘图与图形视图之基本图元绘制的简单介绍-CSDN博客 Qt绘图与图形视图之自定义图元实现拖拽、拉伸、旋转功能-CSDN博客 Qt模型视图代理之MVD(模型-视图-代理)概念的简单介绍 一、基本概念 Qt模型视图代理…

前端页面平滑过渡解决方案

一、问题产生 在使用图片作为页面背景时&#xff0c;无法使用transtion进行平滑过渡&#xff0c;直接切换背景又会降低使用体验。 二、解决方式 使用clip-path对背景图片裁剪配合transtion实现平滑过渡的效果 三、效果展示 网址&#xff1a;ljynet.com 四、实现方式 tem…

ICode国际青少年编程竞赛- Python-1级训练场-for循环练习

ICode国际青少年编程竞赛- Python-1级训练场-for循环练习 1、 for i in range(3):Dev.step(4)Dev.turnLeft()2、 for i in range(3):Dev.step(2)Dev.turnRight()Dev.step(2)Dev.turnLeft()3、 for i in range(3):Dev.step(2)Dev.turnRight()Dev.step(2)Dev.turnLeft()4、 for…

32.Docker认识

Docker介绍 Docker是一个快速交付应用&#xff0c;运行应用的技术。 1.可以将程序、依赖、运行环境一起打包为一个镜像&#xff0c;可以迁移到任意Linux操作系统。 2.运行时利用沙箱机制行程隔离容器&#xff0c;各个应用互不干扰。 3.启动、移除都可以通过一行命令完成&am…

算法学习系列(五十五):背包模型(三)

目录 引言一、潜水员二、背包问题求具体方案三、机器分配四、开心的今明五、金明的预算方案 引言 今天介绍的是背包模型&#xff0c;还是以题目的形式来介绍的。主要讲了背包问题求方案&#xff0c;就是由最优方案递推回去即可。还有就是一些比较经典的背包问题&#xff0c;其…

Spring Boot | Spring Security ( SpringBoot安全管理 )、Spring Security中 的 “自定义用户认证“

目录 : Spring Boot 安全管理 &#xff1a;一、Spring Security 介绍二、Spring Security 快速入门2.1 基础环境搭建 :① 创建Spring Boot 项目② 创建 html资源文件③ 编写Web控制层 2.2 开启安全管理效果测试 :④ 添加 spring-boot-starter-security 启动器⑤ 项目启动测试 三…

Windows server2016关闭ie增强

要关闭Windows Server 2016上的IE增强安全配置&#xff0c;请按照以下步骤操作&#xff1a; 打开“服务器管理器”。点击“本地服务器”。在服务器管理器中&#xff0c;找到“IE增强的安全配置”&#xff0c;点击旁边的“启用”&#xff0c;打开“Internet Explorer增强的安全配…

android init进程启动流程

Android系统完整的启动流程 android 系统架构图 init进程的启动流程 init进程启动服务的顺序 bool Service::Start() {// Starting a service removes it from the disabled or reset state and// immediately takes it out of the restarting state if it was in there.flags_…

【Leetcode每日一题】 动态规划 - 简单多状态 dp 问题 - 删除并获得点数(难度⭐⭐)(70)

1. 题目解析 题目链接&#xff1a;740. 删除并获得点数 这个问题的理解其实相当简单&#xff0c;只需看一下示例&#xff0c;基本就能明白其含义了。 2.算法原理 问题分析 本题是「打家劫舍」问题的变种&#xff0c;但核心逻辑依然保持一致。题目要求从给定的数组nums中选择…

【 书生·浦语大模型实战营】作业(七):大模型实战评测

【 书生浦语大模型实战营】作业&#xff08;七&#xff09;&#xff1a;大模型实战评测 &#x1f389;AI学习星球推荐&#xff1a; GoAI的学习社区 知识星球是一个致力于提供《机器学习 | 深度学习 | CV | NLP | 大模型 | 多模态 | AIGC 》各个最新AI方向综述、论文等成体系的学…

「2024年」前端开发常用工具函数总结 TypeScript

前言 在前端开发中&#xff0c;工具函数是提高代码复用率、保持代码整洁和增加开发效率的关键。使用 TypeScript 编写工具函数不仅可以帮助开发者捕捉到更多的类型错误&#xff0c;还可以提供更清晰的代码注释和更智能的代码补全。下面是一些在 TypeScript 中常用的前端开发工…

【iOS】方法交换(Method Swizzling)

文章目录 前言一、原理与注意用法注意要点Method Swizzing涉及的相关API 二、应用场景与实践1.统计VC加载次数并打印2.防止UI控件短时间多次激活事件3.防崩溃处理&#xff1a;数组越界问题4.防KVO崩溃 总结 前言 上文讲到了iOS的消息发送机制&#xff0c;在消息机制中我们了解…

【打工日常】云原生之使用docker部署Web在线流程图软件

一、drawio介绍 1.drawio简介 draw.io是一款免费、开源、高质量的WEB在线流程图软件&#xff0c;无需注册登录&#xff0c;支持多种图表类型和元素&#xff0c;可在线编辑和导出。它是一个可配置的图表/白板可视化应用程序。该应用程序的设计主要是按原样使用。draw.io不适合作…

AI诗歌创作

诗歌作为一种文学形式&#xff0c;能够通过优美的语言和深刻的意境表达情感和思想&#xff0c;触动人心&#xff0c;引发共鸣。然而&#xff0c;如今随着生活节奏的加快和人们对实用性的追求&#xff0c;写诗这一传统艺术渐渐被人们所忽略。幸运的是&#xff0c;随着人工智能技…

【C++】双指针算法:四数之和

1.题目 2.算法思路 这道题目十分困难&#xff0c;在leetcode上的通过率只有36%&#xff0c;大家要做好心理准备。 在做个题目前强烈建议大家先看看我的上一篇博客&#xff1a;有效三角形个数&#xff0c;看完之后再去leetcode上写一写三数之和&#xff0c;搞懂那两个题目之后…

基于STM32的最小系统电路设计(STM32F103C8T6为例)

前言&#xff1a;本篇博客为嵌入式硬件领域的文章&#xff0c;对 STM32 的最小系统电路设计进行教学。本篇博客以嘉立创 EDA&#xff08;标准版&#xff09;进行绘制 STM32F103C8T6 的最小系统电路 PCB 板&#xff0c;STM32 的最小系统通常包括&#xff1a;微控制器、时钟电路、…