当前位置: 首页 > article >正文

C++修炼:红黑树的模拟实现

        Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客:<但凡.

我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C++修炼之路》

欢迎点赞,关注!

       

目录

1、红黑树的概念

        1.1、什么是红黑树

        1.2、红黑树和AVL树的对比

        1.3、红黑树复杂度

2、红黑树模拟实现

        2.1、红黑树的结构

2.1、红黑树的插入

        2.1.1、插入情况分析

        一、叔叔存在且为红

        二、叔叔存在且为黑或叔叔不存在,并且新增节点是父亲的左

        三、叔叔存在且为黑或叔叔不存在,并且新增节点是父亲的右

         2.2.2、插入代码实现

2.3、查找

2.4、检查是否为二叉树

2.5、其他

3、红黑树的应用


        因为map和set的底层使用红黑树实现的,所以我们先讲解一下红黑树,并模拟实现一下红黑树,为下一期的map和set封装做铺垫。

1、红黑树的概念

        1.1、什么是红黑树

        红黑树是一颗二叉搜索树,他的每个节点都有一个颜色,颜色只能是红色或黑色。和AVL树一样,他也是依靠着几条规则来尽可能保证平衡,保证搜索效率的。我们开门见山直接介绍一下这几条规则:

        (1)每个节点不是红色就是黑色(上面说过了)。

        (2)根节点是黑色的。

        (3)不能出现两个连续的红色节点(如果父亲是红色,孩子一定不是红色)。

        (4)从根节点到任意一个NULL空节点的路线上所含有的黑色节点数量相等。

        当然由于红黑树是一颗二叉搜索树,所以他也具有二叉搜索树的特点。

        基于以上四点规则,红黑树能保证从根节点到NULL节点的最长路径不会超过最短路径的两倍。

        我先给大家透露一点,红黑树也是通过各种旋转来保证平衡,只不过是他不再依靠平衡因子了,而是依靠颜色。红黑树的平衡性没有AVL那么极致,这意味着插入同样的节点构建一棵红黑树需要的旋转次数更少。

        一棵红黑树:

         1.2、红黑树和AVL树的对比

         有些区别我们说过了,我们说一下为什么红黑树的查找比AVL树稍慢。

        其实核心原因还是因为红黑树的树高可能会比AVL树高,因为他不是严格控制平衡。由于搜索(查找)只能是从头到尾遍历这颗树,如果树高更高的话,不可避免的搜索路径更长,效率更低。

        对于100万个节点的树,AVL树高度约20层,而 红黑树高度最多约四十层。

       1.3、红黑树复杂度

         空间复杂度:O(N),时间复杂度:插入,查找,删除都是O(logN)。

2、红黑树模拟实现

         又到了经典的模拟实现环节哈哈。

        2.1、红黑树的结构

enum Colour
{RED,BLACK
};
template<class K,class V>
struct RBTreeNode
{pair<K, V> _kv;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;Colour _col;RBTreeNode(const pair<K,V>& kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_col(RED){}
};
template<class K,class V>
class RETree
{typedef RBTreeNode<K, V> Node;
public:private:Node* _root = nullptr;
};

         和AVL树的节点类似,只不过是把平衡因子换成了颜色标记。另外就是我们这里实现的红黑树默认是key_value类型的,为的是方便后面map的封装(没错,set也是用的key_value版本的红黑树)。但是所有的比较都是按照key来比较的。

2.1、红黑树的插入

         红黑树的插入可以说是最最核心的部分了,所以说我们先分析一下再模拟实现。

        2.1.1、插入情况分析

        插入情况有三种,我们这三种情况的区分是按照叔叔(父亲的兄弟节点)的颜色和是否存在,以及父亲节点和新增节点的位置关系进行区分的。其实严格来讲有六种,因为父亲有可能是爷爷的左,也可能是爷爷的右,但是前三种和后三种其实是非常类似的,只是方向换了一下

        首先我要说明一点,我们所有的新增节点默认都是红色的,因为对于新插入的节点,如果是红色节点他破坏的是规则三,如果插入的是黑色节点,破坏的是规则四,很显然规则四比规则三更难调整。

        一、叔叔存在且为红

        这是最简单的一种情况,只变色不旋转。

  

         当父亲是爷爷的左的时候情况一样,就是单纯变色,不画图了。

         二、叔叔存在且为黑或叔叔不存在,并且新增节点是父亲的左

        当父亲节点是爷爷节点的左,这种情况是单旋加变色:

  

