【C++】红黑树实现

目录

  • 一、红黑树的基本概念
    • [1.1 红黑树是什么?](#1.1 红黑树是什么?)
    • [1.2 红黑树的五大规则](#1.2 红黑树的五大规则)
    • [1.3 红黑树的效率](#1.3 红黑树的效率)
    • [1.4 红黑树的底层节点结构](#1.4 红黑树的底层节点结构)
  • 二、红黑树的实现
    • [2.1 红黑树的 insert 操作](#2.1 红黑树的 insert 操作)
      • [2.1.1 情况一: 变色](#2.1.1 情况一: 变色)
      • [2.1.2 情况二: 单旋 + 变色](#2.1.2 情况二: 单旋 + 变色)
      • [2.1.3 情况三: 双旋 + 变色](#2.1.3 情况三: 双旋 + 变色)
    • [2.2 中序遍历](#2.2 中序遍历)
    • [2.3 其它接口的实现](#2.3 其它接口的实现)
      • [2.3.1 红黑树的验证](#2.3.1 红黑树的验证)
      • [2.3.2 Size 、 Height 函数、查找函数](#2.3.2 Size 、 Height 函数、查找函数)


个人主页<---请点击
C++专栏<---请点击

一、红黑树的基本概念

1.1 红黑树是什么?

红黑树是一种自平衡的二叉查找树 ,红黑树本质上是一棵二叉搜索树 ,它拥有二叉搜索树的所有基本特性。普通的二叉搜索树在插入有序数据时,会退化成链表,查找效率从 O(log n) 降为 O(n)。为了解决这个问题,红黑树在二叉搜索树的基础上增加了以下五个核心规则,通过这些规则来约束树的生长,使其尽可能地保持平衡。

1.2 红黑树的五大规则

  • 每个结点不是黑色就是红色
  • 根结点是黑色的
  • 所有叶子结点都是黑色的。(不是普通的叶子结点,而是叶子结点的左右孩子即空结点NIL 或 NULL
  • 红色结点的两个子结点必须是黑色,即任意一条路径上不能有两个连续的红色结点。这条规则是保证平衡的关键,它限制了任何路径上红色节点的数量。
  • 对于任意一个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的黑色结点

注意:最后两条规则共同确保了没有一条路径会比其他路径长出两倍,从而近似平衡

1.3 红黑树的效率

假设N是红黑树树中结点数量,h最短路径的长度,那么 (2h - 1) <= N <= (22h - 1), 由此推出h ≈ logN,也就是意味着红黑树增删查改最坏也就是走最长路径 2 ∗ logN,那么时间复杂度还是O(logN)

红黑树 的表达相对AVL树要抽象一些,AVL树通过高度差直观的控制了平衡。红黑树通过规则对颜色的约束,间接的实现了近似平衡,它们效率都是同一档次,但是相对而言,插入相同数量的结点,红黑树的旋转次数是更少的,因为它对平衡的控制没那么严格。

1.4 红黑树的底层节点结构

cpp 复制代码
// 枚举值表示颜色
enum Color
{
	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)
		, _col(RED)
	{ }
};

每个结点都是有颜色区分的,所以我们可以枚举RED、BLACK两种状态。与AVL树的节点结构相比将平衡因子换成了节点颜色。

二、红黑树的实现

2.1 红黑树的 insert 操作

细节

  • 如果是空树插入,新增结点是黑色结点 。如果是非空树插入,新增结点必须红色结点 ,因为非空树插入,新增黑色结点就破坏了规则5,规则5是很难维护的。
  • 非空树插入时,新增结点必须红色结点,如果父结点为黑色,则没有违反规则,插入结束,如果父结点为红色,违反了规则4,此时就需要分情况变色处理
cpp 复制代码
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)
	{
		parent = cur;
		if (kv.first > cur->_kv.first)
		{
			cur = cur->_right;
		}
		else if (kv.first < cur->_kv.first)
		{
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}

	cur = new Node(kv);
	cur->_col = RED;

	if (cur->_kv.first > parent->_kv.first) parent->_right = cur;
	else parent->_left = cur;
	cur->_parent = parent;

	while (parent && parent->_col == RED)
	{
		// 处理违反规则的情况
	}

	return true;
}

那么如何处理违反规则的情况呢?违反规则的原因主要是产生了连续的红色结点,我们可以分析一下情况,我们把新插入的结点叫做c(cur) ,把新插入结点的父结点叫做p(parent) ,把父结点的父结点叫做g(grandpa) ,把g结点的另一个孩子结点叫做u(uncle)

其中我们新插入的结点是红色的,p结点一定是红色那么g结点一定是黑色 ,它们都是固定的,所以唯一的变量就是u结点

u结点可以分为三种情况,1、u结点不存在;2、u结点是红色结点;3、u结点是黑色结点。

2.1.1 情况一: 变色

当叔叔存在且为红色结点,则将pu变黑,g变红。在把g当做新的c,继续往上更新 。所以c结点不一定是新增结点,可能是由g结点变红之后再次违反规则产生的新的c

这里我们给出一个实际的示例图:

上图中刚好变色后g结点的父结点为黑结点,所以就不用向上处理了。

c为新增结点时:

c不是新增结点时:

这里有一个细节,就是当c持续往上更新时,如上图,当10就是根节点时,那就不需要向上更新了,但是此时10这个结点变成了红色,循环已经结束了,那怎么处理呢? 很简单,我们在循环外面加上一个_root->_col = BLACK;语句就好了。

搞清楚这些,我们就可以写这种情况在插入部分的代码了。

cpp 复制代码
while (parent && parent->_col == RED)
{
	// 处理违反规则的情况
	Node* grandpa = parent->_parent;
	// g 结点存在, 且左边是 p 结点
	if (grandpa && grandpa->_left == parent)
	{
		Node* uncle = grandpa->_right;

		// 叔叔存在且为红,变色处理
		if (uncle && uncle->_col == RED)
		{
			parent->_col = uncle->_col = BLACK;
			grandpa->_col = RED;

			// 继续向上处理
			cur = grandpa;
			parent = grandpa->_parent;
		}
	}
	else if (grandpa && grandpa->_right == parent)
	{
		Node* uncle = grandpa->_left;

		// 叔叔存在且为红,变色处理
		if (uncle && uncle->_col == RED)
		{
			parent->_col = uncle->_col = BLACK;
			grandpa->_col = RED;

			// 继续向上处理
			cur = grandpa;
			parent = grandpa->_parent;
		}
	}
}
// 将处理过程中变色的根结点恢复成黑色结点
_root->_col = BLACK;

2.1.2 情况二: 单旋 + 变色

u不存在,或者u存在且为黑色结点

复制代码
     g
  p     u
c

如果pg的左,cp的左,那么以g为旋转点进行右单旋,再把p变黑,g变红即可。p变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父结点是黑色还是红色或者为空都不违反规则

复制代码
     g
  u     p
          c

如果pg的右,cp的右,那么以g为旋转点进行左单旋,再把p变黑,g变红即可。p变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父结点是黑色还是红色或者为空都不违反规则


u结点不存在时,c必须是新增结点 。因为u结点这条路径上只有一个黑色结点,如果c是由g变色而来,那么说明左边这条路径上的黑色结点大于1,这绝对不可能,所以当u不存在时,c就是新增结点。

注意:下图中标注的hb是子树中黑色结点的数量

u结点存在且为黑色时,c结点肯定不是新增结点 。因为u结点这条路径上有至少两个黑色结点,如果c为新增结点,那么这条路径上才有一个黑色结点,绝对不可能,所以此时c结点不是新增结点。

了解这些之后就可以实现这种情况的插入部分代码了。

cpp 复制代码
		while (parent && parent->_col == RED)
		{
			// 处理违反规则的情况
			Node* grandpa = parent->_parent;
			// g 结点存在, 且左边是 p 结点
			if (grandpa && grandpa->_left == parent)
			{
				Node* uncle = grandpa->_right;

				// 叔叔存在且为红,变色处理
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandpa->_col = RED;

					// 继续向上处理
					cur = grandpa;
					parent = grandpa->_parent;
				}
				else // 叔叔不存在 或者 存在且为黑
				{
					if (cur == parent->_left)
					{
						//    g
						//  p   u
						//c
						//右单旋 + 变色
						RotateR(grandpa);
						parent->_col = BLACK;
						grandpa->_col = RED;
					}
					break;
				}
			}
			else if (grandpa && grandpa->_right == parent)
			{
				Node* uncle = grandpa->_left;

				// 叔叔存在且为红,变色处理
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandpa->_col = RED;

					// 继续向上处理
					cur = grandpa;
					parent = grandpa->_parent;
				}
				else // 叔叔不存在 或者 存在且为黑
				{
					if (cur == parent->_right)
					{
						//    g
						//  u   p
						//        c
						//左单旋 + 变色
						RotateL(grandpa);
						parent->_col = BLACK;
						grandpa->_col = RED;
					}
					break;
				}
			}
		}
		// 将处理过程中变色的根结点恢复成黑色结点
		_root->_col = BLACK;

		return true;
	}
private:
	void RotateR(Node* parent)
	{
		//parent 是旋转点, 不是轴心
		Node* subL = parent->_left; //相当于插入函数部分的 cur
		Node* subLR = subL->_right;

		//进行旋转操作
		parent->_left = subLR;
		if (subLR)//维护 subLR 的父指针
		{
			subLR->_parent = parent;
		}
		Node* pparent = parent->_parent; //subL 之后要更改的父指针指向

		subL->_right = parent;
		parent->_parent = subL;

		//判断之前 parent 是什么角色, 便于更改 subL 的父指针
		if (parent == _root)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			if (pparent->_left == parent)
			{
				pparent->_left = subL;
			}
			else pparent->_right = subL;

			subL->_parent = pparent;
		}
	}

	void RotateL(Node* parent)
	{
		//parent 是旋转点, 不是轴心
		Node* subR = parent->_right; //相当于插入函数部分的 cur
		Node* subRL = subR->_left;

		//进行旋转操作
		parent->_right = subRL;
		if (subRL)//维护 subRL 的父指针
		{
			subRL->_parent = parent;
		}

		Node* pparent = parent->_parent; //subR 之后要更改的父指针指向

		subR->_left = parent;
		parent->_parent = subR;

		//判断之前 parent 是什么角色, 便于更改 subR 的父指针
		if (parent == _root)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (pparent->_left == parent)
			{
				pparent->_left = subR;
			}
			else pparent->_right = subR;

			subR->_parent = pparent;
		}
	}

其中左单旋、右单旋部分在上期的AVL树的模拟实现部分讲解了,如果感兴趣可以点此查看:详细操作

2.1.3 情况三: 双旋 + 变色

这种情况也是u结点不存在或者存在且为黑色 ,只不过c结点相对p结点的位置发生了变化。

复制代码
      g
   p     u
     c

如果pg的左,cp的右,那么先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,再把c变黑,g变红即可。c变成这棵树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父结点是黑色还是红色或者为空都不违反规则

复制代码
      g
   u     p
       c

如果pg的右,cp的左,那么先以p为旋转点进行右单旋,再以g为旋转点进行左单旋,再把c变黑,g变红即可。c变成这棵树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父结点是黑色还是红色或者为空都不违反规则


现在我们可以补充这种情况在插入部分的代码了。

插入操作的完整代码:

cpp 复制代码
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)
	{
		parent = cur;
		if (kv.first > cur->_kv.first)
		{
			cur = cur->_right;
		}
		else if (kv.first < cur->_kv.first)
		{
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}

	cur = new Node(kv);
	cur->_col = RED;

	if (cur->_kv.first > parent->_kv.first) parent->_right = cur;
	else parent->_left = cur;
	cur->_parent = parent;

	while (parent && parent->_col == RED)
	{
		// 处理违反规则的情况
		Node* grandpa = parent->_parent;
		// g 结点存在, 且左边是 p 结点
		if (grandpa && grandpa->_left == parent)
		{
			Node* uncle = grandpa->_right;

			// 叔叔存在且为红,变色处理
			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				grandpa->_col = RED;

				// 继续向上处理
				cur = grandpa;
				parent = grandpa->_parent;
			}
			else // 叔叔不存在 或者 存在且为黑
			{
				if (cur == parent->_left)
				{
					//    g
					//  p   u
					//c
					//右单旋 + 变色
					RotateR(grandpa);
					parent->_col = BLACK;
					grandpa->_col = RED;
				}
				else
				{
					//    g
					//  p   u
					//    c
					//左右双旋 + 变色
					RotateL(parent);
					RotateR(grandpa);
					grandpa->_col = RED;
					cur->_col = BLACK;
				}

				break;
			}
		}
		else if (grandpa && grandpa->_right == parent)
		{
			Node* uncle = grandpa->_left;

			// 叔叔存在且为红,变色处理
			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				grandpa->_col = RED;

				// 继续向上处理
				cur = grandpa;
				parent = grandpa->_parent;
			}
			else // 叔叔不存在 或者 存在且为黑
			{
				if (cur == parent->_right)
				{
					//    g
					//  u   p
					//        c
					//左单旋 + 变色
					RotateL(grandpa);
					parent->_col = BLACK;
					grandpa->_col = RED;
				}
				else
				{
					//    g
					//  u   p
					//    c
					//右左双旋 + 变色
					RotateR(parent);
					RotateL(grandpa);
					cur->_col = BLACK;
					grandpa->_col = RED;
				}

				break;
			}
		}
	}
	// 将处理过程中变色的根结点恢复成黑色结点
	_root->_col = BLACK;

	return true;
}

2.2 中序遍历

cpp 复制代码
public:
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr) return;
	
		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}

测试代码

cpp 复制代码
void TestRBTree1()
{
	RBTree<int, int> t;
	// 特殊的带有双旋场景的测试用例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		if (e == 14)
		{
			int x = 0;
		}

		t.insert({ e, e });
	}
	t.InOrder();
}

