C/C++数据结构(十二)—— 红黑树

2023-05-16

文章目录

  • 1. 红黑树的概念
  • 2. 红黑树的性质
  • 3. 红黑树节点的定义
  • 4. 红黑树的旋转
  • 5. 红黑树的插入
    • 🍑 情况一
    • 🍑 情况二
    • 🍑 情况三
      • 🍅 叔叔结点存在且为红色
      • 🍅 叔叔结点存在且为黑色
        • 🍊 直线关系
        • 🍊 折线关系
      • 🍅 叔叔结点不存在
        • 🍊 直线关系
        • 🍊 折线关系
    • 🍑 代码实现
  • 6. 红黑树的删除
  • 7. 红黑树的遍历
  • 8. 红黑树的查找
  • 9. 红黑树的高度
  • 10. 红黑树的验证
  • 11. 红黑树的分析
  • 12. 红黑树与AVL树的比较


1. 红黑树的概念

红黑树(R-B TREE,全称:Red-Black Tree),本身是一棵二叉查找树,在其基础上附加了两个要求:

  • 树中的每个结点增加了一个用于存储颜色的标志域。
  • 树中没有一条路径比其他任何路径长出两倍,整棵树要接近于 “平衡” 的状态。

这里所指的路径,指的是从任何一个结点开始,一直到其子孙的叶子结点的长度;

接近于平衡,是指红黑树并不是平衡二叉树,只是由于对各路径的长度之差有限制,所以近似于平衡的状态。

2. 红黑树的性质

红黑树对于结点的颜色设置不是任意的,需满足以下性质的二叉查找树才是红黑树:

  • 树中的每个结点颜色不是红的,就是黑的。
  • 根结点的颜色是黑的。
  • 所有为 NIL 的叶子结点的颜色是黑的(注意:叶子结点说的只是为空(NILNULL)的叶子结点!)
  • 每个红色节点必须有两个黑色的子节点(从每个叶子到根的所有路径上不能有两个连续的红色结点)
  • 从任一节点到其叶子节点的所有路径上都包含相同数目的黑节点。

下图中这棵树,就是一颗典型的红黑树:

在这里插入图片描述

看完红黑树的定义是不是晕了?怎么这么多要求!!不用担心我们一条条的来分析:

第 3 条,显然这里的叶子节点不是平常我们所说的叶子节点,如图标有 NIL 的为叶子节点,为什么不按常规出牌,因为按一般的叶子节点也行,但会使算法更复杂;
 
第 4 条,即该树上决不允许存在两个连续的红节点;
 
第 5 条,比如图中红色节点 8 到左边的叶子节点 1 的路径包含 2 个黑节点,到的叶子节点 6 的路径也包含 2 个黑节点。所有性质 1 到 5 合起来约束了该树的平衡性能,即该树上的最长路径不可能会大于 2 倍最短路径。
 
为什么?因为第 1 条该树上的节点非红即黑,由于第 4 条该树上不允许存在两个连续的红节点,那么对于从一个节点到其叶子节点的一条最长的路径一定是红黑交错的,那么最短路径一定是纯黑色的节点;
 
而又因为第 5 条从任一节点到其叶子节点的所有路径上都包含相同数目的黑节点,这么来说最长路径上的黑节点的数目和最短路径上的黑节点的数目相等!
 
而又因为第 2 条根结点为黑、第 3 条叶子节点是黑,那么可知:最长路径小于等于最短路径的 2 倍。

思考:为什么红黑树最长路径中节点个数不会超过最短路径节点个数的两倍?

我们根据性质可以知道,红黑树当中不会出现连续的红色结点,并且从某一结点到其后代叶子结点的所有路径上包含的黑色结点的数目是相同的。

可以假设在红黑树中,从根到叶子的所有路径上包含的黑色结点的个数都是 N N N 个,那么最短路径就是全部由黑色结点构成的路径,即长度为 N N N

在这里插入图片描述

而最长可能路径就是由一黑一红结点构成的路径,该路径当中黑色结点与红色结点的数目相同,即长度为 2 N 2N 2N

在这里插入图片描述

因此,红黑树从根到叶子的最长可能路径不会超过最短可能路径的两倍,所以对于一棵具有 n 个结点的红黑树,树的高度至多为:2log(n+1)

由此可推出红黑树进行查找操作时的时间复杂度为 O(logN),因为对于高度为 h 的二叉查找树的运行时间为 O(h),而包含有 n 个结点的红黑树本身就是最高为 logN(简化之后)的查找树(h=logN),所以红黑树的时间复杂度为 O(logN)

3. 红黑树节点的定义

这里和AVL树一样用 KV 模型来实现红黑树,为了方便后序的旋转操作,将红黑树的结点定义为三叉链结构,除此之外还新加入了一个成员变量,用于表示结点的颜色。使用枚举来定义结点的颜色,这样可以增加代码的可读性和可维护性,并且便于后序的调试操作。

// 定义结点的颜色
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)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _col(RED)
	{}
};

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

如果设为黑色,就会导致根到叶子的路径上有一条路上,多一个额外的黑节点,这个是很难调整的。但是设为红色节点后,可能会导致出现两个连续红色节点的冲突,那么可以通过颜色调换和树旋转来调整。

