【手撕红黑树】

news/2024/10/18 9:23:43/

前言

相信很多人初学者听到了红黑树后心中不免有些心慌,那你看到了这篇文章后相信会有所收获,我其实刚开始也是对红黑树抱着一种害怕甚至是恐惧,但是在老师的帮助下也终于慢慢的不在恐惧了,你想知道为什么的话就继续往下看吧。(注意本篇博客只讲解了红黑树的插入,没有讲解红黑树的删除,删除比插入还要难一些,为了更好的阅读体验,就不再讲解了)


1 红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

如果说AVL树是大佬设计的话,那么红黑树就是大佬中的大佬设计出来的,为什么这么说呢,我们接下来慢慢看。


2 红黑树的性质

  • 每个结点不是红色就是黑色
  • 根节点是黑色的
  • 如果一个节点是红色的,则它的两个孩子结点是黑色的
  • 对于每个结点,从该结点到其所有后代叶结点的简单路径上均包含相同数目的黑色结点
  • 每个叶子结点都是黑色的 (此处的叶子结点指的是空结点)

我们先不要看最后一条性质,其他性质中比较重要的就是性质三和性质四,我们可以用自己的话来解读性质三和性质四:
性质三的意思是没有连续红色结点
性质四的意思是每条路径下的黑色结点数量是相等的

大家思考一下,为什么满足了上面性质就能够保证红黑树中最长路径中节点个数不会超过最短路径节点个数的两倍

我们可以从极限的条件下来判断:
最短路径是全黑,最长路径是红黑相间,由于要满足性质三和性质四所以最长路径除以最短路径最大也不会超过二倍。

在这里插入图片描述
我们再来看看最后一个性质,有些教科书上可能会有NIL结点的定义,并且把颜色定义为黑色,注意这里的NIL结点并不是一个真正有效的节点,而是一个空结点。通过每条空结点来标识每一条路径,如在上图中就存在着11条路径。

通过红黑树的性质我们也不难发现,其实红黑树的平衡并没有AVL树那么严格,因为红黑树只需要保证最长路径的结点个数不会超过最短路径节点个数两倍即可,但是AVL树要求着所有子树高度差绝对值不超过1。这就导致了红黑树的旋转条件是比AVL树更加苛刻的,也就是在同等条件下红黑树旋转次数是有较大机率低于AVL树的,那么红黑树的性能肯定是比AVL要好上一些的(旋转是有代价的),如果还没有了解过旋转,建议先看看博主的上一篇博客:
[AVL树的旋转]


3 红黑树的模拟实现

3.1 节点的定义

这个很简单,我们已经讲解过很多次了:

#pragma onceenum Corlor
{RED,BLACK
};template<class K, class V>
struct RBTreeNode
{RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;pair<K, V> _kv;Corlor _corlor;RBTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _corlor(RED){}
};template<class K, class V>
class RBTree
{typedef RBTreeNode<K, V> Node;
private:Node* _root = nullptr;
};

这里问题就来了,新节点给的颜色是给红色还是给黑色呢?
给红色的话就可能违背性质三,给黑色结点就违背性质四。如果是你你想违背哪个性质?
这里给新节点的默认颜色为红色比较好,为什么呢?
给出红色结点的话,我们可能就只需要调整本路径下结点颜色,但是给黑色结点的话其他路径黑色结点就不相等了,调整的代价肯定更大。所以新节点的默认颜色给红色是比较合理的。

在这里插入图片描述我们观察上图,如果我们在11 或者15 下插入新节点,那么这就太好了,不需要进一步调整,插入后还是一颗红黑树,但是我们想要在6 22 27下面插入新节点的话,就要调整了,具体怎样调整我们下面会详细讲解。

3.2 分类

约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
红黑树调整分类的关键就是看叔叔

3.2.1 情况一

cur为红,p为红,g为黑,u存在且为红

在这里插入图片描述
老规矩,我们这里画的仍然是抽象图,表示无数种情况,但是他们都可以用同一种方法来解决。
此时我们只需要将p和u置黑,将g置红,然后接着往上调整
为什么要继续往上调整呢?通过观察我们可以很容易分析出问题所在,这样一次调整了后各个路径上黑色结点数目并没有发生改变,但是有可能g结点的父亲结点是红色的,而导致又出现了连续红色结点(注意,调整了后有可能再次调整使用的方法不在是第一种情况)
在这里插入图片描述继续向上调整的话,直到不满足连续红色结点或者已经调整到了根节点。

