二叉树进阶(红黑树)

目录

引言

1.红黑树的概念

2.红黑树的模拟实现

2.1 红黑树节点

2.2 插入分析

2.3 测试

3.建树测试


引言

在上文中,我们介绍了AVLTree这种特殊的二叉搜索树,有着高度平衡的特性,完美的避免了单支

树的出现,但是实际上,我们大多运用的却不是AVLTree,而是这篇介绍的红黑树RBTree,原因

就在于AVLTree经常在插入元素的时候旋转,而每次旋转,其实都要消耗资源的,RBTree也会旋

转,但不会什么情况都进行旋转,减少了旋转次数,同时也保证了高度平衡,所以,RBTree的出

现和广泛应用是有其重要意义.

上面这棵树,如果是AVLTree则已经旋转;但红黑树则不会旋转. 

1.红黑树的概念

红黑树,也是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或

Black.

 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其

他路径长出2倍,因而是接近平衡的.

一棵具体的RBTree具体有以下五点限制

1. 每个结点不是红色就是黑色

2. 根节点是黑色的 

3. 如果一个节点是红色的,则它的两个孩子结点是黑色的 

4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

第一点,不需要多解释,毕竟如果还有其它颜色,那就不叫红黑树了

第二点,根节点是黑色,这是规定

第三点,保证了不可能连续出现红色节点的情况(连续黑色有可能) 

那最短路径就是所有节点都是黑色节点,最长路径就是每两个黑色节点之间插入一个红色节点,做到了确保没有一条路径会比其他路径长出2倍

第四,五点,每条路径,包括黑色的空结点,含有相同数目的黑色节点

2.红黑树的模拟实现

2.1 红黑树节点

红黑树的结点,基本和AVLTree完全相同,不过AVLTree结点里面放的是平衡因子,而红黑树结点

放的则是红黑颜色,这里我们采取枚举的方式来区分红黑颜色的不同,实际上也可以采取不同方式

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)
	{}
};

2.2 插入分析

结点构造完毕后,我们就要考虑插入,也就是如何建一棵红黑树

建一棵红黑树实际和建AVLTree非常类似,当然调整策略会有所不同,不过记住三个原则,建一棵

红黑树实际比建一棵AVLTree树可能还要简单 

Principle1.插入节点都为红色

解释:条件4要求红黑树每条路径的黑色节点个数都要相同,假如我们选择插入黑色节点,那就会

同时破坏所有路径,需要对所有路径进行调整;而插入红色节点,则只需要在局部进行调整,因此

我们人为限定,插入的节点都为红色,节点初始化默认为红色

Principle2.根节点为黑色,等效为与根节点相连的两条分路各有一个黑色节点(如图)

解释:根节点为两条路径的重合节点,假如它为黑色,可以等效为每条路径都有一个黑色节点

Principle3.调整的关键在于叔叔节点,调整同时也时刻记住平衡高度

那介绍完三个原则后,我们就可以开始着手建一棵红黑树了

红黑树是一棵节点附带颜色的搜索二叉树,先是一棵搜索二叉树,再是节点附带颜色

因此,建造一棵红黑树的整体结构,前面就是先构建一棵搜索二叉树,再调整节点的颜色

假如刚开始没有节点,则直接让新插入节点成为根节点,并让它的颜色为黑色

假如已经有根节点,则按照搜索二叉树的规则进行建树即可

    //假如刚开始没有节点,直接使其成为根即可
	//满足规则,根节点必须为黑色
	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->_left;
		}
		else if (cur->_kv.first < kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}

	//建新节点
	cur = new Node(kv);
	if (parent->_kv.first > kv.first)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}
	cur->_parent = parent;

建树后,就是调整节点的颜色,使其成为一棵红黑树

约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

按照原则3,我们将调整节点颜色分为两种情况

第一是叔叔节点存在且为红色

解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整

 

性质3要求红黑树是不能存在连续的红色节点的,因此我们要对红黑树进行颜色调整,使其成为红

黑树,由于叔叔节点的存在,所以此时是不需要进行旋转进行高度调整的.

如何进行红黑树的颜色调整呢?我们可以将需要调整的树抽离出来进行思考

 

进一步观察我们可以发现上面的模型就是我们原则2中对应的模型,该树违反的规则,只是单纯出

现了连续的红色节点,每条路径的黑色节点数目都是不变的(包括空节点为黑色),所以,我们完全

可以按照原则2,对上面的模型进行等效(uncle,parent变黑,grandparent变红)来解决该树颜色调整

问题

 

但是,这棵树,有可能是整棵树的子树,只是我们抽离出来的一部分

grandparent所对应的节点的父节点,可能是黑色,那就不需要往上调整,但假如是红色,就会出

现连续节点为红色的情况,此时就需要向上继续调整,如下面的例子

