C++ —— 红黑树

目录

[1. 红黑树的概念](#1. 红黑树的概念)

[1.1 红黑树的规则](#1.1 红黑树的规则)

[1.2 思考:红黑树如何确保最长路径不超过最短路径的2倍的?](#1.2 思考:红黑树如何确保最长路径不超过最短路径的2倍的?)

[1.3 红黑树的效率](#1.3 红黑树的效率)

[2. 红黑树的实现](#2. 红黑树的实现)

[2.1 红黑树的结构](#2.1 红黑树的结构)

[2.2 红黑树的插入](#2.2 红黑树的插入)

[2.2.1 红黑树插入一个值的大概过程](#2.2.1 红黑树插入一个值的大概过程)

[2.2.2 情况1:变色](#2.2.2 情况1:变色)

[2.2.3 情况2:单旋+变色](#2.2.3 情况2:单旋+变色)

[2.2.4 情况3:双旋+变色](#2.2.4 情况3:双旋+变色)

[2.3 红黑树的插入代码实现](#2.3 红黑树的插入代码实现)

[2.4 红黑树的查找](#2.4 红黑树的查找)

[2.5 红黑树的验证](#2.5 红黑树的验证)

[2.6 红黑树的删除](#2.6 红黑树的删除)


1. 红黑树的概念

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

1.1 红黑树的规则

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

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

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

注意:

上面红黑的路径可不是4条哦,不是数红黑的叶子节点的个数,而是数的是根节点到NULL的路径:

上面有9个NULL,那么就有9条路径

1.3 红黑树的效率

假设N是红黑树树中结点数量 ,h最短路径长度,那么 ,由此推出 ,也意味着红黑树增删查改最坏也就是走最长路径 ,那么时间复杂度还是

.

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

2. 红黑树的实现

2.1 红黑树的结构

2.2 红黑树的插入

2.2.1 红黑树插入一个值的大概过程

  1. 插入一个值按二叉搜索树规则进行插入,插入后我们只需要观察是否符合红黑树的4条规则。
  2. 如果是空树插入,新增节点就是黑色节点。如果是非空树插入,新增节点必须为红色节点,因为非空树插入,新增黑色节点就破化了规则4,规则4是很难维护的。
  3. 非空树插入后,新增节点必须为红色节点,如果父亲节点是黑色的,则没有违反任何规则4,插入结束。(新增节点为红色,可能会破坏规则3,但是新增节点为黑色,一定会破坏规则4)
  4. 非空树插入后,新增节点必须为红色节点,如果父亲节点是红色的,则没有违反任何规则3,进一步分析,c是红色,p为红,g必为黑,这三个颜色都固定了,关键的变化看u的情况,需要根据u分为以下几种情况分别处理。

新增节点标识为c(cur),c的父亲标识为p(parent),p的父亲标识为g(grandfather),p的兄弟标识为u(uncle)。

2.2.2 情况1:变色

c为红,p为红,g为黑,u存在且为红,则将p和u变黑,g变红。再把g当作新的c,继续往上更新。

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

情况1只变色,不旋转。所以无论c是p的左还是右,p是g的左还是右,都是上面的变色处理方式。

图0

  • 跟AVL树类似,图0我们展示了一种具体情况,但是实际中需要这样处理的有很多种情况。
  • 图1将以上类似的处理进行了抽象表达,d/e/f代表每条路径拥有hb个黑色节点的子树,a/b代表每条路径拥有hb-1个黑色节点的根为红的子树,hb>0。
  • 图2/图3/图4,分别展示了 hb == 0/hb == 1/hb == 2的具体情况组合分析,当hb等于2是,这里组合情况上百亿中,这些样例是帮助我们理解,不管情况有多少种,多么复杂,处理方式一样的,变色再继续往上处理即可,所以我们值需要看抽象图即可。

图1

图2

图3

图4

2.2.3 情况2:单旋+变色

c为红,p为红,g为黑,u不存在或者u存在且为黑,u不存在,则c一定是新增节点,u存在且为黑,则c一定不是新增,c之前是黑色的,是在c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。

分析:

p必须变黑,才能解决,连续红色节点的问题,u不存在或者是黑色的,这里单纯的变色是无法解决问题,需要旋转+变色。

g

p u

c

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

g

u p

c

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

2.2.4 情况3:双旋+变色

c为红,p为红,g为黑,u不存在或者u存在且为黑,u不存在,则c一定是新增节点,u存在且为黑,则c一定不是新增,c之前是黑色的,是在c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。

分析:

p必须变黑,才能解决,连续红色节点的问题,u不存在或者是黑色的,这里单纯的变色是无法解决问题,需要旋转+变色。

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变红即可。c变成这颗树新的根,这样子树黑色节点的数量不变,没有连续的红色节点了,且不需要往上更新,因为c的父亲是黑色还是红色或者空都不违反规则。

2.3 红黑树的插入代码实现

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)
	{
		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 ( kv.first < parent->_kv.first)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}
	cur->_parent = parent;

	while (parent && parent->_col == RED)
	{
		Node* granderfather = parent->_parent;
		if (parent == granderfather->_left)
		{
			Node* uncle = granderfather->_right;
			//叔叔存在且为红
			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				granderfather->_col = RED;

				//继续向上处理
				cur = granderfather;
				parent = cur->_parent;
			}
			else
			//叔叔不存在 || 叔叔存在且为黑
			{
				if (cur == parent->_left)
				{
					RotateR(granderfather);
					parent->_col = BLACK;
					granderfather->_col = RED;
				}
				else
				{
					//双旋+变色
					//      g             g    
					//   p        or   p     u(B)
					//      c             c 
					RotateL(parent);
					RotateR(granderfather);
					cur->_col = BLACK;
					granderfather->_col = RED;
				}
			}
		}
		else  //parent = grandfather->right
		{
			//      g
			// u(R)     p
			//             c   
			//叔叔存在且为红,只需要变色,不需要旋转
			Node* uncle = granderfather->_left;
			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				granderfather->_col = RED;

				cur = granderfather;
				parent = cur->_parent;
			}
			else
				//叔叔不存在或叔叔存在且为黑,旋转+变色
			{
				//     g
				//  u     p
				//          c
				if (cur == parent->_right)
				{
					RotateL(granderfather);
					parent->_col = BLACK;
					granderfather->_col = RED;
				}
				else
				{
					//    g
					// u     p 
					//    c 
					RotateR(parent);
					RotateL(granderfather);
					cur->_col = BLACK;
					granderfather->_col = RED;

				}
				break;
			}

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

2.4 红黑树的查找

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

cpp 复制代码
	Node* Find(constK& 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.5 红黑树的验证

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

  1. 规则1枚举颜色类型,天然实现保证了颜色不是黑色就是红色。
  2. 规则2直接检查根即可
  3. 规则3前序遍历检查,遇到红色节点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色方便多了。
  4. 规则4前序遍历,遍历过程中用形参记录跟到当前节点的blackNum(黑色节点数量),前序遍历遇到黑色节点就++blackNum,走到空就计算出了一条路径的黑色节点数量。在任意一条路径黑色接待你数量作为参考值,依次比较即可。
cpp 复制代码
//前序递归遍历
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 && 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 IsBalanceTree()
{
	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);

	//将refNum传到Check函数中


}
cpp 复制代码
// 测试代码
void TestRBTree1()
{
	RBTree<int, int> t;
	// 常规的测试用例
	int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	// 特殊的带有双旋场景的测试用例
	//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		t.Insert({ e, e });
	}
	t.InOrder();
	cout << t.IsBalanceTree() << endl;
}

void TestRBTree2()
{
	const int N = 100000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));

	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
	}

	RBTree<int, int> t;
	for (size_t i = 0; i < v.size(); ++i)
	{
		t.Insert(make_pair(v[i], v[i]));
	}
	cout << t.IsBalanceTree() << endl;
}

int main()
{
	TestRBTree1();
	//TestRBTree2();

	return 0;
}

运行结果:

cpp 复制代码
int main()
{
	TestRBTree2();

	return 0;
}

运行结果:(也没问题)

2.6 红黑树的删除

不是很重要,这里就不对红黑树进行描述了。

相关推荐
炘爚2 小时前
C++(普通指针和成员的区别、指针的使用场景和存储内容)
数据结构·c++·算法
松☆2 小时前
C++ 控制台通讯录管理系统 —— 从零实现到完整解析(附可运行代码)
开发语言·网络·c++
wjs20242 小时前
Python 3 输入和输出
开发语言
炘爚2 小时前
C++(在Mystring类中碰到的构造函数和析构函数以及深拷贝和浅拷贝的问题)
开发语言·c++·算法
Chasing Aurora2 小时前
Python后端开发之旅(五)——DL
开发语言·pytorch·python·深度学习
啥都想学点2 小时前
第18天:Springboot 项目搭建
java·spring boot·后端
Fang fan2 小时前
Java集合
java·开发语言·算法
AI成长日志2 小时前
【笔面试算法学习专栏】链表操作专题:反转、环形检测与合并
学习·算法·面试
liulilittle2 小时前
TC Hairpin NAT 驱动使用手册(个人版)
服务器·开发语言·网络·c++·网络协议·tcp/ip·tc