【C++庖丁解牛】高阶数据结构---红黑树详解(万字超详细全面介绍红黑树)

🍁你好,我是 RO-BERRY
📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识
🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油

在这里插入图片描述


目录

  • 前言
  • 1.红黑树的概念
  • 2.红黑树的性质
  • 3.已经学了BST和AVL树,为啥还要学红黑树
  • 4.红黑树节点的定义
  • 5. 红黑树旋转
    • 5.1 左旋示例
    • 5.2 右旋示例
    • 5.3 代码实现
  • 6.红黑树插入
    • 情况一:父节点为空
    • 情况二:父节点是黑色
    • 情况三:父节点是红色,叔叔也为红色。
    • 情况四:父节点是红色,叔叔为黑色。
    • 插入小结
  • 7. 删除
    • 7.1 删除节点仅存在一个孩子
    • 7.2删除节点不存在孩子
    • 删除小结
  • 8.红黑树的验证
  • 9. 红黑树的查找
  • 9. 红黑树与AVL树的比较
  • 10.map底层为什么用红黑树实现
    • 红黑树:
    • .平衡二叉树(AVL树):
  • 11. 源码
    • 11.1 RBTree.h
    • 11.2 Test.cpp


前言

这篇文章我们再来学习一种平衡搜索二叉树——红黑树
如果要说常见的数据结构里,哪个数据结构最麻烦、最难以掌握?
绝对非红黑树莫属了,如果只是自己看的话很多人可能看很多遍都不太懂红黑树。
红黑树和AVL树都是常见的自平衡二叉搜索树,它们都可以用于高效地支持插入、删除和查找等操作。虽然它们都能够保持树的平衡性,但在不同的应用场景下,红黑树和AVL树有各自的优势和适用性。


1.红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍(最长路径也不会超出最短路径的两倍,因此红黑树的平衡性要求相对宽松,没有AVL树那样严格),从而使搜索树达到一种相对平衡的状态。

在这里插入图片描述


2.红黑树的性质

红黑树是特殊的二叉查找树,又名R-B树(RED-BLACK-TREE),由于红黑树是特殊的二叉查找树,即红黑树具有了二叉查找树的特性,而且红黑树还具有以下特性:

  1. 每个节点要么是黑色要么是红色
  2. 根节点是黑色
  3. 每个叶子节点是黑色,并且为空节点(还有另外一种说法就是,每个叶子结点都带有两个空的黑色结点(被称为黑哨兵),如果一个结点n的只有一个左孩子,那么n的右孩子是一个黑哨兵;如果结点n只有一个右孩子,那么n的左孩子是一个黑哨兵。)
  4. 每个红色节点必须有两个黑色的子节点(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  5. 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
  6. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
  7. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

有几点需要注意的是:

  1. 特性3中指定红黑树的每个叶子节点都是空节点,但是在Java实现中红黑树将使用null代表空节点,因此遍历红黑树时看不到黑色的叶子节点,反而见到的叶子节点是红色的
  2. 特性4保证了从根节点到叶子节点的最长路径的长度不会超过任何其他路径的两倍,例如黑色高度为3的红黑树,其最短路径(路径指的是根节点到叶子节点)是2(黑节点-黑节点-黑节点),其最长路径为4(黑节点-红节点-黑节点-红节点-黑节点)。
    在这里插入图片描述
    思考:为什么满足上面的性质,红黑树就能保证:其最长路径中结点个数不会超过最短路径结点个数的两倍?(其实不带第4条就可以,加不加第4条都不会影响每条路径黑色结点数量是否相等)

那通过上面的性质我们可以得知,红黑树中的结点要么是黑色,要么是红色,这没什么可说的;然后要求根结点一定是黑色的,红色结点不能连续出现,如果出现了红色结点,那它的子结点必须是黑色的,然后还要求每条路径黑色结点的数量必须相等。 那这样其实就可以保证一棵红黑树中最长路径不超高最短路径的两倍。 当然实际中不同的红黑树情况是不一样的,所以我们这里来分析一种极端的情况: 大家想,如果一棵红黑树有红有黑,它里面如果有一条全黑的路径,那这条全黑的路径一定就是最短路径; 如果有一条是一黑一红,一黑一红…,这样黑红相间的,那他就是最长的路径。 然后它们里面的黑色结点个数又是相同的的,所以最长路径最多是最短路径的两倍,不可能超过最短路径两倍。 所以这样红黑树的高度就能够保持在一个相对平衡的范围内,当然他就没有AVL树那么严格。 比如这样的

在这里插入图片描述
那另外:

其实分析上面的性质,红黑树是可以全黑的,全部黑色结点,只要满足上面的性质即可。
然后大家思考一个问题,上面第四条性质——每个叶子结点都是黑色的(此处的叶子结点指的是空结点,也被称为NIL节点),有什么用?

那大家先算一下这个红黑树有多少条路径?
在这里插入图片描述
如果不带空的话,我们可能会认为有5条,但是这里计算路径其实应该走到空(NIL),所以正确的应该是有11条路径。 所有我们可以认为这条规则就是为了更好的帮我们区分不同路径的。

然后再补充一个概念:

结点的黑高(黑色高度):从某结点出发(不含该结点)到达任一空叶结点的路径上经过的黑结点总数
在这里插入图片描述


3.已经学了BST和AVL树,为啥还要学红黑树

然后我们再来分析一个问题:

大家想,对于一棵红黑树来说,如果它里面全部的黑色结点一共有N个的话,那它的最短路径长度就差不多是 l o g 2 ( N ) log_2 (N) log2(N)。 然后整棵树的节点数量就是在【N,2N】之间。 所以最长路径长度就可以认为差不多是2 l o g 2 ( N ) log_2 (N) log2(N) 所以红黑树的查找最少是 l o g 2 ( N ) log_2 (N) log2(N)次,最多是2 l o g 2 ( N ) log_2 (N) log2(N)次,所以红黑树查找的时间复杂度是O( l o g 2 N log_2 N log2N),计算时间复杂度前面的常数项可以省略嘛。 而AVL树也是O( l o g 2 N log_2 N log2N),但AVL树是比较严格的O( l o g 2 N log_2 N log2N),而红黑树是省略了常数项。 所以严格来说,红黑树的查找效率是比不上AVL树的(但对于计算机来说是没什么差别的),但是它们是同一个数量级的,都是O( l o g 2 N log_2 N log2N)。

那既然好像都差不多,为什么我们已经学了AVL树了,还要学红黑树呢?红黑树有什么优势吗?

由于AVL树要求更加严格的平衡,所以在进行插入和删除操作时,可能需要更频繁地进行旋转操作来调整树的结构,以保持平衡。相比之下,红黑树的插入和删除操作需要旋转的次数相对较少,因此在插入和删除操作频繁的情况下,红黑树可能更加高效。 这个如果大家学完这篇文章或许能更好的理解。

所以综合而言:

红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。
红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。
当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。

相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。
在最坏的情况下也可以保证O(logN)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)。