         (我拿最简单的情况来做说明了)

          在上面这种情况下,我们先对爷爷就行右旋,然后再进行变色,把父亲变成黑色,爷爷变成红色。

        和他类似的,当父亲节点是爷爷的右子树,进行如下操作:

  

        三、叔叔存在且为黑或叔叔不存在,并且新增节点是父亲的右

        首先第一种情况是父亲是爷爷的左

        如果父亲是爷爷的右: 

  

        写到这里大家也应该发现了,我们旋转的情况和AVL树是一样的,如果是单纯的一边高,我们就单旋,如果是左子树的右子树高或者右子树的左子树高,也就是不是单纯一边高的,就进行双旋。除此之外的不同就是我们把平衡因子的调整换成了颜色的调整。

        好了以上就是红黑树插入调整的大概几种情形,当然还有叔叔存在且为黑的情况我没画图,因为在代码实现上,叔叔存在且为黑和叔叔不存在属于一种情况,所以说就不画图了(其实是我很懒)。

        接下来我们梳理一下这几种情况:

一、如果父亲是爷爷的左节点

        (1)如果叔叔存在且为红

                       单纯调色

        (2)如果叔叔不存在或叔叔存在且为黑

                        1、如果新增节点是父亲的左

                                单旋加变色。

                        2、如果新增节点是父亲的右

                                双旋加变色。

二、如果父亲是爷爷的右节点

        (1)如果叔叔存在且为红

                       单纯调色

        (2)如果叔叔不存在或叔叔存在且为黑

                        1、如果新增节点是父亲的右

                                单旋加变色。

                        2、如果新增节点是父亲的左

                                双旋加变色。

         2.2.2、插入代码实现

bool Insert(const pair<K, V>& kv)
{//如果树为空就把这个节点作为根if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;//默认为黑return true;}//查找插入位置Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else{return false;//不允许重复值}}//新插入红色节点cur = new Node(kv);cur->_col = RED;//讲解//先插入再调整if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;while (parent && parent->_col == RED){Node* grandfather = parent->_parent;if (parent == grandfather->_left){Node* uncle = grandfather->_right;//叔叔存在且为红if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfather->_col = RED;//循环向上处理cur = grandfather;//此时爷爷相当于是新增红节点parent = cur->_parent;}else//叔叔不存在或叔叔存在且为黑{if (cur == parent->_left){RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{RotateL(parent);RatateR(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 (cur == parent->_right){RatateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}//都更改完后需要调整根节点的颜色,因为根节点颜色可能被改变了_root->_col = BLACK;
}

        如果父亲存在并且父亲的颜色为红,说明此时有两个连续的红,我们就进行调整。这里说一点为什么循环条件中没有判断爷爷是否存在。首先我们设想爷爷不存在的场景,即父亲是根节点。此时爷爷的确不存在,但是由于父亲的颜色一定是黑色的(在此之前我们无法破坏这一点规则),所以说循环一定进不去。也就不必判断爷爷是否存在了。

        还有一个特别特别需要注意的点,如果我们是旋转调整(即第二三中情况),在调整完之后直接退出循环,因为此时我们的爷爷是黑色的,而在第一种情况的变色下爷爷是红色的,如果爷爷是黑色不会破坏规则四,就不要继续循环了。

        以下是两种旋转的代码,和之前AVL树的旋转完全一样,不作解释了直接放在这里:

void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;Node* ppnode = parent->_parent;subL->_right = parent;parent->_parent = subL;if (parent == _root){_root = subL;subL->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = subL;}else{ppnode->_right = subL;}subL->_parent = ppnode;}}void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;Node* ppnode = parent->_parent;subR->_left = parent;parent->_parent = subR;if (parent == _root){_root = subR;subR->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = subR;}else{ppnode->_right = subR;}subR->_parent = ppnode;}}

2.3、查找

        和之前树的查找一样,不多说了。

Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_kv.first < key){cur = cur->_right;}else if (cur->_kv.first > key){cur = cur->_left;}else{return cur;}}return nullptr;}

 2.4、检查是否为二叉树

        这个检查不是重点,不做讲解。