我们可以发现17和25又变成了连续红色,需要继续向上调整,模型依旧是原则2所符合的模型

同样按照相同的方法即可完成红黑树的颜色调整

根节点此时为红色,不符合根节点颜色必须为黑色的性质要求,所以还需要把根节点调整回黑色

 

由于根节点是每条路径的公共节点,所以根据原则2,将根节点调整为黑色,与给每条路径增加一

个黑节点是等效的,依旧满足它是一棵红黑树,至此,完成整棵树的颜色调整

 

// 情况1:u存在且为红,变色处理,并继续往上处理
if (uncle && uncle->_col == RED)
{
	//父亲和叔叔节点都调节为黑色
	parent->_col = BLACK;
	uncle->_col = BLACK;
	//爷爷调节为红色
	grandfather->_col = RED;

	//往上调节
	cur = grandfather;
	parent = cur->_parent;
}

第二是叔叔节点不存在或叔叔节点为黑色

解决方式:变色+单旋或双旋

假如叔叔节点不存在,则cur节点一定是新插入的节点,因为假如cur不是新插入的节点,而它又是

一棵红黑树,则p,cur中一定有一个节点为黑色,这时就不满足性质4(每条路径黑色节点个数相同的

要求)

 

此时直接右单旋即可解决,调整下颜色即可.

假如叔叔节点存在且为黑色,那一定是第一种情况变化而来,即cur节点的颜色最开始一定是黑色

因为按照原则1,插入的新节点都为红色,反推可以得到p一定是红色;由于性质3,不能存在连续

的红色节点,所以cur必定为黑色

现在看到的cur的节点为红色,是由情况1变化而来

 

此时对于叔叔节点为黑色的情况,我们对连续的红色节点采取不同的策略,先旋转调整高度(原则

3),再改变颜色

为什么需要旋转?

1.连续的红节点导致最长路径不再是红黑相间,最短路径依旧是全黑

2.每条路径的黑色节点相同

3.叔叔节点为黑色

进而导致了此时最长路径超过最短路径的两倍,违背了红黑树的定义,因此需要旋转

如上图中的最左边路径与最右边路径

旋转又分为两种情况,一种是单旋,另一种是双旋,区分的原则是cur在父亲的左边或者右边

具体来讲,单旋的操作和AVLTree完全类似,不过记得旋转完后,还需要调整对应的颜色

parent的颜色变为黑色,grandparent的颜色变为红色

 

双旋的操作也和AVLTree完全类似,旋转完后,同样需要调整对应的颜色

cur的颜色变为黑色,grandparent的颜色变为红色

 

else // 情况2+3:u不存在/u存在且为黑,旋转+变色
{
	//     g
	//   p   u
	// c
    //右单旋
	if (cur == parent->_left)
	{
		RotateR(grandfather);
		grandfather->_col = RED;
		parent->_col = BLACK;
	}
	else
	{   
	//     g
    //   p   u
    //     c
	//LR双旋
		RotateL(parent);
		RotateR(grandfather);
		cur->_col = BLACK;
		//parent->_col = RED;
		grandfather->_col = RED;
	}

	break;
}

上述全部讨论的是父亲位于爷爷左边的情况,位于右边也是同样进行分析,这里不再赘述,直接放