也就是说:

  • 插入黑色结点,一定破坏红黑树的性质 4,必须对红黑树进行调整。
  • 插入红色结点,可能破坏红黑树的性质 3,可能对红黑树进行调整。

权衡利弊后,我们在构造结点进行插入时,默认将结点的颜色设置为红色。

4. 红黑树的旋转

当使用红黑树进行插入或者删除结点的操作时,可能会破坏红黑树的 5 条性质,从而变成了一棵普通树,此时就可以通过对树中的某些子树进行旋转,从而使整棵树重新变为一棵红黑树。

旋转操作分为左旋和右旋,同二叉排序树转平衡二叉树的旋转原理完全相同。例如图所示的是对一棵二叉查找树中局部子树进行左旋和右旋操作:

在这里插入图片描述

左旋: 如图所示,左旋时 y 结点变为该部分子树的根结点,同时 x 结点(连同其左子树 a)移动至 y 结点的左孩子。若 y 结点有左孩子 b,由于 x 结点需占用其位置,所以调整至 x 结点的右孩子处。

左单旋代码:

public// 左单旋(右边高需要左单旋)
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* ppNode = parent->_parent; // 先保存parent的parent

		// 1.建立parent和subRL之间的关系
		parent->_right = subRL;
		if (subRL) // 如果subRL节点不为空,那么要更新它的parent
		{
			subRL->_parent = parent;
		}

		// 2.建立subR和parent之间的关系
		subR->_left = parent;
		parent->_parent = subR;

		// 3.建立ppNode和subR之间的关系(分情况讨论parent是整颗树的根,还是局部子树)
		if (parent == _root) // 当parent是根节点时
		{
			_root = subR; // subR就变成了新的根节点
			_root->_parent = nullptr; // 根节点的的parent为空
		}
		else // 当parent是整个树的局部子树时
		{
			if (parent == ppNode->_left) // 如果parent在ppNode的左边
			{
				ppNode->_left = subR; // 那么subR就是parent的左子树
			}
			else // 如果parent在ppNode的右边
			{
				ppNode->_right = subR; // 那么subR就是parent的右子树
			}
			subR->_parent = ppNode; // subR的parent还要指向ppNode
		}
	}

右旋: 如图所示,同左旋是同样的道理,x 结点变为根结点,同时 y 结点连同其右子树 c 作为 x 结点的右子树,原 x 结点的右子树 b 变为 y 结点的左子树。

右单旋代码:

public// 右单旋(左边高就右单旋)
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		//Node* subLR = subL->_right;
		Node* ppNode = parent->_parent;

		// 1.建立parent和subLR之间的关系
		parent->_left = subLR;
		if (subLR) // 如果subLR节点不为空,那么要更新它的parent
		{
			subLR->_parent = parent;
		}

		// 2.建立subL和parent之间的关系
		subL->_right = parent;
		parent->_parent = subL;

		// 3.建立ppNode和subL之间的关系(分情况讨论parent是整颗树的根,还是局部子树)
		if (parent == _root) // 当parent是根节点时
		{
			_root = subL; // subL就变成了新的根节点
			_root->_parent = nullptr; // 根节点的的parent为空
		}
		else // 当parent是整个树的局部子树时
		{
			if (parent == ppNode->_left) // 如果parent在ppNode的左边
			{
				ppNode->_left = subL; // 那么subL就是parent的左子树
			}
			else // 如果parent在ppNode的右边
			{
				ppNode->_right = subL; // 那么subL就是parent的右子树
			}
			subL->_parent = ppNode; // subR的parent还要指向ppNode
		}
	}

关于旋转操作可以参考 C/C++数据结构(十一)—— 平衡二叉树(AVL树)

5. 红黑树的插入

当创建一个红黑树或者向已有红黑树中插入新的数据时,只需要执行以下 3 步:

  • 由于红黑树本身是一棵二叉查找树,所以在插入新的结点时,完全按照二叉查找树插入结点的方法,找到新结点插入的位置;
  • 将新插入的结点结点初始化,颜色设置为红色后插入到指定位置;(将新结点初始化为红色插入后,不会破坏红黑树第 5 条的性质)
  • 由于插入新的结点,可能会破坏红黑树第 4 条的性质(若其父结点颜色为红色,就破坏了红黑树的性质),此时需要调整二叉查找树,想办法通过旋转以及修改树中结点的颜色,使其重新成为红黑树!

为了区分这些节点,我们统一给这些结点命名:

  • 插入的节点标为 cur
  • cur 的父节点标为 p(parent)
  • cur 的祖父节点标为 g(grandfather)
  • cur 的叔树节点标为 u(uncle)。

如下图所示:

在这里插入图片描述

插入结点的第 1 步和第 2 步都非常简单,关键在于最后一步对树的调整!在红黑树中插入结点时,根据插入位置的不同可分为以下 3 种情况。

🍑 情况一

插入位置为整棵树的树根。处理办法:只需要将插入结点的颜色改为黑色即可。

如果新插入节点 cur 位于树的根上,且没有父节点,那么直接插入,并且把该节点的颜色设置为黑色(满足性质 2),并且它在每个路径上对黑节点数目增加一,性质 5 符合。

