C++修炼:红黑树的模拟实现

Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客: <但凡.

我的专栏: 《编程之路》《数据结构与算法之美》《题海拾贝》《C++修炼之路》

欢迎点赞,关注!

目录

1、红黑树的概念

1.1、什么是红黑树

1.2、红黑树和AVL树的对比

1.3、红黑树复杂度

2、红黑树模拟实现

2.1、红黑树的结构

2.1、红黑树的插入

2.1.1、插入情况分析

一、叔叔存在且为红

二、叔叔存在且为黑或叔叔不存在,并且新增节点是父亲的左

三、叔叔存在且为黑或叔叔不存在,并且新增节点是父亲的右

2.2.2、插入代码实现

2.3、查找

2.4、检查是否为二叉树

2.5、其他

3、红黑树的应用


因为map和set的底层使用红黑树实现的,所以我们先讲解一下红黑树,并模拟实现一下红黑树,为下一期的map和set封装做铺垫。

1、红黑树的概念

1.1、什么是红黑树

**红黑树是一颗二叉搜索树,他的每个节点都有一个颜色,颜色只能是红色或黑色。**和AVL树一样,他也是依靠着几条规则来尽可能保证平衡,保证搜索效率的。我们开门见山直接介绍一下这几条规则:

(1)每个节点不是红色就是黑色(上面说过了)。

(2)根节点是黑色的。

(3)不能出现两个连续的红色节点(如果父亲是红色,孩子一定不是红色)。

(4)从根节点到任意一个NULL空节点的路线上所含有的黑色节点数量相等。

当然由于红黑树是一颗二叉搜索树,所以他也具有二叉搜索树的特点。

基于以上四点规则,红黑树能保证从根节点到NULL节点的最长路径不会超过最短路径的两倍。

我先给大家透露一点,红黑树也是通过各种旋转来保证平衡,只不过是他不再依靠平衡因子了,而是依靠颜色。红黑树的平衡性没有AVL那么极致,这意味着插入同样的节点构建一棵红黑树需要的旋转次数更少。

一棵红黑树:

1.2、红黑树和AVL树的对比

有些区别我们说过了,我们说一下为什么红黑树的查找比AVL树稍慢。

其实核心原因还是因为红黑树的树高可能会比AVL树高,因为他不是严格控制平衡。由于搜索(查找)只能是从头到尾遍历这颗树,如果树高更高的话,不可避免的搜索路径更长,效率更低。

对于100万个节点的树,AVL树高度约20层,而 红黑树高度最多约四十层。

1.3、红黑树复杂度

空间复杂度:O(N),时间复杂度:插入,查找,删除都是O(logN)。

2、红黑树模拟实现

又到了经典的模拟实现环节哈哈。

2.1、红黑树的结构

cpp 复制代码
enum Colour
{
	RED,
	BLACK
};
template<class K,class V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col;

	RBTreeNode(const pair<K,V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_col(RED)
	{}
};
template<class K,class V>
class RETree
{
	typedef RBTreeNode<K, V> Node;
public:
	
private:
	Node* _root = nullptr;
};

**和AVL树的节点类似,只不过是把平衡因子换成了颜色标记。**另外就是我们这里实现的红黑树默认是key_value类型的,为的是方便后面map的封装(没错,set也是用的key_value版本的红黑树)。但是所有的比较都是按照key来比较的。

2.1、红黑树的插入

红黑树的插入可以说是最最核心的部分了,所以说我们先分析一下再模拟实现。

2.1.1、插入情况分析

插入情况有三种,我们这三种情况的区分是按照叔叔(父亲的兄弟节点)的颜色和是否存在,以及父亲节点和新增节点的位置关系进行区分的。 其实严格来讲有六种 ,因为父亲有可能是爷爷的左,也可能是爷爷的右,但是前三种和后三种其实是非常类似的,只是方向换了一下

首先我要说明一点,我们所有的新增节点默认都是红色的,因为对于新插入的节点,如果是红色节点他破坏的是规则三,如果插入的是黑色节点,破坏的是规则四,很显然规则四比规则三更难调整。

一、叔叔存在且为红

这是最简单的一种情况,只变色不旋转。

当父亲是爷爷的左的时候情况一样,就是单纯变色,不画图了。

二、叔叔存在且为黑或叔叔不存在,并且新增节点是父亲的左

当父亲节点是爷爷节点的左,这种情况是单旋加变色:

(我拿最简单的情况来做说明了)

在上面这种情况下,我们先对爷爷就行右旋,然后再进行变色,把父亲变成黑色,爷爷变成红色。

和他类似的,当父亲节点是爷爷的右子树,进行如下操作:

三、叔叔存在且为黑或叔叔不存在,并且新增节点是父亲的右

首先第一种情况是父亲是爷爷的左

如果父亲是爷爷的右

写到这里大家也应该发现了,我们旋转的情况和AVL树是一样的,**如果是单纯的一边高,我们就单旋,如果是左子树的右子树高或者右子树的左子树高,也就是不是单纯一边高的,就进行双旋。**除此之外的不同就是我们把平衡因子的调整换成了颜色的调整。

