当前位置: 首页 > news >正文

【C++】红黑树

目录

引言:为啥设计红黑树?

1.红黑树的特点

1.1相关概念

1.2插入的结点的颜色

2.红黑树的设计

2.1、结点设计

2.2基本框架:

3.红黑树插入 

3.1插入的介绍

3.2插入调整

情况一:

情况2:

 情况3

4.查找实现(Find) 

5.红黑树的平衡检查


C++ 进阶: 本仓库存放一些较难C++代码https://gitee.com/j-jun-jie/c---advanced.git

 

引言:为啥设计红黑树?

我们以前讲了搜索二叉树但是如果插入的数据特别接近就会退化成单支树:

 所以又出现了平衡搜索二叉树->AVL树。平衡二叉树保证了在最差的情况下,二叉树依然能够保持绝对的平衡,即左右两个子树的高度差的绝对值不超过1。但是这又会带来一个问题,那就是平衡二叉树的定义过于严格,导致每次插入或者删除一个元素之后,都要去维护二叉树整体的平衡,这样产生额外的代价又太大了。二叉搜索树可能退化成链表,而平衡二叉树维护平衡的代价开销又太大了。

 所以我们采取中庸之道,就是把插入的定义改的宽松一些,可以让他不再那么高度必须最多差一层。维护平衡时的开销也减小一点。所以产生了红黑树。

1.红黑树的特点

1.1相关概念

红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须除了满足二叉搜索树的性质外,还要满足下面的性质:

  • 性质1:每个节点要么是黑色,要么是红色。
  • 性质2:根节点是黑色。
  • 性质3:每个红色结点的两个子结点一定都是黑色。
  • 性质4:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。(重要)
  • 性质5:每个叶子节点(NIL)是黑色。(这个不重要)

红黑树确保没有一条路径会比其他路径长出俩倍(最长路径不超过最短路径的2倍),因而是接近平衡的。

最短路径:全部由黑色节点构成。

最长路径:由一黑一红构成,且红色节点数量等于黑色节点数量。

假设黑色节点有N个,最短路径长度为最长路径长度为2*。但是在红黑树中,不一定有最短路径和最长路径。                   

1.2插入的结点的颜色

基本我们将插入的新节点改成红色,不改成黑色。

我们分析一下这两种情况;

如果插入的是红色,右边的情况不多,但是如果父节点是黑色就对了,红色可能错,这里他违反了的性质3.

但是如果插入的是黑色:

 这个没有违反性质3,但是违反了性质4任意一结点到每个叶子结点的路径都包含数量不相同的黑结点。违反性质3还可能对,但是违反性质4一定错,所以我们给插入的结点赋为红色。

2.红黑树的设计

2.1、结点设计

红黑树可以说是AVL树的兄弟篇,所以我们可以利用一下它兄弟的代码。

这里我们仍旧采用KV模型和三叉链形式设计,但是用不到平衡因子而是用枚举来表示颜色(RED,BLACK)。

enum Colour  
{
	RED,  // 0
    BLACK,  // 1
};
template<class K, class V>
struct RBTreeNode
{
	//三叉连结构
	RBTreeNode<K, V>* _left;//左孩子
	RBTreeNode<K, V>* _right;//右孩子
	RBTreeNode<K, V>* _parent;//父亲
	pair<K, V> _kv;

	Colour _cor;
	//构造函数
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_cor(RED)
	{}
};

这里我们用颜色_cor代替平衡因子,给颜色初始化成红色(原因看特点最后一步)。

2.2基本框架:

//红黑树的类
template <class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
    //……
private:
	Node* _root = nullptr;
};

老生常谈,不说了,直接上狠活。

3.红黑树插入 

3.1插入的介绍

插入的四大件:

  • 1.空节点: 开辟一个新节点,把kv给这个新节点。
  1. 开辟一个新节点。
  2. 手动让根节点的颜色给为黑色。
  • 2,递归找合适的插入位置。
  1. 如果要插入的数大于结点值,往右走,递归。
  2. 如果要插入的数小于结点值,往左走,递归。
  3. 插入的值重复,返回false。
  • 3.链接插入的结点和根节点。
  1. 插入的值小于父节点,链接在左边。
  2. 插入的值大于父节点,链接在右边。
  3. 把cur的_parent链接父节点。
  • 4.进行一系列调整。
  1. 插入结点的父节点是黑色,不用调整。
  2. 插入结点的父节点是红色,调整。(3.2结点调整)