在这里插入图片描述

🍑 情况二

插入位置的父亲结点的颜色为黑色。处理方法:此种情况不需要做任何工作,新插入的颜色为红色的结点不会破坏红黑树的性质。

如果新插入节点 cur 的父节点 p 是黑色,那么性质 4 没有失效(新节点是红色的)。

在这种情形下,树仍是有效的。性质 5 也未受到威胁,尽管新节点 cur 有两个黑色叶子子节点;但由于新节点 cur 是红色,通过它的每个子节点的路径就都有同通过它所取代的黑色的叶子的路径同样数目的黑色节点,所以依然满足这个性质。

在这里插入图片描述

🍑 情况三

插入位置的父亲结点的颜色为红色。处理方法:由于插入结点颜色为红色,其父亲结点也为红色,破坏了红黑树第 4 条性质,此时需要结合其祖父结点和叔叔结点的状态,分为 3 种情况讨论。

🍅 叔叔结点存在且为红色

如果新插入结点 cur 的父亲结点 p 为红色,其祖父结点 g 为黑色,同时其叔叔结点 u 一定存在,且为红色。那么此时破坏了红黑树的第 4 条性质。

解决方案为:将父结点颜色改为黑色;将叔叔结点颜色改为黑色;将祖父结点颜色改为红色;下一步将祖父结点认做当前结点,继续判断,处理结果如下图所示:

在这里插入图片描述

分析:

这种情况下,由于父结点和当前结点颜色都是红色,所以为了不产生冲突,将父结点的颜色改为黑色。虽然避免了破坏第 4 条,但是却导致该条路径上的黑高度增加了 1 ,破坏了第 5 条性质。于是再将祖父结点颜色改为红色、叔叔结点颜色改为黑色后,该部分子树没有破坏第 5 条性质。

但是由于将祖父结点的颜色改变,还需判断是否破坏了上层树的结构,所以需要将祖父结点看做当前结点,继续判断。也就是需要将祖父结点当作新插入的结点,再判断其父结点是否为红色,若其父结点也是红色,那么又需要根据其叔叔的不同,进而进行不同的调整操作。

在这里插入图片描述

如果祖父结点是根结点,那我们直接再将祖父结点变成黑色即可,此时相当于每条路径黑色结点的数目都增加了一个。

在这里插入图片描述

也就是说,为了解决这个问题,需把 g 作为起始点,即把 g 看做一个插入的红色节点继续向上检索,属于哪种情况,按那种情况操作,要么中间就结束,要么就得找到根结点。

🍅 叔叔结点存在且为黑色

如果新插入结点 cur 的颜色为红色,其父亲结点 p 也为红色,其祖父结点 g 为黑色,其叔叔结点 u 存在且为黑色。

如果叔叔结点 u 节点存在,那么一定是黑色的,并且插入结点 cur 原来的颜色也一定是黑色的,现在 cur 之所以为红色,是因为 cur 的子树在调整的过程中将 cur 节点的颜色由黑色改成了红色。此时单纯使用变色已经无法处理了,我们需要进行旋转处理。

🍊 直线关系

如果祖孙三代的关系是直线,也就是说 curparentgrandfather 这三个结点在同一条直线上,那么我们需要先进行单旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根结点就是黑色的,因此无需继续往上进行处理。

  • 直线关系一:parent 是 grandfather 的左孩子,cur 是 parent 的左孩子时,就需要先进行右单旋操作,再进行颜色调整。
  • 直线关系二:parent 是 grandfather 的右孩子,cur 是 parent 的右孩子时,就需要先进行左单旋操作,再进行颜色调整。

如下图所示(直线关系一):

在这里插入图片描述

🍊 折线关系

如果祖孙三代的关系是折线,也就是说 curparentgrandfather 这三个结点不在同一条直线上,那么我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根就是黑色的,因此无需继续往上进行处理。

  • 折线关系一:parent 是 grandfather 的左孩子,cur 是 parent 的右孩子时,就需要先进行左右双旋操作,再进行颜色调整。
  • 折线关系二:parent 是 grandfather 的右孩子,cur 是 parent 的左孩子时,就需要先进行右左双旋操作,再进行颜色调整。

如下图所示(折线关系一):

在这里插入图片描述

🍅 叔叔结点不存在

如果新插入节点 cur 为红色,其父亲结点 p 也为红色,其祖父结点 g 为黑色,其叔叔结点 u 不存在。

当叔叔结点 u 节点不存在时,那么 cur 一定是新插入节点,如果 cur 不是新插入节点的话,那么 curp 一定有一个节点的颜色是黑色,就不满足性质 5(每条路径黑色节点个数相同)。

在这里插入图片描述

如果在插入前父亲结点 p 下面再挂黑色结点的话,就会导致图中两条路径黑色结点的数目不相同,而父亲结点 p 是红色的,因此父亲结点 p 下面自然也不能挂红色结点,所以说这种情况下的 cur 结点一定是新插入的结点。

🍊 直线关系

