C++ 红黑树:从规则到实现,手把手带你写一棵红黑树

news2026/5/5 20:49:00
红黑树是二叉搜索树家族中重要的一员在 C STL 的map和set底层、Linux 内核的调度器、Java 的TreeMap等地方都能看到它的身影。它通过一套精妙的颜色规则在频繁的插入删除中维持着近似平衡既保证了O(log N)的时间复杂度又比 AVL 树拥有更少的旋转次数。一、什么是红黑树红黑树本质上是一棵二叉搜索树但它的每个结点都增加了一个颜色属性只能是红色或黑色。通过下面四条严格的规则红黑树能够保证没有任何一条从根到叶子的路径会比其他路径长出 2 倍从而实现近似平衡。1.1 红黑树的四条铁律结点非红即黑— 每个结点的颜色要么是红色要么是黑色。根结点必为黑色— 树的根结点始终是黑色的。不连续红色— 如果一个结点是红色的那么它的两个孩子都必须是黑色的。也就是说任意一条从根到叶子的路径上不会出现连续的两个红色结点。黑高相同— 对于任意一个结点从它到其所有后代叶子结点NIL 或 NULL 结点的简单路径上黑色结点的数量必须相同。补充在一些经典教材如《算法导论》中会把叶子结点NIL也视为外部结点并强制为黑色这主要是为了让“路径”的定义更加一致。在实际编码中我们通常用 NULL 作为结束标志并假设它也符合黑色规则不影响平衡的判断。1.2 为什么最长路径不会超过最短路径的 2 倍这是红黑树最核心的平衡保证。我们可以从下图中的极端情况来理解根据规则 4每条路径上的黑色结点数量相同记作bhblack height。根据规则 2 和规则 3红色结点不能连续出现因此路径中最多的红色结点就是和黑色结点交替排列即最长路径由“黑—红—黑—红……”组成长度最多为2 * bh。最短路径则全是黑色结点长度为bh。因此任意一条路径长度h满足bh h 2 * bh。这就保证了整棵树的高度始终被控制在对数级别从而保证了增删查改的时间复杂度都是O(log N)。1.3 红黑树 vs AVL 树AVL 树通过记录每个结点的平衡因子左右子树高度差不超过 1来严格控制平衡因此查询性能非常极致但插入和删除时可能需要更多的旋转来恢复平衡。红黑树的设计更“宽容”一些它不追求绝对平衡而是保证最长路径不超过最短路径的 2 倍。这使得红黑树在插入相同数量结点时旋转次数通常比 AVL 树少也因此更适合插入、删除操作非常频繁的场景。二、红黑树的结构定义在代码实现中我们采用 key-value 结构的泛型模板同时为每个结点增加颜色枚举以及指向父亲的_parent指针方便后续调整。// 颜色枚举 enum Colour { RED, BLACK }; // 红黑树结点 templateclass K, class V struct RBTreeNode { pairK, V _kv; RBTreeNodeK, V* _left; RBTreeNodeK, V* _right; RBTreeNodeK, V* _parent; Colour _col; RBTreeNode(const pairK, V kv) : _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED) { } }; // 红黑树类 templateclass K, class V class RBTree { typedef RBTreeNodeK, V Node; public: // 插入、查找、验证等接口 bool Insert(const pairK, V kv); Node* Find(const K key); bool IsBalance(); private: Node* _root nullptr; // 旋转函数与 AVL 树相同只是不更新平衡因子 void RotateL(Node* parent); void RotateR(Node* parent); // 验证辅助函数 bool Check(Node* root, int blackNum, const int refNum); };三、红黑树的插入 —— 核心难点插入操作可以概括为以下几步按照二叉搜索树的规则将新结点插入到正确位置。新结点默认染成红色。这是因为如果是黑色一定会破坏规则 4改变路径上的黑高维护起来代价巨大而插入红色结点只有可能破坏规则 3连续红色相对更容易修正。如果父亲结点是黑色直接结束不需要任何调整。如果父亲结点是红色违反规则 3则需要根据“叔叔结点”即父结点兄弟的颜色和状态分三种情况处理。我们约定c 当前结点curp 父结点g 祖父结点u 叔叔结点。3.1 情况一叔叔存在且为红色 —— 变色就能解决当p红、u红、g黑时我们只需将p和u染黑将g染红将当前处理结点c移动到g继续往上检查。理解p和u变黑相当于在各自子树增加一个黑色结点g变红相当于维持原路径黑高不变。但g变红后可能与更上层的红结点冲突因此需要循环向上更新。如果最后g是根我们再强行把它染回黑色。无论p位于g的左边还是右边c是p的左还是右处理方式完全一样只涉及变色不需要旋转。// 情况一叔叔存在且为红 if (uncle uncle-_col RED) { parent-_col uncle-_col BLACK; grandfather-_col RED; // 继续向上调整 cur grandfather; parent cur-_parent; }3.2 情况二 情况三叔叔不存在或为黑色 —— 旋转变色若u不存在或颜色为黑单纯的变色已经无法解决连续红色问题这时候必须借助旋转。根据p和c的相对位置又细分为单旋和双旋两种3.2.1 单旋场景直线型p是g的左孩子c是p的左孩子→ 对g进行右单旋p是g的右孩子c是p的右孩子→ 对g进行左单旋旋转完毕后将p染黑、g染红。此时p成为新子树的根整体黑高不变且解决了连续红色问题不需要再向上迭代。// 情况二单旋p 为 g 的左c 为 p 的左 if (cur parent-_left) { RotateR(grandfather); parent-_col BLACK; grandfather-_col RED; } // p 为 g 的右c 为 p 的右 if (cur parent-_right) { RotateL(grandfather); parent-_col BLACK; grandfather-_col RED; }3.2.2 双旋场景折线型p是g的左孩子c是p的右孩子→ 先对p左单旋再对g右单旋p是g的右孩子c是p的左孩子→ 先对p右单旋再对g左单旋旋转后将c染黑、g染红。此时c变成了新子树的根同样黑高不变且不需要继续向上调整。// 情况三双旋p 为 g 的左c 为 p 的右 else { RotateL(parent); RotateR(grandfather); cur-_col BLACK; grandfather-_col RED; } // p 为 g 的右c 为 p 的左 else { RotateR(parent); RotateL(grandfather); cur-_col BLACK; grandfather-_col RED; }3.3 插入操作完整代码结合以上所有情况插入函数的伪代码框架如下bool Insert(const pairK, V kv) { // 1. 空树新建黑结点作为根 if (_root nullptr) { _root new Node(kv); _root-_col BLACK; return true; } // 2. 二叉搜索树查找插入位置 Node* parent nullptr; Node* cur _root; while (cur) { parent cur; if (kv.first cur-_kv.first) cur cur-_left; else if (kv.first cur-_kv.first) cur cur-_right; else return false; // 已存在 } // 3. 新建红色结点挂在父结点下 cur new Node(kv); cur-_col RED; if (kv.first parent-_kv.first) parent-_left cur; else parent-_right cur; cur-_parent parent; // 4. 调整红黑树 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); RotateR(grandfather); cur-_col BLACK; grandfather-_col RED; } break; // 旋转后结构稳定可退出 } } else { // 对称情况parent 是祖父的右孩子 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) { RotateL(grandfather); parent-_col BLACK; grandfather-_col RED; } else { RotateR(parent); RotateL(grandfather); cur-_col BLACK; grandfather-_col RED; } break; } } } // 5. 强制根为黑 _root-_col BLACK; return true; }旋转函数与 AVL 树完全一致只需要改变指针指向即可这里不再赘述。3.4 为什么“旋转变色”后就可以直接退出因为经过单旋或双旋后新的子树根结点被染黑的那个代替了原来的g它的颜色一定是黑色。这样一来新根与上层的颜色连接断然不会再出现“连续红色”整棵树的平衡已经恢复所以可以break不再继续向上调整。四、红黑树的查找查找操作完全沿用二叉搜索树的特性复杂度O(log N)。Node* Find(const K key) { Node* cur _root; while (cur) { if (key cur-_kv.first) cur cur-_left; else if (key cur-_kv.first) cur cur-_right; else return cur; } return nullptr; }五、红黑树的验证 —— 你的树真的“红黑”吗写完插入后我们需要一套可靠的验证工具而不是凭感觉判断。直接套用四条规则颜色只能为红或黑 → 枚举保证了这一点。根是黑色。不能有连续红色结点 → 可以用前序遍历检查反向检查父亲颜色更方便若当前结点为红且父亲也为红则违规。每条路径黑高相同 → 先通过最左边一条路径统计出一个参考黑高refNum然后在前序遍历每条路径时累计黑色结点数走到空时对比。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 IsBalance() { 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); }只要IsBalance()返回true就意味着我们的红黑树完全遵守了所有规则平衡性自然就得到了保证。六、红黑树的删除了解红黑树的删除比插入更加复杂涉及更多颜色的互换、兄弟结点的多重判断以及可能的二次调整。本文暂不作深入展开感兴趣的同学可以阅读《算法导论》或《STL 源码剖析》中的相关章节。七、总结红黑树通过简单的颜色规则以“不连续红”“黑高相等”为约束保证树的高度始终在log N2log N之间从而获得稳定的O(log N)增删查改性能。它的实现核心在插入调整叔叔红色只变色向上迭代叔叔黑色/不存在 直线单旋 变色调整结束叔叔黑色/不存在 折线双旋 变色调整结束。与 AVL 树相比红黑树的平衡条件更宽松旋转次数更少特别适合写多读多或频繁插入删除的场景。掌握红黑树不仅加深了对自平衡搜索树的理解更是窥见了许多工业级数据结构的底层设计哲学。如果你觉得这篇文章对你有帮助欢迎点赞、收藏也欢迎在评论区交流你的理解与困惑我们一起进步

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

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

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…