bool Insert(const pair<K, V>& kv)
	{
		//1、一开始为空树,直接new新节点
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;//新插入的节点处理成黑色
			return true;
		}
		//2、寻找插入的合适位置
		Node* cur = _root;
		Node* parent = nullptr;
		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;//插入的值 = 节点的值,数据冗余插入失败,返回false
			}
		}
		//3、找到了插入的位置,进行父亲与插入节点的链接
		cur = new Node(kv);
		cur->_col = RED;//插入的节点处理成红色
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;//插入的值 > 父亲的值,链接在父亲的右边
		}
		else
		{
			parent->_left = cur;//插入的值 < 父亲的值,链接在父亲的左边
		}
		cur->_parent = parent;//三叉链,要双向链接

只需把空节点的_bf改为颜色,这里因为是根节点,所以我们手动改为黑色。

接下来我们看一下插入后如何改变。

3.2插入调整

上面我们也介绍了,如果插入结点的父节点是黑色,我们就不要调整,

父节点是红色就需要调整,那么怎么调整呢?

情况一:

cur为红,parent,grandp为黑,uncle1结点存在为红, 如果说a,bcde都是子树,,我们先不在子树中插入。

如何调整:

  1.  父节点是不是根节点:
  • 父节点是根节点:

把父节点的红色改成黑色:

  • 父节点是子树结点

继续向上调整。 

情况一不需要旋转,所以parent,uncle在左边还是右边不重要。cur在parent的那边也不重要。

情况2:

cur为红,parent,grandp为黑,uncle1结点不存在。

  • 一条龙服务:

如果像图中所画,cur一定是新插入的结点,要不然,cur,p的颜色一定不一样。

那这个不就是AVL树的右单旋吗?(cur插入到较高子树的左侧) ?所以我们先来个旋转,在变更颜色。

如果说p在g的右边,就进行左单旋。方向正好相反。 

  • 闪电型分布:三个结点不在一条线上

总结情况:

  1. p为g的左,cur为p的左,则进行右单旋 + p变黑,g变红
  2. p为g的右,cur为p的右,则进行左单旋 + p变黑,g变红
  3. p是g的左,cur是p的右,则进行左右双旋 + cur变黑, g变红
  4. p是g的右,cur是p的左,则进行右左双旋 + cur变黑, g变红

 情况3

cur为红,p为红,g为黑,u存在且为黑

  • 当p是g的左子树,cur是p的右子树时,将p左单旋,g右单旋,cur变黑,g变红

  •   当p是g的右子树,cur是p的左子树,p右单旋,g左单旋,p变黑,g变红。

其实情况2和情况3可以合并到一起,条件就是u不存在或者u存在但是为黑。

这里我们就不需要考虑u到底是存在还是颜色为黑就行了。

这里的插入分为两大类:

  • parent在grandfather的左边

1.情况一,u存在且为红,cur为红。

  • parent,uncle改变颜色为BLOCK,grandfather为RED

2.情况2+3,u不存在,u存在颜色为黑。

  • 1. c在p的左边,g,p,c一条线,右单旋+变色(p黑,g红)。
  • 2.c在p的右边   p左单旋+g右单旋+变色(cur黑,g红)

  • parent在grandfather的右边:

1.情况一,u存在且为红。cur为红

  • parent,uncle改变颜色为BLOCK,grandfather为RED

