红黑树全解析

红黑树是一种自平衡二叉搜索树,它通过在每个节点上增加一个“颜色”属性(红色或黑色)来维持树的平衡,从而保证了动态数据集操作的高效性。

1. 红黑树的特性与规则

关键特性:

  • 是一种二叉搜索树,具有二叉搜索树的基本性质。
  • 平衡性不是完全严格的,但通过颜色规则限制了树的高度,使其最长路径长度最多为最短路径长度的两倍。

红黑树被广泛应用于需要动态维护有序数据的场景,例如 C++ STL 的 mapset 实现

红黑树的规则

红黑树通过以下规则确保平衡性:

  1. 节点颜色:每个节点要么是红色,要么是黑色。
  2. 根节点必须是黑色
  3. 红色节点的子节点必须是黑色(即红色节点不能连续出现,也称为“红黑交替”)。
  4. 每个节点到其所有后代叶子节点的路径上,必须包含相同数量的黑色节点(称为“黑高”一致性)。
  5. 叶子节点(空节点)是黑色的

时间复杂度

红黑树通过上述规则保证了树的高度在 O(log⁡n)) 的范围内,从而使得常见操作效率始终保持稳定:

  • 查找:O(log⁡n)
  • 插入:O(log⁡n)
  • 删除:O(log⁡n)

空间复杂度

红黑树的空间复杂度与普通二叉搜索树类似,为 O(n),主要用于存储节点和颜色信息。

效率特点

  • 查找效率:与AVL树相近,但略逊于AVL树,因为红黑树的平衡性较松。
  • 插入与删除效率:由于平衡调整操作较少,红黑树的插入和删除操作比AVL树更快。

与其他树的比较

  • 与AVL树:红黑树的旋转操作更少,因此适合频繁插入和删除的场景;而AVL树查找效率稍高,更适合读操作频繁的场景。
  • 与普通二叉搜索树:普通二叉搜索树在最坏情况下可能退化为链表,而红黑树通过颜色规则避免了这种退化。

2.  红黑树的结构

enum Color {
	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;

	Color _col;

	RBTreeNode(const pair<K, V>& _kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
	{}
	
};
template <class T, class V>
class RBTree 
{
	typedef RBTreeNode<K, V> Node;
public:
	//
private:
	Node* _root = nullptr;
};

3. 红黑树的插入

3.1 大致插入过程

1. 按二叉搜索树规则插入: 插入操作首先按照二叉搜索树(BST)的规则进行,也就是通过比较键值确定插入节点的位置。插入后,接下来需要检查并确保红黑树的四条平衡规则仍然成立。

2.1 空树插入:如果树是空的,插入的节点为根节点并且必须是黑色节点。根节点是黑色是红黑树规则之一,确保树的平衡性。

2.2 非空树插入:如果插入的是非空树,则新插入的节点必须是红色节点。这是因为,如果插入黑色节点,可能会破坏红黑树的第四条规则(每个节点到其后代叶子节点的路径上必须有相同数目的黑色节点)。红色节点插入后,通常不会违反规则4,且通过旋转和调整可以保证红黑树的平衡性。

3.1 父节点是黑色的情况:如果插入的节点的父节点是黑色的,则没有违反任何红黑树的规则,插入过程结束。此时,插入节点不需要进行任何额外的调整。

3.2 父节点是红色的情况:如果插入的节点的父节点是红色的,则违反了红黑树的第三条规则(红色节点不能有红色子节点)。此时需要进行颜色固定来进一步处理,尤其是处理插入节点、父节点、祖父节点、以及父节点的兄弟节点(即叔叔节点)之间的关系。

颜色固定

(1)父节点(p)是红色,祖父节点(g)必须是黑色,插入节点(c)的颜色为红色。

(2)根据叔叔节点(u)的颜色以及是否存在,可以产生下面三种变色和旋转组合的情况

3.2 只变色不旋转

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

3.2 变色+单旋

c为红,p为红,g为黑,u不存在或者u存在且为黑。此为变色+单旋的情况。
u不存在,则c⼀定是新增结点;
u存在且为黑,则c⼀定不是新增。在这种情况下,c之前是黑色的,是在c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。
分析:这里单纯的变色无法解决问题,需要旋转+变色。
    g            p
  p u   ->  c   g
c                    u
如果p是g的左,c是p的左,那么以g为旋转点进行右单旋再把p变黑,g变红即可。p变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色 还是红色或者空都不违反规则。
    g             p   
  u p  ->   g    c
      c      u
如果p是g的右,c是p的右,那么以g为旋转点进行左单旋再把p变黑,g变红即可。p变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色 还是红色或者空都不违反规则。

3.3 变色+双旋

c为红,p为红,g为⿊,u不存在或者u存在且为⿊。
u不存在,则c⼀定是新增结点;
u存在且为黑,则c⼀定不是新增而是符合情况1更新上来的。
分析:这里单纯的变色⽆法解决问题,需要旋转+变色。
     g
  p   u
   c
如果p是g的左,c是p的右,那么先以p为旋转点进行左单旋再以g为旋转点进星右单旋再把c变
黑,g变红即可。c变成课这颗树新的根,这样⼦树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父亲是黑色还是红色或者空都不违反规则。
    g
 u   p
    c
如果p是g的右,c是p的左,那么先以p为旋转点进行右单旋再以g为旋转点进行左单旋再把c变
黑,g变红即可

3.4 代码实现

	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 (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if	(kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else {
				return false;
			}
		}
		//插入节点
		cur = new Node(kv);
		cur->_col = RED;
		//与父节点链接
		if (cur->_kv.first < parent->_kv.first)
		{
			parent->_left = cur;
		}
		else {
			parent->_right = cur;
		}
		cur->_parent = parent;
		//变色+旋转。从插入节点向上更新直到符合条件或更新到根节点
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				//1.当u存在且为红色时。只变色不旋转
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				// u存在且为⿊或不存在 -》旋转+变⾊
				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
			{
				//  g
				// u p
				Node* uncle = grandfather->_left;
				// 叔叔存在且为红,-》变⾊即可
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					// 继续往上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else // uncle不存在,或者存在且为⿊
				{
					// 情况⼆:叔叔不存在或者存在且为⿊
					// 旋转+变⾊
					//  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);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return true;	
	}

4. 红黑树的查找

按⼆叉搜索树逻辑实现即可,搜索效率为 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;
	}

5. 红黑树的验证

这里获取最长路径和最短路径,检查最长路径不超过最短路径的2倍是不可行的,因为就算满足这个条件,红黑树也可能颜色不满足规则,当前暂时没出问题,后续继续插入还是会出问题的。所以我们还是去检查4点规则,满足这4点规则,一定能保证最长路径不超过最短路径的2倍。

  1. 规则1枚举颜色类型,天然实现保证了颜色不是黑色就是红色。
  2. 规则2直接检查根即可。
  3. 规则3前序遍历检查,遇到红色节点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色就方便多了。
  4. 规则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->_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);
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值