测试结果

2.3 其它接口的实现

2.3.1 红黑树的验证

检查这棵树是不是红黑树,就是检查这棵树符不符合红黑树的规则。

cpp 复制代码
public:
	bool IsRBTree()
	{
		if (_root == nullptr) return true;
		if (_root->_col == RED) return false;

		// 检查黑色结点数量的一致性
		//计算最左边的路径作为参考值
		Node* leftcur = _root;
		int blackref = 0;
		while (leftcur)
		{
			if (leftcur->_col == BLACK) blackref++;

			leftcur = leftcur->_left;
		}
		
		return Check(_root, 0, blackref);
	}
private:
	bool Check(Node* cur, int numb, int blackref)
	{
		if (cur == nullptr)
		{
			// 此时本条路径的黑色结点计算完了
			if (numb != blackref)
			{
				cout << "黑色结点的数量不相等" << endl;
				return false;
			}

			return true;
		}

		if (cur->_col == RED && cur->_parent && cur->_parent->_col == RED)
		{
			cout << cur->_kv.first << "->" << "连续的红色节点" << endl;
			return false;
		}

		// 统计路径上的黑色结点数量
		if (cur->_col == BLACK) numb++; 

		return Check(cur->_left, numb, blackref)
			&& Check(cur->_right, numb, blackref);
	}