2.情况2+3,u不存在,u存在颜色为黑

  • 1. c在p的右边,g,p,c一条线,左单旋+变色(p黑,g红)。
  • 2.c在p的左边   p右单旋+g左单旋+变色(cur黑,g红)
	//4.进行修改和变色
		while (parent&& parent->_cor == RED)
		{
			Node* grandfather = parent->_parent;
			assert(grandfather);
			assert(grandfather->_cor==BLACK);
			if (parent == grandfather->_left)  //父亲在左,叔叔在右
			{
				Node* uncle = grandfather->_right;
				//1.uncle存在为红,cur,parent是红,gfather黑
				if(uncle&&uncle->_cor==RED)
				{
					//改色
					parent->_cor = uncle->_cor = BLACK;
					grandfather->_cor = RED;
					cur = grandfather;   //此时父亲是叶子节点
					parent = cur->_parent;      //继续往上走
				}
				//情况2+3  uncle不存在+uncle存在且为红
				else
				{
					//  情况2:右单旋+变色。大前提说了,p在g的zuobian
					//      g   
					//   p    (u) 可有可无
					//c
					//
					if (cur == parent->_left)
					{
						RotateR(grandfather);
						cur->_cor = grandfather->_cor = RED;
						parent->_cor = BLACK;
					}

					//  情况3:左单旋+右单旋+变色。大前提说了,p在g的zuobian
					//      g   
					//   p    (u) 可有可无
					//      c
					//
					else
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_cor = BLACK;
						grandfather->_cor  = RED;
					}
					break;
				}
			}
			else //(parent == grandfather->_right)  //父亲在右,叔叔在左
			{
				Node* uncle = grandfather->_left; //此时叔叔在左
				//1.uncle存在为红,cur,parent是红,gfather黑
				if (uncle&& uncle->_cor == RED)
				{
					//改色
					parent->_cor = uncle->_cor = BLACK;
					grandfather->_cor = RED;
					cur = grandfather->_parent;   //此时父亲是叶子节点
					parent = cur->_parent;      //继续往上走
				}
				//情况2+情况3
				else
				{
					//  情况2:左单旋+变色。大前提说了,p在g的zuobian
			      	//      g   
				    //  u      p   
				    //            c
				
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						cur->_cor = grandfather->_cor = RED;
						parent->_cor = BLACK;
					}

					//  情况3:右单旋+左单旋+变色。大前提说了,p在g的zuobian
					//      g   
					//   u      p   
					//        c 
					//
					else
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_cor = BLACK;
						grandfather->_cor = RED;
					}
					break;
				}
			}
		}
		_root->_cor = BLACK;
		return true;
	}

4.查找实现(Find) 

	//查找
	Node* FindR(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key)//key比当前节点小,就向右查找
			{
				cur = cur->_right;
			}
			else if (cur->_kv.first > key)//key比当前节点大,就向左查找
			{
				cur = cur->_left;
			}
			else//找到了
			{
				return cur;
			}
		}
		return nullptr;//空树,直接返回
	}

这里我们仍旧用中序遍历打印。不写了,浪费地方。

5.红黑树的平衡检查

其实检查的时候我们从5条定义入手:

  • 1.根节点为空,正确,是红黑树
  • 2.根节点颜色为红,错误,不是红黑树。
  • 3.从根节点出发的任意一条路径上的黑色节点数一样,我们定义一个balckNum来专门记录。
bool IsBalance()
	{
		if (_root == nullptr)
		{
			return true;
		}
		if (_root->_cor == RED)
		{
			cout << "根节点不是黑色" << endl;
			return false;
		}
		PrevCheck(_root, 0);
		return true;
	}
private:
	void PrevCheck(Node* root, int blackNum)
	{
		if (root == nullptr)
		{
			cout << blackNum << endl;
			return;
		}
		if (root->_cor == BLACK)
		{
			blackNum++;
		}
		PrevCheck(root->_left, blackNum);
		PrevCheck(root->_right, blackNum);
	}

 但是如果插入的数据过多。我们不想看,那能不能整合一下这帮伪军?

bool IsBalance()
	{
		
		if (_root == nullptr)
		{
			return true;
		}
		if (_root->_cor == RED)
		{
			cout << "根节点不是黑色" << endl;
			return false;
		}
		//选择任意一条路径的黑色节点数作为基准值
		int Benchmark = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_cor == BLACK)
			{
				Benchmark++;
			}
			else
				cur=cur->_left;
		}
		return PrevCheck(_root, 0, Benchmark);
		
	}
