【高阶数据结构】手撕红黑树(超详细版本)

这篇具有很好参考价值的文章主要介绍了【高阶数据结构】手撕红黑树(超详细版本)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

🌈欢迎来到数据结构专栏~~手撕红黑树


  • (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
  • 目前状态:大三非科班啃C++中
  • 🌍博客主页:张小姐的猫~江湖背景
  • 快上车🚘,握好方向盘跟我有一起打天下嘞!
  • 送给自己的一句鸡汤🤔:
  • 🔥真正的大师永远怀着一颗学徒的心
  • 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
  • 🎉🎉欢迎持续关注!
    手撕红黑树,数据结构—流浪计划,数据结构,算法

手撕红黑树,数据结构—流浪计划,数据结构,算法

手撕红黑树,数据结构—流浪计划,数据结构,算法

一. 红黑树的概念😎

红黑树也是一种二叉搜索树,但是每个节点都存储了一个颜色,该颜色可以为黑可以为红,因此也叫红黑树

红黑树和AVL树的区别就是:红黑树是近似平衡,但不是完全平衡,没有和AVL树一样 的通过平衡因子来控制高度差,而是通过每条路径上对红黑节点的控制,来达到确保最长路径长度不超过最短路径长度的 2 倍。

手撕红黑树,数据结构—流浪计划,数据结构,算法

二. 五大特性

  1. 根节点一定为黑色
  2. 每个节点只能是
  3. 节点为红色,则该节点的两个子节点都为黑色(也就是树中没有连续的红色节点
  4. 对于每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点(每条路径的黑色节点相等
  5. 每个叶子结点都是黑色(此处的叶子节点指的是空节点NIL节点)

如果是空树,刚好是空节点为黑色,也符合第一条规则

那么问题来了,仅仅依靠这五大特性是如何确保最长路径长度 <= 最短路径长度的 2 倍

根据红黑树的性质3可以得出,红黑树当中不会出现连续的红色结点,而根据性质4又可以得出,从某一结点到其后代叶子结点的所有路径上包含的黑色结点的数目是相同的

所以我们不妨假设极端场景,最短的路径无非就是全黑的情况了,假设此时有 n 个节点,长度就为 n

手撕红黑树,数据结构—流浪计划,数据结构,算法
而最长路径就是:一红一黑排列的,如果有 n 个黑色节点,那么红色节点数目与黑色相同,则长度为 2n,所以不超过两倍!

三. 节点的定义

cv工程师登场!此处我们还是定义成三叉链,只不过把平衡因子替换成了节点颜色,因为节点的颜色就两种,直接枚举就好了

//枚举颜色
enum  Colour
{
	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;//存储键值对
	Colour _col;//节点颜色

	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
	 	, _col(RED)
	{}
};

此处有个问题:为什么构造结点时,默认将结点的颜色设置为红色?

此处我们知道插入黑色的节点是一定违反了性质4;某条路径的黑色节点一定会增加一个,那为了维护结构,我们岂不是要在其他的路径是不是也要增加一个呢?
但此时如果新增的是一个红色的节点呢;如果根节点为红色,那么又会破坏性质 3 出现了连续的红色节点,但是如果根节点为黑色,就不需要进行调整。 也就是说新增red节点不一定会破坏结构,但新增Black节点就一定会破坏。

手撕红黑树,数据结构—流浪计划,数据结构,算法

四. 红黑树插入

此处插入的前半部分和AVL树的插入一样:

  1. 按二叉搜索树的插入方法,找到待插入位置
  2. 将待插入结点插入到树中
  3. 若插入结点的父结点是红色的,则需要对红黑树进行调整

红黑树的关键就在这第三步中!大有来头

⚡模型

我们先给出一个基本模型:(可以是子树也可以是一棵完整的树)

手撕红黑树,数据结构—流浪计划,数据结构,算法

cur :当前节点
p:parent,父节点
g:grandfather,祖父节点
u:uncle,叔叔节点
a,b,c,d,e:子树

顺便科普一下树的路径是从根节点一路走到空节点(NIL 节点)才算一条路径,而不是走到叶子就停下来了

手撕红黑树,数据结构—流浪计划,数据结构,算法
所以上面这棵树的有效路径有9条,而不是4条

🥑情况一:u 存在且为红

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

手撕红黑树,数据结构—流浪计划,数据结构,算法

首先我们知道红黑树的关键是看叔叔uncle;节点的颜色是固定为黑色的;因为不能有两个相同的红色节点,所以我们开始调整!首先将parent变成黑色;又为了不违反性质4,所以uncle的节点也要变成黑色;同时也要将grandparent节点变红,不要忘了这可能只是一颗子树;为了维持每条路径上黑色节点的数量;祖父必须变红,不然会多出一个黑色节点。

手撕红黑树,数据结构—流浪计划,数据结构,算法

最后不要忘了将祖父当成 cur 节点继续向上调整,直到g是根,最后才将变成黑色!

具体代码:

		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			assert(grandfather);
			assert(grandfather->_col == BLACK);

			//关键看叔叔  ~ 判断叔叔的位置
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				//情况1:uncle存在且为红  + 继续往上处理
				if (uncle && uncle->_col = RED)
				{
					//变色:p和u变黑,g变红
					parent->_col = uncle ->_col = Black;
					grandfather->_col = RED;

					//继续往上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else  //情况2 
				{}
			}
			else  //parent == grandfather->_right
			{
				Node* uncle = grandfather->_left;
				//情况1:uncle存在且为红 + 继续往上处理
				if (uncle&& uncle->_col = RED)
				{
					//变色:p和u变黑,g变红
					parent->_col = uncle->_col = Black;
					grandfather->_col = RED;

					//继续往上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else  //情况2 
				{}
			}	
		}
		_root->_col = BLACK;//不管什么,最后根要变黑
		return true;
	}

🥑情况二:

💥具体情况1️⃣:u不存在

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

手撕红黑树,数据结构—流浪计划,数据结构,算法
如果叔叔不存在,那么a/b/c/d/e都是空树,cur是新增节点!因为叔叔不存在的话,parent下也不可能挂上黑节点!

此处也违背了性质4,所谓单纯的变色也无法处理了,那就旋转试试看吧,
祖孙三代在一条直线上偏左,一波右单旋安排,接着根节点变成 parent 后调整颜色,父亲变黑,祖父变红,一波操作下来黑节点数目没边,根节点也是黑色不用再向上调整了

手撕红黑树,数据结构—流浪计划,数据结构,算法

💥具体情况2️⃣:u存在且为黑

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

手撕红黑树,数据结构—流浪计划,数据结构,算法

b和c可以是空树或者是一个红节点,a可以是根也可以是黑节点的子树,下面四种的的任意一种

手撕红黑树,数据结构—流浪计划,数据结构,算法

以下的情况一定是由情况一往上调整过程中才会出现的!即这种情况下的cur结点一定不是新插入的结点,而是上一次情况一调整过程中的祖父结点,如下图:

手撕红黑树,数据结构—流浪计划,数据结构,算法

此上的情况必定是左边的的多一个黑色节点,如上图所示, 假设祖父上面有 x 个黑节点,叔叔下有y个节点,那么左子树(含祖父)现在是 x +1 个,右子树(含祖父)是 x + 2 + y 个,很明显 x + 2 + y > x + 1,因此在插入结点前就已经不满足要求了,所以说叔叔结点存在且为黑这种情况,一定是由情况一往上调整过程中才会出现的!

此处单纯的变色已经无法处理,况且还违背了性质4;这时我们需要进行旋转+变色处理

手撕红黑树,数据结构—流浪计划,数据结构,算法

如果是祖父、父亲、cur 都在同一条直线上,那么只需要单旋即可

💥双旋是怎么样产生的?

若祖孙三代的关系是折线(cur、parent、grandfather这三个结点为一条折现),则我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理

抽象图如下:

手撕红黑树,数据结构—流浪计划,数据结构,算法

以上情况的完整代码:

//如果插入节点的父节点是红色的,则需要对红黑树进行操作
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			assert(grandfather);
			assert(grandfather->_col == BLACK);

			//关键看叔叔  ~ 判断叔叔的位置
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				//情况1:uncle存在且为红  + 继续往上处理
				if (uncle && uncle->_col == RED)
				{
					//变色:p和u变黑,g变红
					parent->_col = uncle ->_col = BLACK;
					grandfather->_col = RED;

					//继续往上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else  //情况2 + 情况3:uncle不存在 + uncle存在且为黑
				{
					//情况二:单旋 + 变色
					//    g
					//  p   u
					//c            
					if (cur = parent->_left)
					{
						RotateR(grandfather);//右旋

						//颜色调整
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else//cur == parent->_right
					{
						//情况三:左右双旋 + 变色
						//    g
						//  p   u
						//    c 
						RotateL(parent);
						RotateR(grandfather);

						//调整颜色
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
			else  //parent == grandfather->_right
			{
				Node* uncle = grandfather->_left;
				//情况1:uncle存在且为红 + 继续往上处理
				if (uncle&& uncle->_col == RED)
				{
					//变色:p和u变黑,g变红
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					//继续往上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else  //情况2 + 情况3:uncle不存在 + uncle存在且为黑
				{
					//情况二:单旋 + 变色
					//    g
					//  u   p
					//        c            
					if (cur = parent->_right)
					{
						RotateL(grandfather);//左单 旋

						//颜色调整
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else//cur == parent->_left
					{
						//情况三:右左双旋 + 变色
						//    g
						//  u   p
						//    c 
						RotateR(parent);
						RotateL(grandfather);

						//调整颜色
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}	
		}

大总结

无论是情况一还是情况二,cur为红,p为红,g为黑这三个条件是固定的

  • 情况一:叔叔存在且为红

    • 1️⃣单纯的变色:p和u变黑,g变红
    • 2️⃣把g继续当成cur,g不是根继续往上处理(继续往上处理有可能变成情况2);g是根就变成黑色
  • 情况二:叔叔不存在 or 叔叔存在且为黑

    • 1️⃣旋转:具体情况来判断是什么旋转(祖孙三代是折线关系就是双旋,直线就是单旋)
    • 2️⃣变色:p变黑, g变红

手撕红黑树,数据结构—流浪计划,数据结构,算法

五. 验证红黑树

还是老套路中序遍历来验证:

void Inorder()
{
	_Inorder(_root);
}

void _Inorder(Node* root)
{
	if (root == nullptr)//空树也是红黑树
		return;
	_Inorder(root->_left);
	cout << root->_kv.first << " ";
	_Inorder(root->_right);
}

手撕红黑树,数据结构—流浪计划,数据结构,算法

那怎么样验证它是红黑树呢?从五大特性下手!

	bool IsBalance()
	{
		if (_root == nullptr)
		{
			return true;
		}

		if (_root->_col == RED)
		{
			cout << "根节点不是黑色" << endl;
			return false;
		}

		// 黑色节点数量基准值
		int benchmark = 0;
		Node* cur = _root;
		while (cur)
		{
		if (cur->_col == BLACK)
		++benchmark;

		cur = cur->_left;//以最左的路径进行
		}

		return PrevCheck(_root, 0, benchmark);
	}
	bool PrevCheck(Node* root, int blackNum, int& benchmark)
	{
		if (root == nullptr)
		{
			//cout << blackNum << endl;
			//return;
			if (benchmark == 0)
			{
				benchmark = blackNum;
				return true;
			}

			if (blackNum != benchmark)
			{
				cout << "某条黑色节点的数量不相等" << endl;
				return false;
			}
			else
			{
				return true;
			}
		}

		if (root->_col == BLACK)
		{
			++blackNum;
		}
        //检测当前节点以及父亲
		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << "存在连续的红色节点" << endl;
			return false;
		}

		return PrevCheck(root->_left, blackNum, benchmark)
			&& PrevCheck(root->_right, blackNum, benchmark);
	}

六. 红黑树的性能

红黑树的删除本节不做讲解,有兴趣的可参考:《算法导论》或者《STL源码剖析》

参考地址

七. 红黑树的性能

AVL 树和红黑树都是平衡二叉树的两个老大哥,他们增删查改的时间复杂度都O(logN),到底谁更技高一筹?

其实在大数据的场景下,比如百万级量化数据,AVL 需要构建大约 20 多层,同时红黑树需要构建大约 40 多层,毕竟红黑树是近似平衡的二叉搜索树。

但是我们知道 20 和 40 在 CPU 运算速度面前并没有什么差别,虽然 AVL 树在效率上会略胜红黑树一点点,但是生活中红黑树的运用却比 AVL 树更为广泛,因为 AVL 树的效率是有代价的,是充分牺牲结构进行不断旋转得到的,而红黑树大大降低了旋转次数会更安全因此,换来了更优的性能文章来源地址https://www.toymoban.com/news/detail-796956.html

到了这里,关于【高阶数据结构】手撕红黑树(超详细版本)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++手撕红黑树

    概念 和AVL树一样,红黑树也是一种二叉搜索树,是解决二叉搜索树不平衡的另一种方案,他在每个节点上增加一个存储位,用于表示节点的颜色,是Red或者Black 红黑树的核心思想是通过一些着色的条件限制,达到一种最长路径不超过最短路径的两倍的状态 所以说红黑树并不

    2024年04月12日
    浏览(29)
  • 【C++】手撕红黑树

    作者简介:დ旧言~,目前大二,现在学习Java,c,c++,Python等 座右铭:松树千年终是朽,槿花一日自为荣。 目标:能直接手撕红黑树。 毒鸡汤:行到水穷处,坐看云起时。。 望小伙伴们点赞👍收藏✨加关注哟💕💕  相信大家肯定听过在C++大名鼎鼎的两颗树,这两颗树分别

    2024年03月23日
    浏览(25)
  • 手撕红黑树

    由于AVL对于平衡的要求过于严格,这就导致了AVL的调整操作会被大量的进行,别看单词的时间消耗短,由于AVL树对于平衡要求的苛刻,这就会积累大量的旋转操作,这一累加起来就是不小的时间消耗; 为了减少AVL树的旋转操作,红黑树不就诞生了! 红黑树:红黑树不是一颗严

    2024年02月04日
    浏览(38)
  • c++代码手撕红黑树

    企业里永远是技术驱动理论发展 比起理解红黑树的原理,更重要的是理解红黑树的应用场景,因为某些应用场景的需要,红黑树才会应运而生。 红黑树的特点: 插入,删除,查找都是O(logn)的复杂度。 红黑树的应用: epoll的实现,内核会在内存开辟一个空间存放epoll的红黑树

    2024年02月06日
    浏览(29)
  • 史上最详细的红黑树讲解(一篇文章教你手撕红黑树)

           🔥🔥 欢迎来到小林的博客!!       🛰️博客主页:✈️小林爱敲代码       🛰️博客专栏:✈️数据结构与算法       🛰️欢迎关注:👍点赞🙌收藏✍️留言       今天给大家讲解红黑树,和AVL树一样,这章暂且不讲删除。

    2024年01月16日
    浏览(32)
  • [数据结构]-红黑树

    前言 作者 : 小蜗牛向前冲 名言: 我可以接受失败,但我不能接受放弃   如果觉的博主的文章还不错的话,还请 点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一、红黑树的基本知识  1、红黑树的概念 2、性质  二、红黑树的模拟实

    2024年02月04日
    浏览(36)
  • 红黑树数据结构

    现在JAVASE中HashMap中底层源码是由数组+链表+红黑树进行设计的,然后很多地方也是用到红黑树,这里单独对红黑树数据结构进行简单的介绍。 目录 红黑树概念 红黑树的性质 自平衡规则 代码   红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可

    2024年02月01日
    浏览(30)
  • 数据结构 | 红黑树

    节点的左边比节点的值小,右边比节点的值大。 节点要么是 红色 ,要么是 黑色 根节点 是黑色 叶子节点都是黑色的空节点 红黑树中红色节点的子节点都是黑色 从任一节点到叶子节点的所有路径都包含相同数目的黑色节点 在添加或者删除节点的时候,如果不满足这些性质会

    2024年01月21日
    浏览(32)
  • 数据结构——红黑树

    目录 概念 性质 结点的定义  插入 调整 当p是g的左孩子时 当p为g的右孩子时 插入完整代码 红黑树的检测 红黑树完整代码(包括测试数据)   红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是RED或BLACK。 通过对任何一条从根到叶子的路径

    2023年04月09日
    浏览(35)
  • 【数据结构】红黑树

    🐱作者:一只大喵咪1201 🐱专栏:《数据结构与算法》 🔥格言: 你只管努力,剩下的交给时间! 在学习AVL树的时候,我们知道,当修改AVL树的结构(插入,删除)时,会通过旋转来保证平衡因子不超过1,所以频繁的修改结构会导致效率低下,今天我们学习的红黑树就完美解

    2023年04月17日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包