测试代码

cpp 复制代码
void TestRBTree1()
{
	RBTree<int, int> t;
	// 特殊的带有双旋场景的测试用例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		if (e == 14)
		{
			int x = 0;
		}

		t.insert({ e, e });
	}
	t.InOrder();
	cout << t.IsRBTree() << endl;
}

测试结果

2.3.2 Size 、 Height 函数、查找函数

cpp 复制代码
public:
	int Size()
	{
		return _Size(_root);
	}
	
	int Height()
	{
		return _Height(_root);
	}
private:
	int _Size(Node* root)
	{
		return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
	}
	int _Height(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
	
		int leftH = _Height(root->_left);
		int rightH = _Height(root->_right);
	
		return leftH > rightH ? leftH + 1 : rightH + 1;
	}
cpp 复制代码
Node* Find(const K& key)
{
	Node* cur = _root;

	while (cur)
	{
		if (key > cur->_kv.first)
			cur = cur->_right;
		else if (key < cur->_kv.first)
			cur = cur->_left;
		else
			return cur;
	}
	return nullptr;
}

测试代码

cpp 复制代码
void TestRBTree1()
{
	RBTree<int, int> t;
	// 特殊的带有双旋场景的测试用例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14, 36, 27, 19, 8 };
	for (auto e : a)
	{
		if (e == 14)
		{
			int x = 0;
		}

		t.insert({ e, e });
		cout << "insert:" << e << "->" << t.IsRBTree() << endl;
	}
	t.InOrder();
	cout << t.IsRBTree() << endl;
	cout << t.Size() << " " << t.Height() << endl;
	cout << t.Find(2) << " " << t.Find(17) << endl;
}