和上面一样,若祖孙三代的关系是直线,也就是说 curparentgrandfather 这三个结点在同一条直线上,那么我们需要先进行单旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根结点就是黑色的,因此无需继续往上进行处理

  • 直线关系一:parent 是 grandfather 的左孩子,cur 是 parent 的左孩子时,就需要先进行右单旋操作,再进行颜色调整。
  • 直线关系二:parent 是 grandfather 的右孩子,cur 是 parent 的右孩子时,就需要先进行左单旋操作,再进行颜色调整。

如下图所示(直线关系一):

在这里插入图片描述

🍊 折线关系

如果祖孙三代的关系是折线,也就是说 curparentgrandfather 这三个结点在同一条折线上,那么我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理。

  • 折线关系一:parent 是 grandfather 的左孩子,cur 是 parent 的右孩子时,就需要先进行左右双旋操作,再进行颜色调整。
  • 折线关系二:parent 是 grandfather 的右孩子,cur 是 parent 的左孩子时,就需要先进行右左双旋操作,再进行颜色调整。

如下图所示(折线关系一):

在这里插入图片描述

🍑 代码实现

红黑树中插入结点的具体实现代码:

public:
	// 插入函数
	bool Insert(const pair<K, V>& kv)
	{
		// 如果红黑树是空树,把插入节点直接作为根节点
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK; // 根结点必须是黑色
			return true; // 插入成功
		}

		// 1.按照二叉搜索树的规则插入
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < kv.first) // 待插入节点的key值大于当前节点的key值
			{
				// 往右子树走
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first) // 待插入节点的key值小于当前节点的key值
			{
				// 往左子树走
				parent = cur;
				cur = cur->_left;
			}
			else // 待插入节点的key值等于当前节点的key值
			{
				return false; // 插入失败,返回false
			}
		}

		// 2.当循环结束,说明cur找到了空的位置,那么就插入
		cur = new Node(kv); // 构造一个新节点
		cur->_col = RED; // 插入结点的颜色设置为红色
		if (parent->_kv.first < kv.first) // 如果新节点的key值大于当前parent节点的key值
		{
			// 就把新节点链接到parent的右边
			parent->_right = cur;
		}
		else // 如果新节点的key值小于当前parent节点的key值
		{
			// 就把新节点链接到parent的左边
			parent->_left = cur;
		}
		cur->_parent = parent; // 别忘了把新节点里面的_parent指向parent(因为我们定义的是一个三叉链)

		// 3.若插入结点的父结点是红色的,则需要对红黑树进行调整(存在连续红色节点的情况)
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent; // 如果parent是红色,那么其父结点一定存在

			if (grandfather->_left == parent) // 当parent是grandfather的左孩子
			{
				Node* uncle = grandfather->_right; // uncle一定是grandfather的右孩子

				//情况一:uncle存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 调整颜色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续往上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况二 + 情况三:uncle不存在 + uncle存在且为黑
				{
					if (cur == parent->_left) 
					{
						//     g
						//   p
						// c
						RotateR(grandfather); //右单旋

						// 颜色调整
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else // cur == parent->_right
					{
						//     g
						//   p
						//     c 
						// 左右双旋
						RotateL(parent);
						RotateR(grandfather);

						// 颜色调整
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					// 子树旋转后,该子树的根变成了黑色,无需继续往上进行处理 
					break;
				}
			}
			else // parent是grandfather的右孩子
			{
				Node* uncle = grandfather->_left; // uncle是grandfather的左孩子

				// 情况一:uncle存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续往上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况二 + 情况三:uncle不存在 + uncle存在且为黑
				{
					if (cur == parent->_right)
					{
						// g
						//   p
						//     c 
						RotateL(grandfather); // 左单旋

						// 颜色调整
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else // cur == parent->_left
					{
						// g
						//   p
						// c

						// 右左双旋
						RotateR(parent);
						RotateL(grandfather);

						// 颜色调整
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					// 子树旋转后,该子树的根变成了黑色,无需继续往上进行处理
					break;
				}
			}
		}
		_root->_col = BLACK; // 根结点的颜色为黑色(可能被情况一变成了红色,需要变回黑色)
		return true; // 插入成功
	}

6. 红黑树的删除

在红黑树中删除结点,思路更简单,只需要完成 2 步操作:

  • 将红黑树按照二叉查找树删除结点的方法删除指定结点;
  • 重新调整删除结点后的树,使之重新成为红黑树;(还是通过旋转和重新着色的方式进行调整)

在二叉查找树删除结点时,分为 3 种情况:

  • 若该删除结点本身是叶子结点,则可以直接删除;
  • 若只有一个孩子结点(左孩子或者右孩子),则直接让其孩子结点顶替该删除结点;
  • 若有两个孩子结点,则找到该结点的右子树中值最小的叶子结点来顶替该结点,然后删除这个值最小的叶子结点。

以上三种情况最终都需要删除某个结点,此时需要判断删除该结点是否会破坏红黑树的性质。判断的依据是:

  • 如果删除结点的颜色为红色,则不会破坏;
  • 如果删除结点的颜色为黑色,则肯定会破坏红黑树的第 5 条性质,此时就需要对树进行调整,调整方案分 4 种情况讨论。

第一种: 删除结点的兄弟结点颜色是红色,调整措施为:将兄弟结点颜色改为黑色,父亲结点改为红色,以父亲结点来进行左旋操作,同时更新删除结点的兄弟结点(左旋后兄弟结点发生了变化),如下图所示:

在这里插入图片描述

第二种: 删除结点的兄弟结点及其孩子全部都是黑色的,调整措施为:将删除结点的兄弟结点设为红色,同时设置删除结点的父结点标记为新的结点,继续判断;

第三种: 删除结点的兄弟结点是黑色,其左孩子是红色,右孩子是黑色。调整措施为:将兄弟结点设为红色,兄弟结点的左孩子结点设为黑色,以兄弟结点为准进行右旋操作,最终更新删除结点的兄弟结点;

第四种: 删除结点的兄弟结点是黑色,其右孩子是红色(左孩子不管是什么颜色),调整措施为:将删除结点的父结点的颜色赋值给其兄弟结点,然后再设置父结点颜色为黑色,兄弟结点的右孩子结点为黑色,根据其父结点做左旋操作,最后设置替换删除结点的结点为根结点;

关于红黑树删除结点具体实现代码大家可以去查阅一下资料,主要是理解思想!!!

7. 红黑树的遍历

中序遍历和二叉树的中序实现一样,只不过因为中序是递归遍历,涉及到传参,所以需要写一个子函数。

代码示例

private:
	// 中序遍历(子函数)(子函数)
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}
public:
	// 中序遍历
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

8. 红黑树的查找

AVL树的查找与二叉搜索树一样:

  • 若树为空树,则查找失败,返回 nullptr。
  • 若树不为空树,则有以下三种情况:
    • 若 key 值小于当前结点的值,则应该在该结点的左子树当中进行查找。
    • 若 key 值大于当前结点的值,则应该在该结点的右子树当中进行查找。
    • 若 key 值等于当前结点的值,则查找成功,返回对应结点。

代码示例

public:
	// 查找函数
	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key) // key值大于该结点的值
			{
				cur = cur->_left; // 在该结点的右子树当中查找
			}
			else if (cur->_kv.first < key) // key值小于该结点的值
			{
				cur = cur->_right; // 在该结点的左子树当中查找
			}
			else // 当前节点的值等于key值
			{
				return cur; //返回该结点
			}
		}
		return nullptr; //查找失败
	}