好了以上就是红黑树插入调整的大概几种情形,当然还有叔叔存在且为黑的情况我没画图,因为在代码实现上,叔叔存在且为黑和叔叔不存在属于一种情况,所以说就不画图了(其实是我很懒)。

接下来我们梳理一下这几种情况:

一、如果父亲是爷爷的左节点

(1)如果叔叔存在且为红

单纯调色

(2)如果叔叔不存在或叔叔存在且为黑

1、如果新增节点是父亲的左

单旋加变色。

2、如果新增节点是父亲的右

双旋加变色。

二、如果父亲是爷爷的右节点

(1)如果叔叔存在且为红

单纯调色

(2)如果叔叔不存在或叔叔存在且为黑

1、如果新增节点是父亲的右

单旋加变色。

2、如果新增节点是父亲的左

双旋加变色。

2.2.2、插入代码实现

cpp 复制代码
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);
	cur->_col = RED;//讲解
	//先插入再调整
	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)
				{
					RotateR(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else
				{
					RotateL(parent);
					RatateR(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
			{
				if (cur == parent->_right)
				{
					RatateL(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else
				{
					RotateR(parent);
					RotateL(grandfather);

					cur->_col = BLACK;
					grandfather->_col = RED;
				}

				break;
			}
		}
	}

	//都更改完后需要调整根节点的颜色,因为根节点颜色可能被改变了
	_root->_col = BLACK;
}

**如果父亲存在并且父亲的颜色为红,说明此时有两个连续的红,我们就进行调整。**这里说一点为什么循环条件中没有判断爷爷是否存在。首先我们设想爷爷不存在的场景,即父亲是根节点。此时爷爷的确不存在,但是由于父亲的颜色一定是黑色的(在此之前我们无法破坏这一点规则),所以说循环一定进不去。也就不必判断爷爷是否存在了。

还有一个特别特别需要注意的点,如果我们是旋转调整(即第二三中情况),在调整完之后直接退出循环,因为此时我们的爷爷是黑色的,而在第一种情况的变色下爷爷是红色的,如果爷爷是黑色不会破坏规则四,就不要继续循环了。

以下是两种旋转的代码,和之前AVL树的旋转完全一样,不作解释了直接放在这里:

cpp 复制代码
void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

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

		Node* ppnode = parent->_parent;

		subL->_right = 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;
		}
	}

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

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

		Node* ppnode = parent->_parent;

		subR->_left = 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;
		}
	}

2.3、查找

和之前树的查找一样,不多说了。

cpp 复制代码
Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key)
			{
				cur = cur->_right;
			}
			else if (cur->_kv.first > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}

		return nullptr;
	}

2.4、检查是否为二叉树

这个检查不是重点,不做讲解。

cpp 复制代码
bool Check(Node* root, int blackNum, const int refNum)
	{
		if (root == nullptr)
		{
			// 前序遍历走到空时,意味着一条路径走完了
			if (blackNum != refNum)
			{
				cout << "存在黑色结点的数量不相等的路径" << endl;
				return false;
			}

			return true;
		}

		// 检查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲就方便多了
		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << root->_kv.first << "存在连续的红色结点" << endl;
			return false;
		}

		if (root->_col == BLACK)
		{
			++blackNum;
		}

		return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
	}

	bool IsBalanceTree()
	{
		if (_root == nullptr)
			return true;

		if (_root->_col == RED)
			return false;

		// 参考值
		int refNum = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				++refNum;
			}
			cur = cur->_left;
		}

		return Check(_root, 0, refNum);
	}

2.5、其他

以下是二叉树节点数,二叉树高度,二叉树先序遍历的代码,很早之前在数据结构篇的二叉树部分就说过了,不做解析。

cpp 复制代码
int _Size(Node* root)
	{
		return root == nullptr ? 0 :
			_Size(root->_left) + _Size(root->_right) + 1;
	}

	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);

		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}

3、红黑树的应用

  1. C++ STL中的map和set:通常使用红黑树实现(下一期就是map和set模拟实现)

  2. Linux内核:进程调度、内存管理等

  3. 数据库系统:索引结构

  4. 实时系统:需要保证最坏情况下性能的场景

好了,今天的内容就分享到这,我们下期再见!

相关推荐
Funny-Boy3 分钟前
初识main函数
汇编·c++
陈天伟教授7 分钟前
Web前端开发 - 制作简单的焦点图效果
java·开发语言·前端·前端开发·visual studio
不吃肘击17 分钟前
MyBatisPlus使用教程
java·开发语言
阿方.91827 分钟前
《C 语言内存函数超详细讲解:从 memcpy 到 memcmp 的原理与实战》
c语言·开发语言·c++
水花花花花花42 分钟前
线性代数基础
线性代数·算法·机器学习
zeijiershuai1 小时前
Mybatis-入门程序、 数据库连接池、XML映射配置文件、MybatisX
xml·java·开发语言·mybatis
codists1 小时前
《算法导论(第4版)》阅读笔记:p115-p126
算法
BanyeBirth1 小时前
C++滑动门问题(附两种方法)
开发语言·c++
会开花的二叉树1 小时前
哈希表的实现(上)
数据结构·散列表
远瞻。1 小时前
【论文精读】2022 CVPR--RealBasicVSR现实世界视频超分辨率(RealWorld VSR)
论文阅读·算法·超分辨率重建