测试结果

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

相关推荐
凌康ACG6 小时前
Sciter之c++与前端交互(五)
c++·sciter
郝学胜-神的一滴8 小时前
Linux命名管道:创建与原理详解
linux·运维·服务器·开发语言·c++·程序人生·个人开发
晚风(●•σ )9 小时前
C++语言程序设计——11 C语言风格输入/输出函数
c语言·开发语言·c++
恒者走天下10 小时前
秋招落定,拿到满意的offer,怎么提高自己实际的开发能力,更好的融入团队
c++
天若有情67310 小时前
【c++】手撸C++ Promise:从零实现通用异步回调组件,支持链式调用+异常安全
开发语言·前端·javascript·c++·promise
学困昇11 小时前
C++中的异常
android·java·c++
合作小小程序员小小店11 小时前
桌面安全开发,桌面二进制%恶意行为拦截查杀%系统安全开发3.0,基于c/c++语言,mfc,win32,ring3,dll,hook,inject,无数据库
c语言·开发语言·c++·安全·系统安全
Codeking__11 小时前
C++ 11 atomic 原子性操作
开发语言·c++
crescent_悦11 小时前
PTA L1-020 帅到没朋友 C++
数据结构·c++·算法
卡提西亚12 小时前
C++笔记-34-map/multimap容器
开发语言·c++·笔记