9. 红黑树的高度

因为后面要验证最长路径是否会超过最短路径的 2 倍,所以我们要分别求树的最长路径和最短路径

代码示例

private:
	// 计算红黑色的最长路径(子函数)
	int _maxHeight(Node* root)
	{
		if (root == nullptr)
			return 0;

		int lh = _maxHeight(root->_left);
		int rh = _maxHeight(root->_right);

		return lh > rh ? lh + 1 : rh + 1;
	}

	// 计算红黑色的最短路径(子函数)
	int _minHeight(Node* root)
	{
		if (root == nullptr)
			return 0;

		int lh = _minHeight(root->_left);
		int rh = _minHeight(root->_right);

		return lh < rh ? lh + 1 : rh + 1;
	}
public:
	// 计算高度
	void Height()
	{
		cout << "最长路径:" << _maxHeight(_root) << endl;
		cout << "最短路径:" << _minHeight(_root) << endl;
	}

10. 红黑树的验证

红黑树的检测分为两步:

  1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
  2. 检测其是否满足红黑树的性质

代码示例

private:
	// 判断是否为红黑树(子函数)
	bool _IsValidRBTree(Node* pRoot, size_t k, const size_t blackCount)
	{
		//走到null之后,判断k和black是否相等
		if (nullptr == pRoot)
		{
			if (k != blackCount)
			{
				cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl;
				return false;
			}
			return true;
		}

		// 统计黑色节点的个数
		if (BLACK == pRoot->_col)
			k++;

		// 检测当前节点与其双亲是否都为红色
		if (RED == pRoot->_col && pRoot->_parent && pRoot->_parent->_col == RED)
		{
			cout << "违反性质三:存在连在一起的红色节点" << endl;
			return false;
		}

		return _IsValidRBTree(pRoot->_left, k, blackCount) &&
			_IsValidRBTree(pRoot->_right, k, blackCount);
	}
public:
	// 判断是否为红黑树
	bool IsBalanceTree()
	{
		// 检查红黑树几条规则

		Node* pRoot = _root;
		// 空树也是红黑树
		if (nullptr == pRoot)
			return true;

		// 检测根节点是否满足情况
		if (BLACK != pRoot->_col)
		{
			cout << "违反红黑树性质二:根节点必须为黑色" << endl;
			return false;
		}

		// 获取任意一条路径中黑色节点的个数 -- 比较基准值
		size_t blackCount = 0;
		Node* pCur = pRoot;
		while (pCur)
		{
			if (BLACK == pCur->_col)
				blackCount++;

			pCur = pCur->_left;
		}

		// 检测是否满足红黑树的性质,k用来记录路径中黑色节点的个数
		size_t k = 0;
		return _IsValidRBTree(pRoot, k, blackCount);
	}

11. 红黑树的分析

对红黑树的分析其实就是对 2-3 查找树的分析,红黑树能够保证符号表的所有操作即使在最坏的情况下都能保证对数的时间复杂度,也就是树的高度。

