红黑树深度解析:设计原理与实现逻辑

  • 路径:从根节点到一个空节点算一条路径
  • 路径长度:从根节点到空节点的长度

红黑树的概念

  • 红黑树是一颗二叉搜索树,它的每个节点增加一个存储为来表示结点的颜色,可以是红色或者黑色。
  • 通过对任何一条从根到叶子的路径上个点的颜色进行约束,红黑树确保没有一条路径会比其他路经长出2倍,因而是接近平衡的。
  • 下图的红黑树共9条路径

红黑树的规则

  • 规则1:每个结点不是红色就是黑色
  • 规则2:根节点是黑色的
  • 规则3:如果一个节点是红色的,则它的两个孩子节点必须是黑色的,也就是说任意一条路径不会有连续的红色节点
  • 规则4:对于任意一个节点,从该节点到其所有NULL节点的简单路径上,均包含相同数量的黑色节点

红黑树如何确保最长路径不超过最短路径的2倍的?

  • 按照规则4和上面的图可知,从根到 NULL 节点的每条路径都有相同数量的黑色节点,所以在极端情况下,最短路径就是全是黑色节点的路径,假设最短路径长度为bh(black height)
  • 由规则2和3可知,任意一条路径不会有连续的红色节点,所以在极端场景下,最长的路径是一黑一红间隔组成,那么最长路径长的为 2*bh
  • 综上,任意一条从根到 NULL 节点路径的长度为 h,那么 bh <= h <=2*bh

红黑树效率问题

  • 因为红黑树高度是从 bh 到 2*bh,所以节点个数是 2^bh - 1 <= N <= 2^(2*bh) - 1,所以 bh ≈ logN。这就意味着红黑树增删改查最坏也就是走嘴上路径2 * logN,那么时间复杂度还是 O(logN)
  • 插入相同的节点,AVL树旋转次数比红黑树要多,所以说红黑树还是很高效的

红黑树的实现

红黑树的结构

复制代码
//枚举值表示颜色
enum Colour
{
	RED,
	BLACK
};

//这里默认使用key/value结构实现
template<class K,class V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Color _col;

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

template<class K,class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
private:
	Node* _root = nullptr;
};

红黑树的插入

大致过程

  • 插入一个值按二叉搜索树规则进行插入,插入后我们只需要关注是否符合红黑树的4条规则
  • 如果是空树插入,新增节点是黑色节点;如果是非空树插入,新增节点必须是红色节点,因为非空树插入,新增黑色节点就破坏了规则4,规则4是很难维护的
  • 非空树插入后,如果父亲是黑色的,则没有违反任何规则,插入结束
  • 非空树插入后,==如果父亲节点是红色的,==则违反了规则3,需要进一步分析,==c(新插入节点)是红色,p(父亲节点)是红色,g(p的父亲节点)必为黑,p的兄弟节点标识为u,==需要根据u的不同分情况处理

情况1:变色

  • c为红,p为红,g为⿊,u存在且为红
  • 将 p 和 u 变黑,g 变红,再把 g 当作新的 c,继续向上更新
  • 因为p和u都是红⾊,g是⿊⾊,把p和u变⿊,左边⼦树路径各增加⼀个⿊⾊结点,g再变红,相 当于保持g所在⼦树的⿊⾊结点的数量不变,同时解决了c和p连续红⾊结点的问题,需要继续往上更新是因为g是红⾊,如果g的⽗亲还是红⾊,那么就还需要继续处理;如果g的⽗亲是⿊⾊,则处理结束了;如果g就是整棵树的根,再把g变回⿊⾊。

情况2:单旋+变色

  • c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点;u存在且为⿊,则 c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上来的。

  • p必须是黑色才能解决连续红色的问题,u不存在或者是黑色的,这里单纯的变色无法解决问题

    • 如果p是g的左,c是p的左,那么以g为旋转点进行右单旋,再把p变黑,g变红即可。p变成这棵树新的跟,这样子树黑色节点的数量不变,没有连续的红色节点了,且不需要向上更新,因为p的父亲是黑色节点

    • 如果==p是g的右,c是p的右,那么以g为旋转点进⾏左单旋,再把p变⿊,g变红即可。==p变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为p的⽗亲是⿊⾊还是红⾊或者空都不违反规则

      复制代码
      	g
      p		u

      c

      复制代码
      	g
      u		p
      			c
  • 适用于左节点和左节点的左节点都为红或右节点和右节点的右孩子都为红,旋转后将p变黑,g变红