3.2.2 情况二

cur为红,p为红,g为黑,u不存在/u存在且为黑

在这里插入图片描述
我们先分析第一种情况:u不存在。
如果u不存在的话,那么cur一定是新增。此时光变色已经无法解决问题了,因为此时已经不满足最长路径中节点个数不会超过最短路径节点个数的两倍,就需要旋转处理:
在这里插入图片描述这里处理方式是:右单旋+p变黑,g变红。
也有可能p和cur都在右边且在一条直线上,所以处理方式可能是:左单旋+p变黑,g变红。
总结本次调整方法为:单旋+p变黑,g变红

同理,当u存在且为黑的时候仍然是同种处理方式:
在这里插入图片描述旋转变色完以后就不用再向上更新了

3.2.3 情况三

cur为红,p为红,g为黑,u不存在/u存在且为黑

等等,这种方式不就是第二种方式吗?
别着急,我们先来看看图:
在这里插入图片描述
从图片中我们不难发现,g p cur 三者并没有在一条直线,而是一种折线关系,这种情况我们只是单旋就处理不了了,在这张图中我们先以p点进行左单旋,在以g点进行右单旋上。
(注意,我们在b和c下面插入结点导致引起上面这种关系的都可以用这种方式处理)
在这里插入图片描述
在这里插入图片描述当然,不只是这一种情况,还可能是反着来,那么处理方式就可能是:
以p右单旋+以g左单旋+cur变黑,g变红。
所以这次情况处理方式是:双旋+cur变黑,g变红
旋转变色完以后就不用再向上更新了

3.3 代码实现

