C++数据结构重要知识点(3)(红黑树及其插入操作)

1.红黑树和AVL树的区别

红黑树和AVL树都是平衡树,都是为了解决二叉搜索树的劣势。其中,AVL树的左右子树的高度差不超过1,而红黑树的最长路径不超过最短路径的二倍,也就是说,红黑树是一种近似平衡,而AVL树是严格平衡。看起来红黑树要略逊一筹,但实际上两者效率差不多。当我们学习时间复杂度时,就知道n和2n在本质上没有区别,n、2n、3n......它们都是O(N),对于红黑树也是如此,就算最短和最长路径最多可以相差一倍,但对于计算机来说,最短和最长都差不多,它们是在同一个量级的。而红黑树的近似平衡还意味着它旋转的次数会比AVL树少,旋转开销上会少一些。

可以看出,红黑树牺牲了一些平衡,换来了一些旋转优势;AVL树保证平衡,但旋转次数变多了。

map和set底层使用的都是红黑树

2.红黑树的重要性质

红黑树和AVL树本质上都是控制树的高度,但两者的思想差别还是很大的。

红黑树的每个节点都对应有一种颜色,要么是红,要么是黑

(1)根节点是黑色

(2)如果一个结点是红色,那它的两个结点是黑色(不能存在连续的红节点,但黑色节点可以连续存在)

(3)对于每个节点而言,从该节点到其后代叶结点的简单路径上,均包含相同数量的黑色节点(最重要)

(4)每个NIL结点(nullptr结点)都是黑色,叶子数量 != NIL结点标识路径数量,每个叶子有两个NIL结点(了解)

(5)最长路径最多是最短路径的二倍,理论上最短路径全是黑结点,最长路径一黑一红,有最长路径和有最短路径反而意味着树不均衡

(6)最短路径横切可以得到满二叉树,最短logN,最长2 * logN

我们从上面的性质中就能了解到,红黑树控制高度依赖(2)和(3),只要保证没有连续红色结点(最长一黑一红交替,最短全黑,始终满足红黑树要求)和同根下不同路径黑色节点相同,那就一定满足高度条件,这和AVL树的平衡因子思想有较大出入,需要体会,下面我顺着红黑树的实现思路进一步分析

3.insert

定义节点,利用枚举常量来标识不同节点对应的颜色

处理空树

nodeC向下探路,nodeP找到插入节点的父节点

上面这些都和AVL树处理一致,很好理解。

插入节点

插入节点默认都是红节点,要保证从同一个根出发每条路径上的黑色节点数相同,所以要插入红节点。但是这样就又会破坏另一个规则——不能出现连续红节点。所以,接下来的所有操作都是基于黑色节点数相同的条件来解决这个问题的。换句话说,我们只需要关注连续红节点的问题。

4.两种情况的引入

我们可以从一个节点开始,慢慢地加节点,思考可能出现的情况,这是解决复杂难题的一个方式

(1)变色

根据上面插入节点的代码可知,每次插入后nodeC都指向新插入的结点,nodeP是它的父节点,nodeC和nodeP都一定不为空,nodeC一定为红

前两次插入都因为nodeP是黑色,所以跳出了。而第三个结点插入时就出现了连续红节点的情况。如何解决呢,这就是第一种情况——直接变色。

只要nodeG和nodeU存在,nodeU是红色,就可以采取这种解决办法

这样解决有一个问题,就是nodeG从黑色变成了红色,这虽然保证了黑色结点数量不变,但是却可能增加新的麻烦。

可是,难点在于循环条件该怎样写,nodeC、nodeP、nodeG同时跨了三代,要是nodeC、nodeP为空或nodeG为空呢?

首先,第一次循环的时候nodeC、nodeP必定不为空,nodeG可能为空,如果nodeG为空,那么nodeP一定是根节点,一定是黑色,所以第一次循环要么直接通过nodeP为黑跳出循环,要么就存在nodeG。

第二次及以后的循环就有多种情况了。nodeC被赋值为nodeG,一定不为空,如果nodeP为空,说明nodeC是根,在上一次循环结束后变为黑色,变色已经结束,不应该进入循环。如果nodeG为空,和上面一样,说明nodeP一定为黑,也不需要处理了。

综上,只要nodeP和nodeG有一个为空,就不应该进入循环,只需要判断循环后更新的nodeP、nodeG为不为空就可以了。只要进入循环体,就说明nodeC,nodeP、nodeG均不为空