在分析之前,为了更加直观,下面是以升序,降序和随机构建一颗红黑树的动画。

  • 以升序插入构建红黑树:

在这里插入图片描述

  • 以降序插入构建红黑树:

在这里插入图片描述

  • 随机插入构建红黑树

在这里插入图片描述

从上面三张动画效果中,可以很直观的看出,红黑树在各种情况下都能维护良好的平衡性,从而能够保证最差情况下的查找,插入效率。

下面来详细分析下红黑树的效率。

(1)在最坏的情况下,红黑树的高度不超过 O ( 2 l o g N ) O(2logN) O(2logN)

最坏的情况就是,红黑树中除了最左侧路径全部是由 3-node 节点组成,即红黑相间的路径长度是全黑路径长度的 2 倍。

下图是一个典型的红黑树,从中可以看到最长的路径(红黑相间的路径)是最短路径的 2 倍:

在这里插入图片描述

(2)红黑树的平均高度大约为 O ( l o g N ) O(logN) O(logN)

12. 红黑树与AVL树的比较

红黑树和AVL树都是高效的平衡二叉树,增删查改的时间复杂度都是 O ( l o g N ) O(logN) O(logN),但红黑树和AVL树控制二叉树平衡的方式不同:

  • AVL树是通过控制左右高度差不超过1来实现二叉树平衡的,实现的是二叉树的严格平衡。
  • 红黑树是通过控制结点的颜色,从而使得红黑树当中最长可能路径不超过最短可能路径的2倍,实现的是近似平衡。

相对于AVL树来说,红黑树降低了插入结点时需要进行的旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,实际运用时也大多用的是红黑树。红黑树可以保证在最坏情况下的时间复杂度仍为 O ( l o g N ) O(logN) O(logN)。当数据量多到一定程度时,使用红黑树比二叉查找树的效率要高。

并且同平衡二叉树相比较,红黑树没有像平衡二叉树对平衡性要求的那么苛刻,虽然两者的时间复杂度相同,但是红黑树在实际测算中的速度要更胜一筹!

红黑树的应用:

  • C++ STL库 – map/setmutil_map/mutil_set
  • Java 库
  • linux内核
  • 其他一些库