bool insert(const pair<K, V>& kv){if (_root == nullptr){_root = new Node(kv);_root->_corlor = BLACK;return true;}Node* cur = _root;Node* parent = nullptr;while (cur){if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else{return false;}}cur = new Node(kv);if (parent->_kv.first > kv.first){parent->_left = cur;cur->_parent = parent;}else{parent->_right = cur;cur->_parent = parent;}while (parent && parent->_corlor == RED){Node* grand = parent->_parent;assert(grand);assert(grand->_corlor == BLACK);if (grand->_left == parent){Node* uncle = grand->_right;//情况一:uncle存在且为红if (uncle && uncle->_corlor == RED){uncle->_corlor = parent->_corlor = BLACK;grand->_corlor = RED;//继续往上处理cur = grand;parent = cur->_parent;}else{//情况二和三://1 :parent->_left==cur(右单旋+变色)//      g//   p     u//cif (parent->_left == cur){RotateR(grand);parent->_corlor = BLACK;grand->_corlor = RED;}else{//2 :parent->_right==cur(左单旋+右单旋+变色)//      g//   p     u//      cRotateL(parent);RotateR(grand);cur->_corlor = BLACK;grand->_corlor = RED;}//此时已经旋转变色完成,可以break出去break;}}else//grand->_right=parent{Node* uncle = grand->_left;//情况一:uncle存在且为红if (uncle && uncle->_corlor == RED){uncle->_corlor = parent->_corlor = BLACK;grand->_corlor = RED;//继续往上处理cur = grand;parent = cur->_parent;}else{//情况二和三://1 :parent->_right==cur(左单旋+变色)//      g//   u     p//            cif (parent->_right == cur){RotateL(grand);parent->_corlor = BLACK;grand->_corlor = RED;}else{//2 :parent->_left==cur(右单旋+左单旋+变色)//      g//   u     p//      cRotateR(parent);RotateL(grand);cur->_corlor = BLACK;grand->_corlor = RED;}//此时已经旋转变色完成,可以break出去break;}}}_root->_corlor = BLACK;return true;}

3.4 红黑树的验证

验证的话我们要从红黑树的性质开始着手,只要满足了红黑树的几个性质自然就没啥问题.

最主要的验证是性质三和性质四:

//bool prevCheck(Node* root, int blackCnt, int& benchmark)bool prevCheck(Node* root, int blackCnt, int benchmark){if (root == nullptr){/*if (benchmark == 0){benchmark=blackCnt;return true;}*/if (benchmark != blackCnt){cout << "每条路径上的黑色结点不相等" << endl;return false;}else{return true;}}if (root->_corlor == BLACK){++blackCnt;}if (root->_corlor == RED && root->_parent->_corlor == RED){cout << "有连续的红色结点" << endl;return false;}return prevCheck(root->_left, blackCnt, benchmark)&& prevCheck(root->_right, blackCnt, benchmark);}bool isbalance(){if (_root && _root->_corlor == RED){cout << "根节点不是黑色" << endl;return false;}int benchMark = 0;Node* cur = _root;while (cur){if (cur->_corlor == BLACK)++benchMark;cur = cur->_left;}int blackCnt = 0;return prevCheck(_root, blackCnt, benchMark);}

处理方式有很多种,像每条路径下的黑色节点我们可以一次性先算出来然后传参数即可,也可以不算出来,传参数引用来修改。具体方式大家可以自行选择。
大家测试时最好多用几组随机数测测。


5 总结

大家看到了这里相信也对红黑树有了一个谱,其实说难吧,感觉还没有刚学AVL的旋转难,关键是如何把图画好,跟着图一步一步的来,大概率是不会出错的。如果该文对你有帮助的话能不能一键三联支持博主呢


http://www.ppmy.cn/news/69786.html

相关文章

[Python物联网]Python基础知识和语法--Python模块和包--Python快速上手开发物联网上位机程序

目录 一、前言 二、模块的导入 三、模块的定义 四、包的定义 五、包的相对导入 六、示例代码 七、总结 一、前言 在 Python 中&#xff0c;模块是指一个包含 Python 代码的文件。而包则是指一个包含多个模块的目录。模块和包是 Python 代码复用的基本组织方式。在本文中…

说说谷歌Chrome浏览器无痕浏览器窗口

当您启用无痕浏览后&#xff0c;设备的其他用户将不会看到您的历史记录。 Chrome 不会保存您的浏览记录或您在表单中填写的信息。当您浏览时&#xff0c;Chrome 会记住相应的 Cookie 和网站数据&#xff0c;但当您退出无痕模式时&#xff0c;Chrome 会删除这些数据。您可在打开…

Redis进阶

主要内容 Redis持久化Redis主从Redis哨兵Redis分片集群 Redis持久化 Redis有两种持久化的方案: RDB持久化AOF持久化 1. RDB持久化 RDB全称Redis Database Backup file&#xff08;Redis数据备份文件&#xff09;&#xff0c;也被叫做Redis数据快照。简单来说就是把内存中的所…

Android Framework——Binder 监控方案

作者&#xff1a;低性能JsonCodec 在 Android 应用开发中&#xff0c;Binder 可以说是使用最为普遍的 IPC 机制了。我们考虑监控 Binder 这一 IPC 机制&#xff0c;一般是出于以下两个目的&#xff1a; 卡顿优化&#xff1a;IPC 流程完整链路较长&#xff0c;且依赖于其他进程…

【Linux系统】Linux进程信号详解

Linux进程信号 0 引言1 认识信号1.1 什么是信号1.2 发送信号的本质1.3 信号的处理 2 信号的产生2.1 键盘产生2.2 调用系统函数向进程发送信号2.3 由软件条件产生信号2.4 硬件异常产生信号 3 信号的保存4 信号的处理5 总结 0 引言 本篇文章会从Linux信号的产生到信号的保存&…

信息熵、交叉熵、KL散度公式的简单理解

整理&#xff1a;我不爱机器学习 1 信息量 信息量是对信息的度量&#xff0c;就跟时间的度量是秒一样&#xff0c;考虑一个离散的随机变量 x 的时候&#xff0c;当观察到的这个变量的一个具体值的时候&#xff0c;我们接收到了多少信息呢&#xff1f; 例如听到太阳从东方升起…

免费优质的网页设计素材网站推荐

找到一个免费优质的网页设计素材网站并不容易。 有些网站要么需要网站的会员&#xff0c;要么设计素材质量差。本文整理总结了10个免费的网页设计材料网站&#xff0c;希望给大家带来一些帮助。 即时设计资源社区 即时设计资源社区是由在线协同设计软件即时设计推出的设计社…

【C++】函数高级

目录 &#x1f34a;一.函数的默认参数&#x1f34a; 1.默认参数的性质 2.函数默认参数的注意事项 &#x1f34e;二.函数的占位参数&#x1f34e; &#x1f34f;三.函数的重载 &#x1f34f; 1.重载的性质和条件 &#xff08;1&#xff09;修改参数的个数 &#xff…