红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高,所以在插入和删除中所做的后期维护操作肯定会比红黑树要耗时好多,
但是他们的查找效率都是O(logN),所以红黑树应用还是高于AVL树的. 实际上插入 AVL 树和红黑树的速度取决于你所插入的数据。
如果你的数据分布较好,则比较宜于采用 AVL树(例如随机产生系列数),但是如果你想处理比较杂乱的情况,则红黑树是比较快的。


4.红黑树节点的定义

// 节点的颜色
enum Color{RED, BLACK};
// 红黑树节点的定义
template<class ValueType>
struct RBTreeNode
{
	RBTreeNode(const ValueType& data = ValueType(),Color color = RED)
    	: _pLeft(nullptr)
    	,_pRight(nullptr)
    	, _pParent(nullptr)
    	, _data(data)
    	, _color(color)
		{}
	RBTreeNode<ValueType>* _pLeft;   // 节点的左孩子
	RBTreeNode<ValueType>* _pRight;  // 节点的右孩子
	RBTreeNode<ValueType>* _pParent; // 节点的双亲(红黑树需要旋转,为了实现简单给出该字段)
	ValueType _data;            // 节点的值域
	Color _color;               // 节点的颜色
};

思考:在节点的定义中,为什么要将节点的默认颜色给成红色的?

我们来分析一下如果我们插入一个新结点给的是黑色,那它一定会违反上面提到的红黑树的第5条性质——每条路径上黑色结点的数量一致。如图:

在这里插入图片描述
因为原来每条路径黑色结点数量是相同的,现在新插入一个黑色结点的话,那插入的这条路径会增加一个黑色结点,但是其它路径数量不变,所以其它所有的路径黑色结点数量都和这一条不相等,这样就比较麻烦了。 那如果我们插入结点默认给红色呢?会违反规则吗? 那红色的话其实要看具体情况了: 如果插入一个红色结点,但是它的父亲也是红色,那就出事了,因为红黑树里面不能出现连续的红色结点,那这种情况就需要调整了。 但如果它的父亲是黑色,那就没问题,不需要进行什么处理。

所以我们新插入的结点给成红色,当然如果是第一次插入根结点我们可以直接给黑色,因为红黑树要求根结点必须是黑色。

5. 红黑树旋转

在分析插入和删除操作前,我们需要先说明一下旋转操作,这个操作在后续操作中都会用得到。旋转操作分为左旋和右旋,左旋是将某个节点旋转为其右孩子的左孩子,而右旋是节点旋转为其左孩子的右孩子。
在这里插入图片描述

5.1 左旋示例

在这里插入图片描述

5.2 右旋示例

在这里插入图片描述