但有一个需要注意的是nodeU可能为空,不过这是要在nodeG不为空的情况下才能讨论的,是在循环体内部讨论的,所以上面nodeU初始化为nullptr

在循环体内先将父节点不为红的处理掉,第一次循环时这个条件是一定不会触发的,你可能回想nodeP为根,但注意nodeP为根时nodeG为空,根本不会进入这个循环,这是将那些变完色后没有触发新问题的情况处理掉。

再确定nodeU的情况

之后进行变色处理,注意根的变色要特殊处理,nodeP和nodeG的更新正常进行即可,只要两者有其一为空就会跳出循环,标志着变色结束。逻辑是严密的。

当nodeU为空时又是另一种情况了。

(2)旋转

变色是尽可能地将连续红色结点的问题解决而不旋转,但在有的情况下就必须旋转了。

下面画一下简图

旋转逻辑和AVL树的一模一样,无论双旋单旋,并且变色逻辑也很简单

nodeU为空和nodeU为黑这两种情况对应的旋转操作一样(nullptr处理有细微区别),但它们出现的情况完全不一样。

当nodeU为空的时候nodeC一定是新增加的结点,因为右子树为空,所有路径都只能有根这一个黑色节点,根据不能存在连续红色结点这条规则,有且仅有nodeC为新增节点才满足。

当nodeU为黑色结点时nodeC一定不为新增结点,而是nodeC = nodeG赋值上来的。仔细看上面的图,会发现我展示的根本不算是红黑树(不同路径黑色节点不同)。如果nodeC是新插入的,那么插入结点所在路径一定不符合红黑树规则

旋转完之后就不用继续向上更新了。原因在于旋转完后的旋转点都是黑色的,无一例外。之前为什么要选择向上更新,是因为变色会导致nodeG变成红色,可能产生新的问题,而旋转就不会,只要旋转完,就代表更新完毕。

注意单双旋影响的结点不同,原因在于双旋的第一次单旋会使得nodeC和nodeP换位,使得单旋是nodeP和nodeG变色,而双旋是nodeC和nodeG变色,其余节点都不受影响。

单旋的实现和AVL树的一样,虽然模型不一样,AVL树的旋转模型对子树的高度有讲究,红黑树没有,但它们本质上要处理的结点一样,要注意的nullptr也一样。如果根要改变,就要特殊处理。

5.总体循环逻辑

红黑树在思想上是通过控制红色节点的连续性来保障高度差的问题的,这和AVL树有很大不同,需要时间接收。

在插入节点的时候,默认增加红节点,这样所有路径的黑色节点数量都不会改变。当插入新节点时,nodeC和nodeP一定不为空,如果出现了nodeC和nodeP都为红色,那就一定存在nodeG,如果这时还有nodeU且为红,就一组一组向上更新,只要nodeP、nodeG为空,就会跳出循环,不用担心越界问题。如果不符合变色规则就旋转,旋转逻辑和AVL树一样,旋转完后直接跳出循环,不再向上调整。

6.所有代码

#pragma once

#include <iostream>
#include <utility>
#include <string>
using namespace std;



namespace my_RBTree
{
	enum Color
	{
		RED,
		BLACK
	};


	template<class K, class V>
	struct RBTreeNode
	{
		RBTreeNode(const pair<K, V>& val, Color col = RED)//默认创建红节点
			:_col(col)
			,_val(val)
			,_left(nullptr)
			,_right(nullptr)
			,_parent(nullptr)
		{}
			
		Color _col;
		pair<K, V> _val;
		RBTreeNode<K, V>* _left;
		RBTreeNode<K, V>* _right;
		RBTreeNode<K, V>* _parent;
	};


	template<class K, class V>
	class RBTree
	{
		typedef RBTreeNode<K, V> Node;
	public:

		void RotateL(Node* const node)
		{
			Node* nodeP = node->_parent, * nodeR = node->_right, * nodeRL = nodeR->_left;
			node->_right = nodeRL, node->_parent = nodeR;
			nodeR->_left = node, nodeR->_parent = nodeP;

			if (nodeRL)
				nodeRL->_parent = node;
			
			if (nodeP)
			{
				if (nodeP->_left == node)
					nodeP->_left = nodeR;
				else
					nodeP->_right = nodeR;
			}

			if (node == _root)
				_root = nodeR;

		}