另外强烈推荐在 wikipedia – RBTree 中红黑树的这篇讲解文章。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C/C++数据结构(十二)—— 红黑树 的相关文章

  • DIY组装无人机电机+电调+电池+桨叶搭配知识

    以下内容转载至下网址 有一点点修改 DIY组装无人机电机 43 电调 43 电池 43 桨叶搭配知识 xff08 转贴 xff09 多旋翼 模吧 moz8 com https www moz8 com forum php mod 61 vi
  • (每日一练)MATLAB二维插值

    在前面介绍了学习MATLAB的一维插值方法 xff0c 今天来学习MATLAB二维插值方法 首先来看二维插值函数的使用格式 xff1a z1 61 interp2 x y z x1 y1 39 method 39 其中x y z分别是我们给
  • Ubuntu18.04安装D435iSDK和ROS Wapper

    实验室新到D435i深度相机 xff0c 我想来跑跑开源算法 xff0c 安装驱动各种帖子很多 xff0c 我把我看到两篇最有用的帖子整理一下 帖子连接放在文末 1 安装Intel RealSense SDK 2 0 参考 xff1a ht
  • 【Ros控制机械臂学习笔记】move_group C++interface接口学习

    在完成机器人URDF模型建立 xff0c 利用moveit setup assistant配置生成robot moveit config文件夹之后 xff0c 接下来就是要的学习方向有两个 一个是向下位机走 xff0c 即上图的右面 xff
  • MATLAB校准磁力计

    初识magcal函数 语法 A b expmfs 61 magcal D A b expmfs 61 magcal D fitkind 描述 A xff0c b xff0c expmfs 61 magcal xff08 D xff09 返回
  • C语言练习之路--函数篇

    目录 一 前言二 选择题三 编程题1 乘法口诀表2 交换两个整数3 函数判断闰年4 函数判断素数5 递归实现n的k次方6 计算一个数的每位之和 xff08 递归实现 xff09 7 strlen的模拟 xff08 递归实现 xff09 8
  • 使用VOFA调试PID算法

    1 选用FireWater模式 这个模式下才支持多通道数据 2 代码编写 我使用的是cubemx和keil xff08 1 先在cubemx里把串口打开 我用的是401 xff08 2 打开我的Keil继续 span class token
  • 基于 JSP+Mysql 学生成绩查询web系统

    文章目录 一 学习任务二 学习内容1 准备工作1 1 相关软件1 2 源代码 2 连接MySQL3 idea配置4 运行结果5 web访问 三 参考博客 一 学习任务 首先在Mysql中创建相应的学生成绩表 xff0c 然后基于 JSP 4
  • jquery获取指定元素的指定属性的值

    使用jquery获取指定元素的指定属性的值 xff1a 选择器 attr 34 属性名 34 gt 用来获取那些值不是true false的属性的值 选择器 prop 34 属性名 34 gt 用来获取值是true false的属性的值 例
  • Maven3.6.1下载安装基本使用 (初识)(自用)

    对MAVEN的粗浅认识 Apache Maven 是 项目管理与构建工具 基于POM xff08 项目对象模型 xff09 的概念 作用 提供了一套标准化的项目结构 粗浅理解就是 xff0c 通常eclipse xff0c idea等jav
  • VSCode代码格式化快捷键

    我们在编写代码和阅读别人代码的时候 xff0c 容易出现同级元素缩进没有对齐的情况 xff0c 我们需对代码进行格式化 xff0c 以方便自己和他人的阅读 在vscode中使用快捷键 Shift 43 Alt 43 F 使用示例 xff1a
  • for循环【C++】

    for循环 执行一个特定循环的控制结构 for 条件 条件判断 条件处理 执行体 xff1b 条件 条件判断和条件处理都不是必要的 xff0c 当三者都没有 xff0c 则相当于一个无限循环 条件不一定需要在括号内声明和初始化 xff0c
  • 基于深度强化学习的智能船舶航迹跟踪控制

    基于深度强化学习的智能船舶航迹跟踪控制 人工智能技术与咨询 昨天 本文来自 中国舰船研究 xff0c 作者祝亢等 关注微信公众号 xff1a 人工智能技术与咨询 了解更多咨询 xff01 0 引 言 目前 xff0c 国内外对运载工具的研究
  • 面向区块链的高效物化视图维护和可信查询

    面向区块链的高效物化视图维护和可信查询 人工智能技术与咨询 来源 xff1a 软件学报 xff0c 作者蔡 磊等 摘 要 区块链具有去中心化 不可篡改和可追溯等特性 可应用于金融 物流等诸多行业 由于所有交易数据按照交易时间顺序存储在各个区
  • 基于深度学习的磁环表面缺陷检测算法

    基于深度学习的磁环表面缺陷检测算法 人工智能技术与咨询 来源 xff1a 人工智能与机器人研究 xff0c 作者罗菁等 关键词 缺陷检测 xff1b 深度学习 xff1b 磁环 xff1b YOLOv3 xff1b 摘要 在磁环的生产制造过
  • 基于PX4的地面无人车避障系统及路径规划研究

    基于PX4的地面无人车避障系统及路径规划研究 人工智能技术与咨询 来源 xff1a 动力系统与控制 xff0c 作者姜琼阁等 关键词 地面无人车 xff1b 避障 xff1b PX4 xff1b 摘要 地面无人车避障及路径规划是指 xff0
  • 基于图像的数据增强方法发展现状综述

    基于图像的数据增强方法发展现状综述 人工智能技术与咨询 2022 03 22 20 57 点击蓝字 关注我们 来源 xff1a 计算机科学与应用 xff0c 作者冯晓硕等 关键词 数据增强 xff1b 图像数据集 xff1b 图像处理 xf
  • 基于改进SSD算法的小目标检测与应用

    人工智能技术与咨询 点击蓝字 关注我们 来源 xff1a 计算机科学与应用 xff0c 作者刘洋等 关键词 SSD xff1b 深度学习 xff1b 小目标检测 摘要 xff1a 摘要 针对通用目标检测方法在复杂环境下检测小目标时效果不佳
  • Excel线性回归分析

    文章目录 一 学习任务二 学习内容1 1 高尔顿数据集进行线性回归分析1 1 1 父母身高平均值和其中一个子女身高进行回归分析1 1 2 父子身高回归方程1 1 3 母子身高回归方程 1 2 Anscombe四重奏数据集进行回归分析 一 学
  • 组网雷达融合处理组件化设计与仿真

    人工智能技术与咨询 点击蓝色 关注我们 关键词 xff1a 组网雷达 点迹融合 航迹融合 组件化设计 仿真 摘要 数据融合处理是多雷达组网的核心 以典型防空雷达网为参考对象 xff0c 采用组件化设计方式 xff0c 将组网数据融合处理过程