5.3 代码实现

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

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		subR->_left = parent;
		Node* ppnode = parent->_parent;
		parent->_parent = subR;

		if (parent == _root)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = subR;
			}
			else
			{
				ppnode->_right = subR;
			}
			subR->_parent = ppnode;
		}
	}

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

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		subL->_right = parent;

		Node* ppnode = parent->_parent;
		parent->_parent = subL;

		if (parent == _root)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = subL;
			}
			else
			{
				ppnode->_right = subL;
			}
			subL->_parent = ppnode;
		}
	}

6.红黑树插入

红黑树的插入过程和二叉查找树插入过程基本类似,不同的地方在于,红黑树插入新节点后,需要进行调整,以满足红黑树的性质。性质1规定红黑树节点的颜色要么是红色要么是黑色,那么在插入新节点时,这个节点应该是红色还是黑色呢?

我都想不说答案,大家直接看性质5 (从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。),你插个黑的是不是必然会影响是不是就要调整了,是不是必然就是红的了,对的吧,红的出现两红连续了,可能才需要调整,对的吧,所以插入的节点应该是红色的。

插入我们可能感觉很迷茫,不知道如何下手,这玩意这可咋插入呀,一想到就头大就逃避,哈哈哈,凡复杂事情,其实按情况去划分,把每种情况都克服了,整体也就克服了。我们站在待插入节点的角度,去看我要往哪个父节点下插入,那么看父节点的话,那插入的几种情况我们就来看看哈,我们这里待插入的节点为N节点:

情况一:父节点为空

当父节点为空,也就是到根节点了,因为性质2所以将节点 N 直接变黑作为根节点,即可。
在这里插入图片描述

情况二:父节点是黑色

当父节点是黑色,这种情况下,性质4和性质5没有受到影响,不需要调整。
在这里插入图片描述

情况三:父节点是红色,叔叔也为红色。

待插入节点N的父节点为红色,叔叔也是红色的情况时,性质4就会被打破,此时需要进行调整。这种情况下,先将 父节点 和 叔叔节点 染成黑色,再将 爷爷节点 的颜色染成红色。此时经过爷爷的路径上的黑色节点数量不变(想想是不是),性质5仍然满足。但需要注意的是 G 被染成红色后,可能会和它的父节点形成连续的红色节点,此时需要递归向上调整。

在这里插入图片描述
当然此情况下的这些情况都属于情况三哈。
在这里插入图片描述

情况四:父节点是红色,叔叔为黑色。

这种情况跟情况三的不同关键点就在于叔叔的节点颜色,情况三叔叔是红色,而情况四叔叔是黑色的哈。我们的角度从父节点的左右以及待插入节点是父节点的左右可以分成细拆成4种情况,我们来看下这四种情况的处理:
(1)父节点为左子树,待插入节点为父节点的左子树
在这里插入图片描述
(2)父节点为左子树,待插入节点为父节点的右子树
在这里插入图片描述
小记:当父节点为左子树时,待插入的节点都要往左靠(这个理解不,你看上图待插入N在父节点P的右边时,是不是先左旋了一下,都靠左边了,然后跟待插入节点在左子树时保持都靠左边啦)然后父节点和爷爷换色,然后右旋的。