bool Check(Node* root, int blackNum, const int refNum){if (root == nullptr){// 前序遍历走到空时,意味着一条路径走完了if (blackNum != refNum){cout << "存在黑色结点的数量不相等的路径" << endl;return false;}return true;}// 检查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲就方便多了if (root->_col == RED && root->_parent->_col == RED){cout << root->_kv.first << "存在连续的红色结点" << endl;return false;}if (root->_col == BLACK){++blackNum;}return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);}bool IsBalanceTree(){if (_root == nullptr)return true;if (_root->_col == RED)return false;// 参考值int refNum = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK){++refNum;}cur = cur->_left;}return Check(_root, 0, refNum);}

 2.5、其他

        以下是二叉树节点数,二叉树高度,二叉树先序遍历的代码,很早之前在数据结构篇的二叉树部分就说过了,不做解析。

int _Size(Node* root){return root == nullptr ? 0 :_Size(root->_left) + _Size(root->_right) + 1;}int _Height(Node* root){if (root == nullptr)return 0;int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;}void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}

3、红黑树的应用

  1. C++ STL中的map和set:通常使用红黑树实现(下一期就是map和set模拟实现)

  2. Linux内核:进程调度、内存管理等

  3. 数据库系统:索引结构

  4. 实时系统:需要保证最坏情况下性能的场景

        好了,今天的内容就分享到这,我们下期再见!

 

相关文章:

C++修炼:红黑树的模拟实现

Hello大家好&#xff01;很高兴我们又见面啦&#xff01;给生活添点passion&#xff0c;开始今天的编程之路&#xff01; 我的博客&#xff1a;<但凡. 我的专栏&#xff1a;《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C修炼之路》 欢迎点赞&#xff0c;关注&am…...

基于Python+YOLO模型的手势识别系统

本项目是一个基于Python、YOLO模型、PyQt5的实时手势识别系统&#xff0c;通过摄像头或导入图片、视频&#xff0c;能够实时识别并分类不同的手势动作。系统采用训练好的深度学习模型进行手势检测和识别&#xff0c;可应用于人机交互、智能控制等多种场景。 1、系统主要功能包…...

自制操作系统day10叠加处理

day10叠加处理 叠加处理&#xff08;harib07b&#xff09; 现在是鼠标的叠加处理&#xff0c;以后还有窗口的叠加处理 涉及图层 最上面小图层是鼠标指针&#xff0c;最下面的一张图层用来存放桌面壁纸。移动图层的方法实现鼠标指针的移动以及窗口的移动。 struct SHEET { u…...

docker初学

加载镜像&#xff1a;docker load -i ubuntu.tar 导出镜像&#xff1a;docker save -o ubuntu1.tar ubuntu 运行&#xff1a; docker run -it --name mu ubuntu /bin/bash ocker run -dit --name mmus docker.1ms.run/library/ubuntu /bin/bash 进入容器&#xff1a;docke…...

## Docker 中 Elasticsearch 启动失败:日志文件权限问题排查与解决

好的&#xff0c;这是一份关于你遇到的 Docker Elasticsearch 启动报错问题的笔记&#xff0c;包含问题描述、我的分析判断以及最终的解决方案&#xff0c;适合用于整理成文章。 Docker 中 Elasticsearch 启动失败&#xff1a;日志文件权限问题排查与解决 在使用 Docker部署 E…...

鸿蒙Flutter实战:23-混合开发详解-3-源码模式引入

引言 在前面的文章混合开发详解-2-Har包模式引入中&#xff0c;我们介绍了如何将 Flutter 模块打包成 Har 包&#xff0c;并引入到原生鸿蒙工程中。本文中&#xff0c;我们将介绍如何通过源码依赖的方式&#xff0c;将 Flutter 模块引入到原生鸿蒙工程中。 创建工作 创建一个…...

leetcode:2469. 温度转换(python3解法,数学相关算法题)

难度&#xff1a;简单 给你一个四舍五入到两位小数的非负浮点数 celsius 来表示温度&#xff0c;以 摄氏度&#xff08;Celsius&#xff09;为单位。 你需要将摄氏度转换为 开氏度&#xff08;Kelvin&#xff09;和 华氏度&#xff08;Fahrenheit&#xff09;&#xff0c;并以数…...

【软件安装】Windows操作系统中安装mongodb数据库和mongo-shell工具

这篇文章&#xff0c;主要介绍Windows操作系统中如何安装mongodb数据库和mongo-shell工具。 目录 一、安装mongodb数据库 1.1、下载mongodb安装包 1.2、添加配置文件 1.3、编写启动脚本&#xff08;可选&#xff09; 1.4、启动服务 二、安装mongo-shell工具 2.1、下载mo…...

跨域问题及其CORS解决方案:gin框架中配置跨域

一、同源策略 浏览器的同源策略&#xff08;Same-Origin Policy&#xff09;要求&#xff1a;只有协议、域名和端口都相同的请求才被视为同源&#xff0c;才允许正常访问。 两个URL在以下三个方面完全相同时称为"同源"&#xff1a; 协议相同&#xff08;如都是http或…...

记共享元素动画导致的内存泄露

最近在给项目的预览图片页增加共享元素动画的时候&#xff0c;发现了LeakCanary一直报内存泄露。 LeakCanary日志信息 ┬─── │ GC Root: Thread object │ ├─ java.lang.Thread instance │ Leaking: NO (the main thread always runs) │ Thread name: main │ …...

Flyweight(享元)设计模式 软考 享元 和 代理属于结构型设计模式

1.目的&#xff1a;运用共享技术有效地支持大量细粒度的对象 Flyweight&#xff08;享元&#xff09;设计模式 是一种结构型设计模式&#xff0c;它的核心目的是通过共享对象来减少内存消耗&#xff0c;特别是在需要大量相似对象的场景中。Flyweight 模式通过将对象的共享细节与…...

Win/Linux安装flash attention2

1.Win 安装Flash_attn &#xff08;1&#xff09;第一步&#xff1a;下载flash_attn-xxx.whl 文件 在 1&#xff09;地址1&#xff1a;HuggingFace 官网 Flash-attn页面 2&#xff09;地址2&#xff1a;Github 地址 下载对应cuda、torch、python版本的whl文件&#xff1b; …...

【原创】ubuntu22.04下载编译AOSP 15

安装依赖的库&#xff0c;顺便把vim 也安装一下 sudo apt-get install vim sudo apt-get install git gnupg flex bison build-essential zip curl zlib1g-dev libc6-dev-i386 x11proto-core-dev libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip font…...

服务器网络配置 netplan一个网口配置两个ip(双ip、辅助ip、别名IP别名)

文章目录 问答 问 # This is the network config written by subiquity network:ethernets:enp125s0f0:dhcp4: noaddresses: [192.168.90.180/24]gateway4: 192.168.90.1nameservers:addresses:- 172.0.0.207- 172.0.0.208enp125s0f1:dhcp4: trueenp125s0f2:dhcp4: trueenp125…...

响应面法(Response Surface Methodology ,RSM)

响应面法是一种结合统计学和数学建模的实验优化技术&#xff0c;通过有限的实验数据&#xff0c;建立输入变量与输出响应之间的数学模型&#xff0c;找到最优操作条件。 1.RSM定义 RSM通过设计实验、拟合数学模型&#xff08;如多项式方程&#xff09;和分析响应曲面&#xff…...

针对面试-java集合篇

1.什么是数组 数组(Array)是一种用连续的内存空间存储相同数据类型数据的线性数据结构。 2.数组下标为什么从0开始 寻址公式是:baseAddressi*dataTyeSize&#xff0c;计算下标的内存地址效率较高 3.查找的时间复杂度 随机(通过下标)查询的时间复杂度是O(1) 查找元素(未知…...

Spring Boot 拦截器:解锁5大实用场景

一、Spring Boot中拦截器是什么 在Spring Boot中&#xff0c;拦截器&#xff08;Interceptor&#xff09;是一种基于AOP&#xff08;面向切面编程&#xff09;思想的组件&#xff0c;用于在请求处理前后插入自定义逻辑&#xff0c;实现权限校验、日志记录、性能监控等非业务功能…...

展锐 Android 15 锁定某个App版本的实现

Android 15 系统锁定Antutu版本的实现方法 在Android系统开发中,有时需要锁定特定应用的版本以确保系统稳定性或测试一致性。本文将介绍如何通过修改Android源码来锁定Antutu跑分软件的版本。 修改概述 这次修改主要涉及以下几个方面: 禁用产品复制文件的检查添加指定版本…...

有两个Python脚本都在虚拟环境下运行,怎么打包成一个系统服务,按照顺序启动?

环境&#xff1a; SEMCP searx.webapp python 问题描述&#xff1a; 有两个python脚本都在虚拟环境下运行&#xff0c;怎么打包成一个系统服务&#xff0c;按照顺序启动&#xff1f; 解决方案&#xff1a; 将这两个 Python 脚本打包成有启动顺序的系统服务&#xff0c;最…...

【Linux cmd】查找进程信息

1、包含 "Test" 关键字的进程 ps -ef | grep Test 显示系统中所有进程的详细信息&#xff0c;包括用户 ID&#xff08;UID&#xff09;、进程 ID&#xff08;PID&#xff09;、父进程 ID&#xff08;PPID&#xff09;、启动时间&#xff08;STIME&#xff09;、终端…...

与网格共舞 - 服务网格的运维与问题排查 (Istio 实例)

与网格共舞 - 服务网格的运维与问题排查 (Istio 实例) 在领略了服务网格(以 Istio 为例)在流量管理、可观测性和安全方面提供的强大能力后,我们自然会思考:如何将这个“神器”请进我们的生产环境,并让它稳定、可靠地运行?这需要我们关注运维层面的实践。 部署与升级:网…...

Python 脚本执行命令的深度探索:方法、示例与最佳实践

在现代软件开发过程中&#xff0c;Python 脚本常常需要与其他工具和命令进行交互&#xff0c;以实现自动化任务、跨工具数据处理等功能。Python 提供了多种方式来执行外部命令&#xff0c;并获取其输出&#xff0c;重定向到文件&#xff0c;而不是直接在终端中显示。这种能力使…...

PotPlayer 4K 本地万能影音播放器

今日分享一款来自吾爱论坛大佬分享的啥都能播的的本地播放器&#xff0c;不管是不管是普通视频、4K超清、蓝光3D&#xff0c;还是冷门格式&#xff0c;它基本都能搞定。而且运行流畅不卡顿&#xff0c;电脑配置低也能靠硬件加速&#xff0c;让你根本停不下来。 自带解码器&…...

2025年电工杯A题第一版本Q1-Q4详细思路求解+代码运行

A题 光伏电站发电功率日前预测问题 问题背景 光伏发电是通过半导体材料的光电效应&#xff0c;将太阳能直接转化为电能的技术。光伏电站是由众多光伏发电单元组成的规模化发电设施。 光伏电站的发电功率主要由光伏板表面接收到的太阳辐射总量决定&#xff0c;不同季节太阳光…...

基于阿里云DashScope API构建智能对话指南

背景 公司想对接AI智能体&#xff0c;用于客服系统&#xff0c;经过调研和实施&#xff0c;觉得DashScope 符合需求。 阿里云推出的DashScope灵积模型服务为开发者提供了便捷高效的大模型接入方案。本文将详细介绍如何基于DashScope API构建一个功能完善的智能对话系统&#x…...

HOW - 基于组件库组件改造成自定义组件基本规范

文章目录 Select 选择器改造1. 明确组件目标2. 定义组件 API3. 合理使用默认值4. 支持类型安全的 options 传递5. 支持 ForwardRef&#xff08;可选&#xff09;6. 封装样式&#xff08;可选&#xff09;7. 使用示例 ...props 位置推荐顺序&#xff1a;最后原因&#xff1a;简要…...

九州未来十三载:开源赋能 智启未来

2012年&#xff0c;九州未来以“开源赋能云边变革”为使命&#xff0c;开启中国开放云边基础架构服务的探索之路。十三载坚守深耕&#xff0c;我们始终以开源为翼&#xff0c;以算力为基&#xff0c;在科技浪潮中砥砺前行&#xff0c;见证并推动着AI时代的算力变革。 坚守初心丨…...

2025年AI搜索引擎发展洞察:技术革新与市场变革

引言&#xff1a;AI搜索的崛起与市场格局重塑 2024-2025年&#xff0c;AI搜索市场迎来了前所未有的变革期。随着DeepSeek-R1等先进大语言模型的推出&#xff0c;传统搜索引擎、AI原生搜索平台以及各类内容平台纷纷加速智能化转型&#xff0c;推动搜索技术从基础信息检索向深度…...

dify调用Streamable HTTP MCP应用

一、概述 上一篇文章&#xff0c;介绍了使用python开发Streamable HTTP MCP应用&#xff0c;链接&#xff1a;https://www.cnblogs.com/xiao987334176/p/18872195 接下来介绍dify如何调用MCP 二、插件 安装插件 需要安装2个插件&#xff0c;分别是&#xff1a;Agent 策略(支持 …...

HCIP实验五

一、实验拓扑图&#xff1a; 二、实验需求分析&#xff1a; 1. PreVal策略&#xff1a;要求确保R4通过R2到达192.168.10.0/24 &#xff0c;需在R4上针对去往该网段路由配置PreVal策略&#xff0c;为经R2的路径赋予更高优先值&#xff0c;影响本地路由表选路。 2. AS Path策略…...