随机推荐

  • 人工智能 知识图谱

    关于举办 2022年数字信息化培训项目系列 知识图谱Knowledge Graph构建与应用研修班线上课程的通知 各有关单位 一 培训目标 本次课程安排紧密结合理论与实践 xff0c 深入浅出 xff0c 循序渐进 从基本概念讲起 xff0
  • 深度学习(Deep Learning)

    知识关键点 1 人工智能 深度学习的发展历程 2 深度学习框架 3 神经网络训练方法 4 卷积神经网络 xff0c 卷积核 池化 通道 激活函数 5 循环神经网络 xff0c 长短时记忆 LSTM 门控循环单元 GRU 6 参数初始化方法
  • 基于深度学习的机器人目标识别和跟踪

    如今 xff0c 深度学习算法的发展越来越迅速 xff0c 并且在图像处理以及目标对象识别方面已经得到了较为显著的突破 xff0c 无论是对检测对象的类型判断 xff0c 亦或者对检测对象所处方位的检测 xff0c 深度学习算法都取得了远超
  • 零基础Linux版MySQL源码方式安装+配置+远程连接完整图解 无坑实录

    无论开发还是运维 xff0c 项目环境搞不定 xff0c 还真让你干不成活 xff0c MySQL在不同场景 不同平台下安装方式也不同 xff0c 本次主要分享centos7下MySQL源码rpm方式安装 xff0c 其它方式后续分享 xf
  • C++,友元,语法+示例,非常详细!!!!

    友元概念 友元的目的就是让一个函数或者类 访问另外一个类中的私有成员 友元的关键字为 friend 友元的几种实现 全局函数做 友元类做 友元成员函数做 友元重载函数做 友元 全局函数做 友元 include lt iostream gt
  • STL——STL简介、STL六大组件

    一 STL是什么 STL standard template library xff1a C 43 43 标准模板库 xff0c 是C 43 43 标准库的重要组成部分 xff0c 不仅是一个可复用的组件库 xff0c 还是一个包罗数据结构
  • 文件流指针和文件描述符

    1 文件流指针和文件描述符的产生 fopen函数打开文件成功后会返回文件流指针 open函数打开文件成功后返回的是文件描述符 他俩的相同点是通过文件流指针和文件描述符都可以对文件进行操作 2 fopen函数和open函数的介绍 fopen函
  • docker 操作

    查看容器 xff1a sudo docker ps a 删除容器 xff1a sudo docker rm NAMES 容器的名字 下载镜像 xff1a sudo docker pull rmus2022 server v1 2 0 查看镜
  • 树莓派32位系统烧录及连接

    目录 前言 一 烧录树莓派系统 1 格式化tf卡 2 烧录系统 二 连接树莓派 1 开启SSH 2 开启网络共享 3 下载Putty 三 开启图形化界面 非必须 最后 xff1a 前言 我在树莓派环境搭建的过程中 xff0c 看了几十篇博客
  • 鸢尾花Iris数据集进行SVM线性分类

    文章目录 一 学习任务二 学习内容1 鸢尾花数据集使用SVM线性分类1 1 SVM介绍1 2 LinearSVC xff08 C xff09 方式实现分类1 3 分类后的内容基础上添加上下边界 三 参考博客 一 学习任务 安装python3
  • intel realsense d435i相机标定中文文档

    intel realsense d435i相机标定中文文档 此文档参考了官方的英文文档 xff0c 原地址面向英特尔 实感 深度摄像头的 IMU 校准工具 intelrealsense com IMU概述 xff1a 惯性测量单元 imu
  • VScode-git提交 无法推送refs到远端

    在将代码同步到远端仓库时 xff0c 弹窗提醒 无法推送refs到远端 您可以试着运行 拉取 功能 xff0c 整合您的更改 但尝试后发现 拉取 功能也无法解决问题 xff0c 最后是因为文件过大原因 xff0c 在这里记录一下解决方法 x
  • VMware16虚拟机中安装OpenEuler详细教程指南

    文章目录 安装前提准备镜像创建虚拟机安装欧拉踩坑指南 x1f351 网络指南 安装前提 Windown 10VMware 16openEuler 20 03 LTS SP3 准备镜像 镜像地址 xff1a OpenEuler 直接在官网下载
  • C/C++排序算法(三)—— 冒泡排序和快速排序

    文章目录 前言1 冒泡排序 x1f351 基本思想 x1f351 图解冒泡 x1f351 动图演示 x1f351 代码实现 x1f351 代码优化 x1f351 特性总结 2 快速排序 x1f351 hoare 版本 x1f345 图解过程
  • C/C++排序算法(四)—— 归并排序和计数排序

    文章目录 前言1 归并排序 x1f351 基本思想 x1f351 算法图解 x1f345 分组 x1f345 归并 x1f345 比较 x1f351 动图演示 x1f351 代码实现 x1f351 非递归实现 x1f345 情况一 x1f3
  • C++深入浅出(九)—— 多态

    文章目录 1 多态的概念2 多态的定义及实现 x1f351 多态的构成条件 x1f351 虚函数 x1f351 虚函数的重写 x1f351 虚函数重写的两个例外 x1f351 C 43 43 11的override 和 final x1f3
  • C++STL剖析(八)—— unordered_set和unordered_multiset的概念和使用

    文章目录 前言1 unordered set的介绍和使用 x1f351 unordered set的构造 x1f351 unordered set的使用 x1f345 insert x1f345 find x1f345 erase x1f3
  • C++STL剖析(九)—— unordered_map和unordered_multimap的概念和使用

    文章目录 1 unordered map的介绍和使用 x1f351 unordered map的构造 x1f351 unordered map的使用 x1f345 insert x1f345 operator x1f345 find x1f
  • C++STL剖析(十)—— 位图(bitset)

    文章目录 1 位图的介绍2 位图的概念3 位图的实现 x1f351 构造函数 x1f351 设置指定位 x1f351 清除指定位 x1f351 获取指定位的状态 x1f351 打印函数 4 总结 1 位图的介绍 在介绍位图之前先来看一道面试
  • C/C++数据结构(十二)—— 红黑树

    文章目录 1 红黑树的概念2 红黑树的性质3 红黑树节点的定义4 红黑树的旋转5 红黑树的插入 x1f351 情况一 x1f351 情况二 x1f351 情况三 x1f345 叔叔结点存在且为红色 x1f345 叔叔结点存在且为黑色 x1f