情况3:双旋+变色

  • c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点;u存在且为⿊,则 c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变色将c从⿊⾊变成红⾊,更新上来的
  • p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解决问题,需要旋转+变⾊。
    • 如果==p是g的左,c是p的右,那么先以p为旋转点进⾏左单旋,再以g为旋转点进⾏右单旋,再把c变⿊,g变红即可。==c变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为c的⽗亲是⿊⾊还是红⾊或者空都不违反规则。

    • 如果p是g的右,c是p的左,那么先以p为旋转点进⾏右单旋,再以g为旋转点进⾏左单旋,再把c变⿊,g变红即可。c变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为c的⽗亲是⿊⾊还是红⾊或者空都不违反规则

      复制代码
      g

      p u
      c

      复制代码
      g

      u p
      c

适用于左节点和左节点的右节点都为红或右节点和右节点的左左节点都为红,旋转后将c变黑,g变红

复制代码
	//旋转代码的实现跟AVL树是一样的,只是不需要更新平衡因子
	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;
			//   g
			// p   u
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)
				{
					//u存在且为红-》变色继续向上处理
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//u存在且为黑或者不存在-》旋转+变色
					if (cur == parent->_left)
					{
						//		g
						//	 p		u
						//c
						//单旋
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//		g
						//	p		u
						//		c
						//双旋
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					
					break;
				}
			}
			else
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					grandfather->_col = RED;
					parent->_col = uncle->_col = BLACK;
					
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//		g
					//	u		p
					//				c
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//		g
						//	u		p
						//		c
						RotateR(parent);
						RotateL(grandfather);
						grandfather->_col = RED;
						cur->_col = BLACK;
					}

					break;
				}
			}
		}
		_root->_col = BLACK;
		return true;
	}

红黑树的查找

  • 按二叉搜索树逻辑实现即可,搜索效率O(logN)

    复制代码
      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点规则,⼀定能保证最⻓路径不超过最短路径的2倍。
    • 规则1枚举颜⾊类型,天然实现保证了颜⾊不是⿊⾊就是红⾊。

    • 规则2直接检查根即可

    • 规则3前序遍历检查,遇到红⾊结点查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检 查⽗亲的颜⾊就⽅便多了。

    • 规则4前序遍历,遍历过程中⽤形参记录跟到当前结点的blackNum(⿊⾊结点数量),前序遍历遇到⿊⾊结点就++blackNum,⾛到空就计算出了⼀条路径的⿊⾊结点数量。再任意⼀条路径⿊⾊结点数量作为参考值,依次⽐较即可

      复制代码
      bool Check(Node* root, int blackNum, const int refNum)
      {
      	if (root == nullptr)
      	{
      		//前序遍历走到空时,意味着一条路径走完了
      		if (refNum != blackNum)
      		{
      			cout << "存在黑色结点的数量不相等的路径" << endl;
      			return false;
      		}
      
      		return true;
      	}
      
      	//检查孩子不方便,因为孩子有两个,且不一定存在
      	//反过来检查父亲就方便多了
      	if (root->_parent && 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);
      }
相关推荐
gjxDaniel2 小时前
A+B问题天堂版
c++·算法·字符串·字符数组
M__332 小时前
动态规划进阶:简单多状态模型
c++·算法·动态规划
米优2 小时前
使用Qt实现消息队列中间件动态库封装
c++·中间件·rabbitmq
N.D.A.K2 小时前
CF2138C-Maple and Tree Beauty
c++·算法
AI视觉网奇2 小时前
ue 5.5 c++ mqtt 订阅/发布 json
网络·c++·json
程序员-King.2 小时前
day159—动态规划—打家劫舍(LeetCode-198)
c++·算法·leetcode·深度优先·回溯·递归
txinyu的博客3 小时前
解析muduo源码之 StringPiece.h
开发语言·网络·c++
墨雪不会编程3 小时前
C++【string篇4】string结尾篇——字符编码表、乱码的来源及深浅拷贝
android·开发语言·c++
CSDN_RTKLIB3 小时前
【map应用】组合键统计
c++·stl