首页 > 编程语言 >【C++小白到大牛】红黑树那些事儿

【C++小白到大牛】红黑树那些事儿

时间:2024-08-17 21:26:43浏览次数:12  
标签:结点 小白到 cur parent C++ grandfather 红黑树 col

目录

前言:

一、红黑树的概念

二、红黑树的性质

三、红黑树结点的定义

四、红黑树的插入

情况一:u存在且为红

情况二:u不存在/u存在且为黑

小总结:

原码:

五、红黑树的检验

六、性能比较


前言:

我们之前已经学过了二叉搜索树的优化版——AVL树,这次我们来学习二叉搜索树的另外一种优化版本——心心念念的红黑树。

之前还是初学者的博主觉得红黑树简直就是神明般的存在,而现如今也能理清楚红黑树的基本框架和插入规则,不禁感慨万分。忆往昔峥嵘岁月,展未来任重道远~

一、红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。因此下面我们就来研究怎样的一个配色规则使得红黑树接近平衡。

二、红黑树的性质

  1.  每个结点不是红色就是黑色 
  2. 根节点是黑色的  
  3. 如果一个节点是红色的,则它的两个孩子结点必须是黑色的,没有连续的红色节点  
  4.  每条路径均包含相同数目的黑色结点  

问题:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?

答:因为在上面的规则制约下,最长路径就是一黑一红,而最短路径就是全黑,又因为每条路径的黑色结点数量相同,因此能推出上面的结论最长路径中节点个数不会超过最短路径节点个数的两倍!

三、红黑树结点的定义

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

问题:在节点的定义中,为什么要将节点的默认颜色给成红色的?也就是新增节点为什么是红色?

答:

因为新插入结点颜色是红色不一定错误,但是插入黑色一定有问题

插入黑色结点为什么难以控制,违反规则?因为你在任何一个叶子结点的位置插入黑色结点就不满足,每条路径的黑色结点的数量是一样的,没法解决! 

四、红黑树的插入

第一步: 按照二叉搜索的树规则找新节点的插入位置

这里根二叉搜索树的所有先前插入规则是一样的,都需要根据二叉搜索树的性质去找到新插入结点的具体位置

第二步:检测新节点插入后,红黑树的性质是否造到破坏

因为默认新插入结点的颜色是红色,所以首先需要判断父亲的颜色:

  1. 插入位置的父亲是黑色,不需要处理,插入结束
  2. 插入位置的父亲是红色,出现了连续的红色结点,需要处理

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

所以下面的所有情况都是根据插入结点的父亲是红色来判断。我们来根据叔叔的情况来分情况讨论~

我们为什么要以u的存在与否、颜色与否来分情况讨论呢?因为只有叔叔的结点是不确定的,c、p、g颜色都是确定的!因为c、p颜色都是红色,而g的颜色一定是黑色,因为不能有两个连续的红色结点,所以只有叔叔是未知数。

情况一:u存在且为红

解决方法:p/u变黑,g变红,如果g是根,再把g变黑,如果g不是根,继续往上处理(g当成c,此时的g就相当于新插入的红色结点c,循环判断,直到根节点为止)

爷爷为什么要变红?因为爷爷有可能不是整个树的根,为了保持当前树的黑色结点不变,如果爷爷是根,将爷爷变成黑即可。

这里的a/b/c/d/e不用管,p/u是g的左或者右都不影响,cur是p的左或者右也不影响,处理方式都是一样的

情况二:u不存在/u存在且为黑

下面是u不存在的情况:

这时就已经违反了红黑树的规则,就需要我们进行旋转处理

p为g的左孩子,cur为p的左孩子,则进行有单旋转;相反,p为g的右孩子,cur为p的右孩子,则进行左单旋转。

颜色变换:p、g变色,p变黑,g变红

下面是u存在且为黑的情况

这里也是单旋,因此在结构一样的情况下,u不存在和u存在且为空的情况处理方式都是一样的


上面因为因插入的结点都是与原先结点在同一侧,因此直接采用单旋就可以解决问题,下面我们来讲解,新插入结点与原先结点不在同一侧,需要先单旋变为同一侧,接着再以根节点为旋转点,朝着另外一个方向进行旋转,这里就是双旋,与AVL树旋转的思路几乎一样

下面是u不存在的情况,旋转最后将cur和g变色

然后是u存在且为黑的情况

小总结:

p为g的左孩子,cur为p的右孩子,左右双旋+变色

p为g的右孩子,cur为p的左孩子,右左双旋+变色。

看是否在一边,如果在一边直接单旋即可,若不在,先单旋变为同一边,接着再单旋即可。