(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 (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为空,这里就是要插入的位置
		cur = new Node(kv); // 红色的
		//连接
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		//链接父亲
		cur->_parent = parent;
		//如果插入以后它的parent是红色的,就需要处理
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			//如果父亲是爷爷的左孩子,那右孩子就是叔叔
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				// 情况一:叔叔存在且为红
				if (uncle && uncle->_col == RED)
				{
					//变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续往上处理
					//更新cur为grandfather,判断它的父亲是什么情况
					//1.如果不存在或者为黑,就需要继续处理,也不会进行循环
					//2.如果它的父亲存在且为红,重新循环进行调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else   
				{
					// 情况二:叔叔不存在或者存在且为黑
					// 旋转+变色
					if (cur == parent->_left)
					{
						//       g
						//    p    u
						// c
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//       g
						//    p     u
						//      c
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break;
				} 
			}
			else
			{
				Node* uncle = grandfather->_left;
				// 情况一:叔叔存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续往上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					// 情况二:叔叔不存在或者存在且为黑
					// 旋转+变色
					//      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;
	}

7. 删除

相较于插入操作,红黑树的删除操作则要更为复杂一些。删除操作首先要确定待删除节点有几个孩子,如果有孩子,不能直接删除该节点。我们看上边插入的时候看的是叔叔节点的眼色,而删除是看的待删除节点的兄弟节点的颜色。还是一样我们站在待删除节点的角度,我们可以从如下两个方面去考虑:

  • 节点的颜色,节点有红黑两种可能,那么删除黑节点就会造成节点失衡。
  • 节点的构造,有三种可能:其一是删除节点的孩子都为null,其二是删除节点的一个孩子节点为null,其三是删除节点的两个孩子节点都不为null。而如果两个节点都不为null,那么我们就需要找替代节点(前驱或者后继结点)来替代删除,而删除替代结点就会是情况一和情况二。

删除的第三种情况大家应该知道吧,跟我们二叉排序树删除是不是要找一个前驱或者后继节点来替换,然后将前驱或者后继节点的值赋值给待删除的节点,然后删除前驱或者后继节点。而前驱或者后继结点的判断可以使用下图的方式,将节点Key落入X轴中(实质是中序排序)其后一个结点就是后继节点,前一个就是前驱节点,那么我们看一下后继节点替代删除的方案,比如删除-1,那么可以将0替换到-1的位置,然后删除0,这样就回到情况一, 再比如删除4,那么使用5来代替,然后删除5,这样就回到了情况二。

在这里插入图片描述

由于前驱和后继至多只有一个孩子节点,这样我们就把原来要删除的节点有两个孩子的问题转化为只有一个孩子节点的问题,问题被简化了一些。这个大家理解的吧,不理解的话建议大家先去看一下二叉排序树的东西,比如二叉排序树的删除,再来这里看红黑树就好理解一点了。

那么针对红黑树的删除,我们这里可分为三种情形:

  1. 删除的节点有一个孩子节点,删除节点只能是黑色,其子节点也必为红色,此时用删除节点的子节点接到父节点,且将子节点颜色涂黑,保证黑色数量。

在这里插入图片描述

  1. 删除的节点没有孩子节点,如果是红色还好直接删除即可,如果是黑色就需要考虑平衡了

在这里插入图片描述

在这里插入图片描述

  1. 删除的节点有两个孩子节点,与二叉排序树一样,使用前驱或者后继节点作为替换的删除节点,场景转至为1或2处理。

在这里插入图片描述
接下来我们就仔细分析下情形一和情形二。

7.1 删除节点仅存在一个孩子

删除节点存在一个孩子,那么此时,不需要考虑节点的颜色,因为该节点必然为黑色 (因为性质4,红下必有俩黑或者俩null节点,而我们这里的情况是只有一个孩子,所以必为黑),且孩子节点必然为红(如果为黑,那么D节点就会有一方黑色节点比另一方多一,这个理解么?因为我们说过红黑树的任一黑节点为根的树也必是一颗红黑树)。那么情况一的所有情况如下:

在这里插入图片描述
这种情况处理较为简单,只需要将节点P的指针指向DL或者DR,然后将DL或者DR的颜色变更为黑色, 就保证了红黑树的平衡,如下:

在这里插入图片描述
上述情况需要注意当D是根节点时,删除操作之后,孩子节点会成为新的根节点

7.2删除节点不存在孩子

删除节点不存在孩子,那么此时就需要考虑节点的颜色。

  • 删除节点为红色
    如果为红色,因为性质4那么父节点肯定为黑色,那么直接删除即可哈,P节点断掉指向节点D的指针指向就好了。

在这里插入图片描述

  • 删除节点为黑色
    我们考虑黑色,这种情况较为复杂,因为黑色节点被删除之后,红黑树会失去平衡,此时需要调整平衡。那么可能的情况有如下几种(如果D为根节点,删除操作后,红黑树变为空树即可,下面以非根节点的情况为例来分析)

    • 父节点为红色,兄弟节点不存在孩子
      父节点为红色,兄弟节点不存在孩子(兄弟节点必然存在,且为黑色),这种情况稍微好处理,因为将父节点变为黑色,兄弟节点变为红色,即可保证红黑树平衡。

在这里插入图片描述

    • 父节点为红色,兄弟节点存在孩子
      父节点为红色,兄弟节点存在孩子,此时无法像情况 3.3.2.1 那么处理,因为兄弟节点存在的孩子必然为红色(理解么?因为我们说了要删除的节点D没有孩子,父亲是红,兄弟就必是黑节点,黑节点下如果还有黑节点,是不是父节点为根的子树就不是红黑树了,对不对),那么此时就需要进行旋转。

在这里插入图片描述

    • 父节点为黑色
      当兄弟结点不存在孩子节点,此时无法通过旋转来实现平衡,即P节点无法继续调整,那么就B设置为 红色,保证P这个子树平衡,然后通过P节点,向上递归维持平衡。

比如P的父亲是红色的话:

在这里插入图片描述

删除小结

(1)删除节点之后,看看这个节点是不是黑色的叶子节点,如果不是,简单处理就可以达到平衡了;

(2)先看N是不是根节点,是的话啥都不用管;不是的话看兄弟什么颜色:

  • 兄弟是红色:进行旋转涂色,去到兄弟为黑色那里处理

  • 兄弟是黑色,看看兄弟子节点是不是全部都是黑。

  • 全黑的话,看父节点什么颜色进行对应处理;

  • 不全黑,看兄在的位置,兄在左的话,看兄的左子是不是红色,进行对应处理;兄在右的话,看兄的右子是不是红色,进行对应处理。

8.红黑树的验证

验证其是否平衡且满足红黑树性质
那如何判断它是否满足是一棵红黑树呢?

其实就是去检查那几条规则(性质):

首先结点颜色要么是黑色,要么是红色,这没什么好检查的。 然后根结点必须是黑色,这个可以检查一下,如果根结点不是黑色(是红色)直接就不符合了

	bool IsBalance()
	{
		if (_root && _root->_col == RED)
			return false;

		return Check(_root);
	}

然后如果出现连续的红色结点,那也不符合。 那怎么检查有没有出现连续红色结点呢? 我们可以去遍历这棵树,然后遇到红色结点判断它的孩子是不是红色结点,如果存在红色结点它的孩子也是红色,那就不符合。 这样确实可以,但是不太好,因为孩子的话要检查两个,而且结点的孩子有还有可能不存在,为空,那还得再加一个判断。 所以我们可以这样做:遍历遇到红色结点我们去看它的父亲,如果它的父亲也为红色那就不行。 而判断父亲的话,只有根结点没有父亲,但是根结点是黑色的也不会去检查它。 所以这样写会好一点

	bool Check(Node* cur)
	{
		if (cur == nullptr)
			return true;

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

	bool IsBalance()
	{
		if (_root && _root->_col == RED)
			return false;

		return Check(_root);
	}

然后还剩一个,我们要检查每条路径黑色结点数量是否相等,存在不相等的情况就不符合。 那这个要如何检查呢? ,我们可以先求出一条路径的黑色结点数量,把它作为基准值,然后再递归求出每条路径的黑色结点数量和它比较,如果存在不相等的情况,就不符合。

	bool Check(Node* cur, int blackNum, int refBlackNum)
	{
		if (cur == nullptr)
		{
			//走到空就是一条路径走完了,比较一下是否相等
			if (refBlackNum != blackNum)
			{
				cout << "黑色节点的数量不相等" << endl;
				return false;
			}

			//cout << blackNum << endl;
			return true;
		}

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

		if (cur->_col == BLACK)
			++blackNum;
		
		return Check(cur->_left, blackNum, refBlackNum)
			&& Check(cur->_right, blackNum, refBlackNum);
	}

	bool IsBalance()
	{
		if (_root && _root->_col == RED)
			return false;
		
		//先求出一条路径黑色结点数量
		int refBlackNum = 0;
		Node* cur = _root;
		while (cur)
		{
			if(cur->_col == BLACK)
				refBlackNum++;

			cur = cur->_left;
		}
		//检查是否出现连续红色结点及所有路径黑色结点数量是否相等
		return Check(_root, 0, refBlackNum);
	}

9. 红黑树的查找

那红黑树的查找就也和搜索二叉树的查找一样,之前讲过,这里就不再说了。

9. 红黑树与AVL树的比较

红黑树和AVL树都是高效的自平衡搜索二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N)。 红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,所以AVL树的插入和删除操作比红黑树更耗时。 因为AVL树在插入和删除节点后,会进行更多的旋转操作以维持一个较为严格的平衡,所以插入和删除操作的时间复杂度更高。 相对而言,红黑树对平衡的控制比较宽松,降低了插入删除时需要旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。 在实际应用中,红黑树的使用更广泛。许多编程语言和库都使用红黑树作为基础数据结构,例如C++ STL中的std::map和std::set就是基于 红黑树实现的。

10.map底层为什么用红黑树实现

红黑树:

红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。
通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,
因此,红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。

性质:

  1. 每个节点非红即黑
  2. 根节点是黑的;
  3. 每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的;
  4. 如果一个节点是红色的,则它的子节点必须是黑色的。
  5. 对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点;

.平衡二叉树(AVL树):

红黑树是在AVL树的基础上提出来的。

平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。

AVL树中所有结点为根的树的左右子树高度之差的绝对值不超过1。

将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。

11. 源码

11.1 RBTree.h

#pragma onceenum Colour
{
    RED,
    BLACK,
};
​
template <class T>
struct RBTreeNode
{
    RBTreeNode<T>* _parent;
    RBTreeNode<T>* _left;
    RBTreeNode<T>* _right;
    T _data;
    Colour _col;RBTreeNode(const T& data)
        :_parent(nullptr)
        , _left(nullptr)
        , _right(nullptr)
        , _data(data)
        , _col(RED)
    {}};
​
template <class T>
class RBTree
{
    typedef RBTreeNode<T> Node;
public:
    bool Insert(const T& data)
    {
        if (_root == nullptr)
        {
            _root = new Node(data);
            _root->_col = BLACK;
            return true;
        }
        Node* parent = nullptr;
        Node* cur = _root;
        while (cur)
        {
            if (data < cur->_data)
            {
                parent = cur;
                cur = cur->_left;
            }
            else if (data > cur->_data)
            {
                parent = cur;
                cur = cur->_right;
            }
            else
            {
                return false;
            }
        }
        //走到这里cur为空,就是key应该插入的位置
        cur = new Node(data);
        //链接
        if (data < parent->_data)
            parent->_left = cur;
        if (data > parent->_data)
            parent->_right = cur;//链接父亲指针
        cur->_parent = parent;//如果插入之后它的parent是红的,就需要进行调整
        while (parent && parent->_col == RED)
        {
            Node* grandfather = parent->_parent;
            //如果父亲是祖父的左孩子,那右孩子就是叔叔
            if (parent == grandfather->_left)
            {
                Node* uncle = grandfather->_right;
                //这里处理的情况是叔叔存在且为红,变色+向上继续处理
                if (uncle && uncle->_col == RED)
                {
                    //将p,u改为黑,g改为红
                    parent->_col = BLACK;
                    uncle->_col = BLACK;
                    grandfather->_col = RED;//更新cur为grandfather,判断它的父亲是什么情况:
                    //1.如果不存在或者为黑,就需要继续处理了,也不会进行循环了
                    //2.如果它的父亲存在且为红,重新循环进行调整
                    cur = grandfather;
                    parent = cur->_parent;
                }
                else//叔叔不存在/叔叔存在且为黑的情况
                {
                    //     g
                    //   p   u
                    // c 
                    if (cur == parent->_left)//左左——右单旋+变色
                    {
                        RotateR(grandfather);
                        parent->_col = BLACK;
                        grandfather->_col = RED;
                    }
                    else//左右——左右双旋+变色
                    {
                        //     g
                        //   p   u
                        //     c
                        RotateL(parent);
                        RotateR(grandfather);
                        cur->_col = BLACK;
                        grandfather->_col = RED;
                    }
                    //这里调整完就可以break了,因为颜色都变的合适了,而相关的链接关系旋转会帮我们处理
                    break;
                }
            }
            //如果父亲是祖父的右孩子,那左孩子就是叔叔
            else //parent = grandfather->_right
            {
                Node* uncle = grandfather->_left;
                //这里处理的情况是叔叔存在且为红
                if (uncle && uncle->_col == RED)
                {
                    //将p,u改为黑,g改为红
                    parent->_col = BLACK;
                    uncle->_col = BLACK;
                    grandfather->_col = RED;//更新cur为grandfather,判断它的父亲是什么情况:
                    //1.如果不存在或者为黑,就需要继续处理了,也不会进行循环了
                    //2.如果它的父亲存在且为红,重新循环进行调整
                    cur = grandfather;
                    parent = cur->_parent;
                }
                else
                {
                    if (cur == parent->_right)//右右——左单旋+变色
                    {
                    //    g
                    //  u   p
                    //        c
                        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;
    }
    void InOrder()
    {
        _InOrder(_root);
        cout << endl;
    }
    bool IsBlance()
    {
        if (_root && _root->_col == RED)
        {
            cout << "根结点是红色" << endl;
            return false;
        }//先求出一条路径黑色结点数量
        int mark = 0;
        Node* cur = _root;
        while (cur)
        {
            if (cur->_col == BLACK)
                ++mark;
            cur = cur->_left;
        }//检查是否出现连续红色结点及所有路径黑色结点数量是否相等
        return _Check(_root, 0, mark);
    }
    int TreeHeight()
    {
        return _TreeHeight(_root);
    }
private:
    int _TreeHeight(Node* root)
    {
        if (root == nullptr)
            return 0;
        int RightH = _TreeHeight(root->_left);
        int leftH = _TreeHeight(root->_right);
        return RightH > leftH ? RightH + 1 : leftH + 1;
    }
    bool _Check(Node* root, int blackNum, int mark)
    {
        if (root == nullptr)
        {
            //走到空就是一条路径走完了,比较一下是否相等
            if (blackNum != mark)
            {
                cout << "存在黑色结点数量不相等" << endl;
                return false;
            }
            return true;
        }if (root->_col == BLACK)
            ++blackNum;if (root->_col == RED && root->_parent->_col == RED)
        {
            cout << "出现连续红色结点" << endl;
            return false;
        }return _Check(root->_left, blackNum, mark)
            && _Check(root->_left, blackNum, mark);
    }
    void _InOrder(Node* root)
    {
        if (root == nullptr)
        {
            return;
        }
        _InOrder(root->_left);
        cout << root->_data << " ";
        _InOrder(root->_right);
    }
    //左单旋
    void RotateL(Node* parent)
    {
        Node* subR = parent->_right;
        Node* subRL = subR->_left;//旋转并更新_parent指针
        parent->_right = subRL;
        if (subRL)
            subRL->_parent = parent;//先保存一下parent->_parent,因为下面会改它
        Node* pparent = parent->_parent;//旋转并更新_parent指针
        subR->_left = parent;
        parent->_parent = subR;//若pparent为空则证明旋转的是一整棵树,因为根结点的_parent为空
        if (pparent == nullptr)
        {
            //subR是新的根
            _root = subR;
            _root->_parent = nullptr;
        }
        //若pparent不为空,则证明旋转的是子树,parent上面还有结点
        else
        {
            //让pparent指向子树旋转之后新的根
            if (pparent->_left == parent)
            {
                pparent->_left = subR;
            }
            else
            {
                pparent->_right = subR;
            }
            //同时也让新的根指向pparent
            subR->_parent = pparent;
        }
    }//右单旋
    void RotateR(Node* parent)
    {
        Node* subL = parent->_left;
        Node* subLR = subL->_right;//旋转并更新_parent指针
        parent->_left = subLR;
        if (subLR)
            subLR->_parent = parent;//先保存一下parent->_parent,因为下面会改它
        Node* pparent = parent->_parent;//旋转并更新_parent指针
        subL->_right = parent;
        parent->_parent = subL;//若parent等于_root则证明旋转的是一整棵树(这也是一种判断方法)
        if (parent == _root)
        {
            _root = subL;
            _root->_parent = nullptr;
        }
        else
        {
            //让pparent指向子树旋转之后新的根
            if (parent == pparent->_left)
            {
                pparent->_left = subL;
            }
            else
            {
                pparent->_right = subL;
            }
            //同时也让新的根指向pparent
            subL->_parent = pparent;
        }
    }
​
private:
    Node* _root = nullptr;
};

11.2 Test.cpp

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include <time.h>
#include "RBTree.h"
void RBTest1()
{
    //int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
    //int arr[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
    int arr[] = { 95,47,32,29,7,7,2,50,74,30 };
​
    RBTree<int> t1;
    for (auto e : arr)
    {
        t1.Insert(e);
    }
    t1.InOrder();
​
    cout << t1.IsBlance() << endl;}void RBTest2()
{
    srand((unsigned int)time(nullptr));
    const int N = 100000;
    RBTree<int> t;
    for (int i = 0; i < N; ++i)
    {
        int x = rand() + i;
        t.Insert(x);
    }
    cout << t.TreeHeight() << endl;
​
    AVLTree<int, int> t2;
    for (int i = 0; i < N; ++i)
    {
        int x = rand() + i;
        t2.Insert(make_pair(x, x));
    }
    cout << t2.TreeHeight() << endl;}
int main()
{
    RBTest2();
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/510563.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

前端学习<三>CSS进阶——0102-CSS布局样式

前言 css 进阶的主要内容如下。 1、css 非布局样式 html 元素的分类和特性 css 选择器 css 常见属性&#xff08;非布局样式&#xff09; 2、css 布局相关 css 布局属性和组合解析 常见布局方案 三栏布局案例 3、动画和效果 属于 css 中最出彩的内容。 多背景多投影特…

Cookie 和 Session

1. 回顾 Cookie是浏览器在本地持久化存储结构的一种机制. 1.1 Cookie的数据从哪里来? 服务器返回给浏览器的. 1.2 Cookie的数据是什么样的? Cookie的数据是键值对结构. 并且这里的键值对都是程序员自定义的. 1.3 Cookie的作用是什么? Cookie可以在浏览器这边存储一些…

Linux网络编程一(协议、TCP协议、UDP、socket编程、TCP服务器端及客户端)

文章目录 协议1、分层模型结构2、网络应用程序设计模式3、ARP协议4、IP协议5、UDP协议6、TCP协议 Socket编程1、网络套接字(socket)2、网络字节序3、IP地址转换4、一系列函数5、TCP通信流程分析 第二次更新&#xff0c;自己再重新梳理一遍… 协议 协议&#xff1a;指一组规则&…

16.Python多线程

如果想让我们的程序同时执行多个任务&#xff0c;就需要使用多线程技术了 。到目前为止&#xff0c;我们编写的程序都是单线程的&#xff0c;在运行时一次只能执行 一个任务。 1 线程相关的知识 1.1 进程 一个进程就是一个正在执行的程序&#xff0c;每一个进程都有自己独立…

C++ 基础复习

1.常量 #include<iostream> using namespace std; // 1.define 宏常量 #define N 50 int main(){//N 60; // define定义的数据为常量&#xff0c;一旦修改会报错 cout<<N<<endl;//2.const 修饰的常量 const int m 12; //m 24; //错误 const修饰的常量不能…

限速虚量就赔一万元?看我如何薅羊毛!2024公认最好的随身WiFi

作为一个大名鼎鼎的羊毛党&#xff0c;最热衷的莫过于网上各种可以薅羊毛的信息。这天我们的羊毛群里说有一个叫格行的随身WiFi品牌挺嚣张的&#xff0c;号称限速虚量就赔付一万元&#xff01;还发了带章的承诺函&#xff01;我心说随身WiFi限速虚量的臭名声早就烂大街了&#…

基本电路理论-电流和电压的参考方向

&#x1f308;个人主页&#xff1a;会编程的果子君 &#x1f4ab;个人格言:“成为自己未来的主人~” 电流及参考方向 电流&#xff1a;带电粒子有规则的定向移动 电流强度&#xff1a;单位时间内通过导体横截面的电荷量&#xff0c;即&#xff1a;idq/dt 单位&#xff1a…

【C++】lambda 表达式 / 包装器 / bind 绑定

目录 一. lambda 表达式1. lambda 表达式的语法1. lambda 表达式的使用2. lambda 表达式的捕捉列表 二. 包装器三. bind 绑定 一. lambda 表达式 Lambda 表达式是 C11 标准引入的一种新特性, 它提供了一种方便的方式来定义匿名函数. lambda 表达式实际上是一个匿名的仿函数; …

ZYNQ学习之Ubuntu下Linux文件系统、用户权限与磁盘管理

基本都是摘抄正点原子的文章&#xff1a;<领航者 ZYNQ 之嵌入式Linux 开发指南 V3.2.pdf&#xff0c;因初次学习&#xff0c;仅作学习摘录之用&#xff0c;有不懂之处后续会继续更新~ 一、Linux 文件系统 1.1 Linux 文件系统简介以及类型 操作系统的基本功能之一就是文件管…

SpringBoot快速入门笔记(1)

文章目录 一、环境准备1、maven2、新建项目版本问题 二、项目上手1、HelloController2、热部署3、控制器4、参数传递5、ParamsController 一、环境准备 1、maven 把下载的maven包给配置好 2、新建项目版本问题 新建项目发现没有Java8&#xff0c;新版本IDEA问题&#xff08;2…

JAVA基础02-Java语言基础以及编译准备工作

什么是JAVA语言 Java是一门面向对象的编程语言&#xff0c;不仅吸收了C语言的各种优点&#xff0c;还摒弃了C里难以理解的多继承、指针等概念&#xff0c;因此Java语言具有功能强大和简单易用的两个特征。 &#xff08;可以编写桌面应用程序、Web应用程序、分布式系统和嵌入式…

HTTPS跟HTTP有区别吗?

HTTPS和HTTP的区别&#xff0c;白话一点说就是&#xff1a; 1. 安全程度&#xff1a; - HTTP&#xff1a;就像是你和朋友面对面聊天&#xff0c;说的话大家都能听见&#xff08;信息明文传输&#xff0c;容易被偷听&#xff09;。 - HTTPS&#xff1a;就像是你们俩戴着加密耳机…

idea 报错 Could not list the contents of folder “ftps

idea 报错 Could not list the contents of folder "ftps 解决方案 这里看到了网上的解决方案&#xff0c;顺便再记录一下。打开 【高级】菜单 - 取消勾选 被动模式。然后点击测试连接&#xff0c;显示连接成功&#xff01; ftp中的主动模式和被动模式 主动模式&…

嵌入式MCU和SOC之间的区别是什么?

今日话题&#xff0c;嵌入式MCU和SOC之间的区别是什么&#xff1f;表面上看&#xff0c;MCU代表嵌入式微控制器&#xff0c;而SOC代表片上系统&#xff0c;似乎只是嵌入式系统的不同称谓。然而&#xff0c;在实际的研发和产品设计中&#xff0c;你会发现它们在软硬件层面存在显…

【Python项目】基于django的【医用耗材网上申领系统】

医院信息化是社会发展的一个重要标志&#xff0c;它涉及到医院的各个方面&#xff0c;包括人员和物资&#xff0c;因此受到社会各界的广泛关注。近年来&#xff0c;随着医疗耗材数量的不断增加&#xff0c;如何有效管理这些耗材已经成为管理人员、医生以及社会各方面共同面临的…

数据结构(初阶)第二节:顺序表

从本文正式进入对数据结构的讲解&#xff0c;开始前友友们要有C语言的基础&#xff0c;熟练掌握动态内存管理、结构体、指针等章节&#xff0c;方便后续的学习。 顺序表&#xff08;Sequence List&#xff09; 线性表的概念&#xff1a;线性表&#xff08;linear list&#xff…

C++初阶:模拟实现vector类

模拟实现vector类 实现代码:vector.h #pragma once #include<assert.h> #include<iostream> using namespace std;namespace bit {template<class T>class vector{public:// Vector的迭代器是一个原生指针typedef T* iterator;typedef const T* const_ite…

网络升级固件

资源信息 可知 &#xff1a; install\soc_cv1800b_milkv_duo_sd\boot.sd文件较设备中的同名文件多了128个字节的文件头&#xff1b;install\soc_cv1800b_milkv_duo_sd\rawimages\boot.sd文件与设备中同名文件相同&#xff1b; 环境搭建 服务器 启动TFTP服务 安装TFTP服务器…

vue2项目安装(使用vue-cli脚手架)

使用npm安装 安装镜像&#xff08;使npm创建项目更快&#xff09;&#xff1a;镜像可更换 npm config set registry https://registry.npmmirror.com1.全局安装vue-cli&#xff08;一次&#xff09; npm install -g vue/cli 2. 查看vue-cli 版本 vue --version 3. 创建项目…