上插入的源代码

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

		//建新节点
		cur = new Node(kv);
		if (parent->_kv.first > kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;

		//调整节点颜色
		while (parent&& parent->_col == RED)
		{
			//找爷爷
			Node* grandfather = parent->_parent;
			//父亲为爷爷的左节点
			if (grandfather->_left == parent)
			{
				//则叔叔是爷爷的右节点
				Node* uncle = grandfather->_right;
				// 情况1:u存在且为红,变色处理,并继续往上处理
				if (uncle && uncle->_col == RED)
				{
					//父亲和叔叔节点都调节为黑色
					parent->_col = BLACK;
					uncle->_col = BLACK;
					//爷爷调节为红色
					grandfather->_col = RED;

					//往上调节
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况2+3:u不存在/u存在且为黑,旋转+变色
				{
					//     g
					//   p   u
					// c
					//右单旋
					if (cur == parent->_left)
					{
						RotateR(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{   
						//    g
					   //   p   u
					  //      c
						//LR双旋
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						//parent->_col = RED;
						grandfather->_col = RED;
					}

					break;
				}
			}
			//父亲为爷爷的右节点
			else
			{
				//则叔叔是爷爷的左节点
				Node* uncle = grandfather->_left;
				// 情况1:u存在且为红,变色处理,并继续往上处理
				if (uncle && uncle->_col == RED)
				{
					//父亲和叔叔节点都调节为黑色
					parent->_col = BLACK;
					uncle->_col = BLACK;
					//爷爷调节为红色
					grandfather->_col = RED;

					//往上调节
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况2+3:u不存在/u存在且为黑,旋转+变色
				{
					//     g
					//   u   p
					//        c
					//左单旋
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						//     g
					    //   u   p
					    //     c
						//RL双旋
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						//parent->_col = RED;
						grandfather->_col = RED;
					}

					break;
				}
			}
		}

		_root->_col = BLACK;

		return true;
	}

 左右单旋的代码和AVLTree的代码实现相同,这里不再重复说明

//左旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		//b变成30的右
		parent->_right = subRL;
		//父节点也需要调整,但subRL可能为空
		if (subRL)
			subRL->_parent = parent;

		//调整时未必是整棵树的调整,所以还需要考虑parent的链接问题,因此需要先记录ppNode
		Node* ppNode = parent->_parent;

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

		if (ppNode == nullptr)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			//在调整爷爷节点指向的时候,还需要考虑原来parent是爷爷的左还是右
			//subR重新链接回爷爷的左或者右
			if (ppNode->_right == parent)
			{
				ppNode->_right = subR;
			}
			else
			{
				ppNode->_left = subR;
			}

			subR->_parent = ppNode;
		}
	}

	//右旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		//b变成60的左
		parent->_left = subLR;
		//父节点也需要调整,但subRL可能为空
		if (subLR)
			subLR->_parent = parent;

		//调整时未必是整棵树的调整,所以还需要考虑parent的链接问题,因此需要先记录ppNode
		Node* ppNode = parent->_parent;

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

		if (ppNode == nullptr)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			//在调整爷爷节点指向的时候,还需要考虑原来parent是爷爷的左还是右
			//subL重新链接回爷爷的左或者右
			if (ppNode->_right == parent)
			{
				ppNode->_right = subL;
			}
			else
			{
				ppNode->_left = subL;
			}

			subL->_parent = ppNode;
		}
	}

2.3 测试

检验一棵树是否是红黑树,不能按照路径长度来进行判断,最长路径是最短路径的两倍,并不能说

明它是一棵红黑树

而是要从5条性质出发,逐一进行验证

1. 每个结点不是红色就是黑色

2. 根节点是黑色的 

3. 如果一个节点是红色的,则它的两个孩子结点是黑色的 

4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

第一点,不需要检验

第二点,首先直接进行判断根节点是否为黑色

bool IsRBTree()
{
		//假如根节点存在,但颜色不是黑色,则不是红黑树
		if (_root && _root->_col == RED)
		{  
			cout << "根节点颜色是红色" << endl;
			return false;
		}
    ...
}

第三点和第四点,走一个深度遍历,每遇到一个黑色节点,则blacknum进行加一

bool _Check(Node* root, int blackNum, int benchmark)
	{
		//假如到空节点(叶子节点),说明已经走完一条路径,可以开始判断
		if (root == nullptr)
		{   
			//假如统计出的黑色节点个数和参考黑色节点个数不同,则一定不是红黑树
			if (blackNum != benchmark)
			{
				cout << "某条路径黑色节点的数量不相等" << endl;
				return false;
			}

			return true;
		}

		//递归遇到黑色节点时,则blackNum可以加1
		if (root->_col == BLACK)
			blackNum++;

		//假如连续存在两个红色节点,则也不是红黑树,注意还需要判断父节点是否存在
		if (root->_col == RED && root->_parent && root->_parent->_col == RED)
		{
			cout << "存在连续的红色节点" << endl;
			return false;
		}

		//递归判断是否是红黑树,左子树和右子树都为红黑树,则为红黑树
		return _Check(root->_left, blackNum, benchmark)
			&& _Check(root->_right, blackNum, benchmark);
	}

结合上面的代码,即可得到完整的测试红黑树代码

	bool IsRBTree()
	{
		//假如根节点存在,但颜色不是黑色,则不是红黑树
		if (_root && _root->_col == RED)
		{  
			cout << "根节点颜色是红色" << endl;
			return false;
		}

		//随便选一条路径作为黑色节点参考点
		int benchmark = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
				benchmark++;
			cur = cur->_left;
		}

		// 连续红色节点
		return _Check(_root, 0, benchmark);
	}

3.建树测试

void TestRBTree1()
{
	int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	RBTree<int, int> t1;
	for (auto e : a)
	{
		/*	if (e == 14)
			{
			int x = 0;
			}*/

		t1.Insert(make_pair(e, e));
		//cout << e << "插入:" << t1.IsBalance() << endl;
	}

	t1.Inorder();
	cout << t1.IsRBTree() << endl;
}

void Test_RBTree2()
{
	srand(time(0));
	const size_t N = 5000000;
	RBTree<int, int> t;
	for (size_t i = 0; i < N; ++i)
	{
		size_t x = rand() + i;
		t.Insert(make_pair(x, x));
		//cout << t.IsBalance() << endl;
	}

	//t.Inorder();

	cout << t.IsRBTree() << endl;
	cout << t.Height() << endl;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值