原码:

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	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); // 红色的
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_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;
					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;
	}

五、红黑树的检验

思路:

要想验证这棵树是不是红黑树,需要从红黑树的概念入手,分为以下几点:

  1. 根是黑的
  2. 没有连续的红色结点
  3. 每条路径的黑色结点的数量相同

可以先遍历一条路径直接到根节点,以这条路径的黑色结点作为基准值,然后再接着遍历其他路径,看其他路径的黑色结点数量是否与其相同,进行判断即可。

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

六、性能比较

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logN),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,红黑树降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。

标签:结点,小白到,cur,parent,C++,grandfather,红黑树,col
From: https://blog.csdn.net/hanwangyyds/article/details/141094220

相关文章

  • 成绩排序—————c++
    先看问题:成绩排序时间限制:C/C++1000MS,其他语言2000MS内存限制:C/C++256MB,其他语言512MB难度:普及-分数:100 OI排行榜得分:12(0.1*分数+2*难度)出题人:root描述给出班里某门课程的成绩单,请你按成绩从高到低对成绩单排序输出,如果有相同分数则名字字典序小的在前。输入......
  • C++输出
    Hello!Hi!这是我的第一个程序如何输出上面的文字?C++提供了一个函数——cout1|#include<iostream>2|3|intmain()4|{5|cout<<"Hello!";6|return0;7|}以上代码是cout的应用话说回来,如何实现文章开头的效果呢?你可能会用以下代码1|cout<<"Hello!";2|cout<......
  • C++-练习-20
    题目:WilliamWingate从事披萨饼分析服务。对于每个披萨饼,它都需要记录下列信息:披萨饼从事公司的名称,可以有多个单词组成披萨饼的直径披萨饼的重量。请设计一个能够存储这些信息的结构,并编写一个使用这种结构变量的程序。程序将请求用户输入上述信息,然后显示这些信息。请......
  • 【C++进阶学习】第十三弹——C++智能指针的深入解析
    前言:在C++编程中,内存管理是至关重要的一个环节。传统的手动内存管理方式容易导致内存泄漏、悬挂指针等问题。为了解决这些问题,C++引入了智能指针。本文将详细讲解C++中智能指针的概念、种类、使用方法以及注意事项。目录一、引言二、智能指针的原理及目的2.1智能指针......
  • C++_类和对象(下篇)
    一、目标1.再谈构造函数2.Static成员3.友元4.内部类二、对目标的讲解1.再谈构造函数1.1构造函数体赋值在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。classDate{public: Date(intyear,intmonth......
  • C/C++ 拷贝构造函数 | 赋值构造函数 | 移动构造函数 | 移动赋值构造函数
    文章目录前言1.拷贝构造函数(CopyConstructor)2.赋值构造函数(CopyAssignmentOperator)3.移动构造函数(MoveConstructor)4.移动赋值构造函数(MoveAssignmentOperator)总结前言C++中关于一个对象的构造,就有很多讲究。其中最常用的可能就是拷贝构造函数......
  • 【c++】用c++写一个十六进制颜色随机产生器
     引入:大家在设计网页时有没有不知道用啥颜色,词汇量太少不知道有啥颜色单词?今天教大家用C++一个程序来随机生成一个16进制的颜色值 #include<iostream>#include<cstdlib>#include<ctime>intmain(){ srand(time(nullptr)); intarr[]={0,1,2,3,4,5,6,7,8,......
  • 彼岸花开C++,模版初阶
    欢迎访问小马的博客,如果觉得小马的博客有帮助的话,记得点赞收藏加关注哦~~~  模版初阶(1)泛型编程(2)函数模版(3)类模版模版初阶(1)泛型编程如何实现一个通用的交换函数?voidSwap(int&a,int&b){inttmp=a;a=b;b=tmp;}voidSwap(double&a,do......
  • 【C++】STL 知识总复习
    文章目录1.STL使用1.1常见的容器1.1.1序列式容器1.1.2关联式容器1.1.3容器适配器1.2迭代器1.2.1输入迭代器(InputIterator)1.2.2输出迭代器(OutputIterator)1.2.3前向迭代器(ForwardIterator)1.2.4双向迭代器(BidirectionalIterator)1.2.5随机访问迭......
  • c++ (2-0) 从txt读取和保存数据
     CMakeLists.txt #设置CMake的最小版本要求cmake_minimum_required(VERSION3.10)#设置项目名称和版本project(PoseSaverVERSION1.0)#设置C++标准为C++11set(CMAKE_CXX_STANDARD11)set(CMAKE_CXX_STANDARD_REQUIREDTrue)#查找Eigen库find_packa......