private:
	bool PrevCheck(Node* root, int blackNum, int Benchmark)
	{
		
		if (root == nullptr)
		{
			//cout << blackNum << endl;
			// return;
			if (blackNum != Benchmark)
			{
				cout << "某条黑色结点数量不相等";
				return false;
			}
			else
				return true;
		}
		if (root->_cor==BLACK)
		{
			blackNum++;
		}
		if (root->_cor == RED && root->_parent->_cor == RED)
		{
			cout << "存在两个红色结点" << endl;
			return false;
		}
		
		return PrevCheck(root->_left, blackNum, Benchmark)
		      &&PrevCheck(root->_right, blackNum, Benchmark);
	}

 删除操作我们就不写了,有兴趣的老铁可以上网去搜一下。

相关文章:

  • 提升C内功--函数栈帧的创建和销毁(动画讲解)
  • Buffer Pool Size of Total RAM No data
  • Python添加水印简简单单,三行代码教你批量添加
  • 微服务中间件
  • C语言学习-数组应用-三子棋(4.1)
  • java编程思想
  • HECTF2022
  • CTFshow web37 38 39 40
  • vue3项目,vite+vue3+ts+pinia(8)-开发和生产模式配置+跨域
  • 基于STM32-Socket-Qt 遥控小车(一代)
  • 对Java中的Exception(异常)机制的详细总结(大全)
  • 浏览器无痕模式有什么作用,手机浏览器开启无痕模式的方法
  • 猿创征文 | Devpos运维的10个日常使用工具分享
  • 基于IPv6的5G专网终端身份认证技术与应用
  • 3D开发学习之笛卡尔坐标系
  • 4. 路由到控制器 - Laravel从零开始教程
  • CSS3 变换
  • Java小白进阶笔记(3)-初级面向对象
  • java正则表式的使用
  • JS进阶 - JS 、JS-Web-API与DOM、BOM
  • Leetcode 27 Remove Element
  • 安卓应用性能调试和优化经验分享
  • 动态规划入门(以爬楼梯为例)
  • 高程读书笔记 第六章 面向对象程序设计
  • 函数式编程与面向对象编程[4]:Scala的类型关联Type Alias
  • 看图轻松理解数据结构与算法系列(基于数组的栈)
  • 码农张的Bug人生 - 见面之礼
  • 如何合理的规划jvm性能调优
  • 如何解决微信端直接跳WAP端
  • 微服务入门【系列视频课程】
  • 移动互联网+智能运营体系搭建=你家有金矿啊!
  • 异步
  • 摩拜创始人胡玮炜也彻底离开了,共享单车行业还有未来吗? ...
  • # Java NIO(一)FileChannel
  • (4)通过调用hadoop的java api实现本地文件上传到hadoop文件系统上
  • (done) NLP “bag-of-words“ 方法 (带有二元分类和多元分类两个例子)词袋模型、BoW
  • (多级缓存)缓存同步
  • (附源码)spring boot基于Java的电影院售票与管理系统毕业设计 011449
  • (经验分享)作为一名普通本科计算机专业学生,我大学四年到底走了多少弯路
  • (十)T检验-第一部分
  • (原創) 如何讓IE7按第二次Ctrl + Tab時,回到原來的索引標籤? (Web) (IE) (OS) (Windows)...
  • .NET Conf 2023 回顾 – 庆祝社区、创新和 .NET 8 的发布
  • .net core开源商城系统源码,支持可视化布局小程序
  • .NET Standard、.NET Framework 、.NET Core三者的关系与区别?
  • .NET 回调、接口回调、 委托
  • .Net(C#)自定义WinForm控件之小结篇
  • .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
  • .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间)...
  • .Net多线程总结
  • .net专家(张羿专栏)
  • @entity 不限字节长度的类型_一文读懂Redis常见对象类型的底层数据结构
  • @Resource和@Autowired的区别
  • [ C++ ] STL_stack(栈)queue(队列)使用及其重要接口模拟实现
  • [ 数据结构 - C++]红黑树RBTree
  • [2544]最短路 (两种算法)(HDU)