		void RotateR(Node* const node)
		{
			Node* nodeP = node->_parent, * nodeL = node->_left, * nodeLR = nodeL->_right;
			node->_left = nodeLR, node->_parent = nodeL;
			nodeL->_right = node, nodeL->_parent = nodeP;

			if (nodeLR)
				nodeLR->_parent = node;

			if (nodeP)
			{
				if (nodeP->_left == node)
					nodeP->_left = nodeL;
				else
					nodeP->_right = nodeL;
			}

			if (node == _root)
				_root = nodeL;

		}

		bool insert(const pair<K, V>& val)
		{
			if (_root == nullptr)
			{
				_root = new Node(val, BLACK);
				return true;
			}

			Node* nodeP = nullptr, * nodeC = _root;
			while (nodeC)
			{
				if (nodeC->_val.first == val.first)
				{
					cout << "插入失败,树中已有该key!" << endl;
					return false;//去重
				}
				if (nodeC->_val.first < val.first)
					nodeP = nodeC, nodeC = nodeC->_right;
				else
					nodeP = nodeC, nodeC = nodeC->_left;
			}

			if (nodeP->_val.first < val.first)
				nodeP->_right = new Node(val), nodeC = nodeP->_right, nodeC->_parent = nodeP;
			else
				nodeP->_left = new Node(val), nodeC = nodeP->_left, nodeC->_parent = nodeP;

			Node* nodeG = nodeP->_parent, * nodeU = nullptr;
			while (nodeP && nodeG)//只有nodeP和nodeG都存在才向上走
			{
				if (nodeP->_col != RED)//处理父亲不为红节点的情况
					break;

				if (nodeG->_left == nodeP)
					nodeU = nodeG->_right;//nodeU可能为空
				else
					nodeU = nodeG->_left;//nodeU可能为空

				//nodeU不为空
				if (nodeU && nodeP->_col == RED && nodeU->_col == RED)//这个时候nodeG一定为黑
				{
					nodeU->_col = nodeP->_col = BLACK;

					if (nodeG == _root)//变色,根节点不变色
						nodeG->_col = BLACK;
					else
						nodeG->_col = RED;

					nodeC = nodeG, nodeP = nodeC->_parent;
					if (nodeP)
						nodeG = nodeP->_parent;//nodeG为空会退出循环
					else
						nodeG = nullptr;//没有nodeP会退出循环
				}

				//nodeU为空
				else
				{
					if (nodeG->_left == nodeP)
					{
						if (nodeC == nodeP->_left)
						{
							RotateR(nodeG);//右单旋
							nodeG->_col = RED, nodeP->_col = BLACK;
						}
						else//左右双旋
						{
							RotateL(nodeP);
							RotateR(nodeG);
							nodeC->_col = BLACK, nodeG->_col = RED;
						}
					}
					else
					{
						if (nodeC == nodeP->_right)
						{
							RotateL(nodeG);//左单旋
							nodeP->_col = BLACK, nodeG->_col = RED;
						}
						else//右左双旋
						{
							RotateR(nodeP);
							RotateL(nodeG);
							nodeC->_col = BLACK, nodeG->_col = RED;
						}
					}

					break;
				}
			}

			cout << "该key-value插入成功" << endl;
			return true;
		}



		bool Check()
		{
			if (_root->_col == RED)//根节点为红
				return false;

			return _Check1(_root) && _Check2(_root, num_of_black(_root), 0);
		}


		void InOrder()
		{
			_InOrder(_root);
		}

	private:

		bool _Check1(Node* cur)//是否有连续红节点
		{
			if (cur == nullptr)
				return true;

			if (cur->_col == RED)
			{
				if (cur->_parent->_col == RED)
					return false;
			}

			return _Check1(cur->_left) && _Check1(cur->_right);
		}

		int num_of_black(Node* root)//计算一条路径黑色节点数量
		{
			if (root == nullptr)
				return 0;
			return (root->_col == BLACK ? 1 : 0) + num_of_black(root->_left);
		}

		bool _Check2(Node* cur, int num, int count)
		{
			if (cur == nullptr)
			{
				if (count == num)
					return true;
				return false;
			}

			if (cur->_col == BLACK)
				count++;
			
			return _Check2(cur->_left, num, count) && _Check2(cur->_right, num, count);
		}

		void _InOrder(Node* cur)
		{
			if (cur == nullptr)
				return;
			_InOrder(cur->_left);
			cout << cur->_val.first << " : " << cur->_val.second << endl;
			_InOrder(cur->_right);
		}

		
	private:
		Node* _root = nullptr;
	};


}

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值