【C++】map与set底层结构——红黑树

目录

前言

1.红黑树

1.1红黑树的概念

1.2红黑树的规则

2.红黑树的实现

2.1红黑树的结构

2.2红黑树的插入

2.2.1变色

2.2.2单旋+变色

2.2.3双旋+变色

2.2.4插入代码

2.3红黑树的查找

2.4红黑树的验证

3.测试代码


前言

map与set作为STL中自带的搜索结构,其底层依托于红黑树而实现。本文将详细讲解红黑树的概念及其实现逻辑,其中部分基础逻辑(如旋转)可参考AVL树的实现。更多C++内容可前往>| C++专栏 |<


1.红黑树

1.1红黑树的概念

红⿊树是⼀棵⼆叉搜索树,他的每个结点增加⼀个存储位来表⽰结点的颜⾊,可以是红⾊或者⿊⾊。 通过对任何⼀条从根到叶⼦的路径上各个结点的颜⾊进⾏约束,红⿊树确保没有⼀条路径会⽐其他路径⻓出2倍,因⽽是接近平衡的。

1.2红黑树的规则

1.每个节点不是红色就是黑色

2.根节点是黑色的

3.如果⼀个节点是红⾊的,则它的两个孩⼦节点必须是⿊⾊的,也就是说任意⼀条路径不会有连续的红⾊节点。

4.对于任意⼀个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的黑色节点


2.红黑树的实现

2.1红黑树的结构

红黑树整体还是搜索树的一种,但每个节点除了存储键值、左子节点和右子节点指针外,还需要额外存储一个颜色属性。这里的颜色属性可用枚举值来表示。

cpp 复制代码
#pragma once
#include<iostream>
using namespace std;

enum Color
{
	RED,
	BLACK
};

template<class K, class V>
class RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode* _left;
	RBTreeNode* _right;
	RBTreeNode* _parent;
	Color _col;

	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
	{ }
};

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:

private:
	Node* _root = nullptr;
};

2.2红黑树的插入

1.红黑树插入情况分析

1.大体插入按二叉搜索树的规则插入,插入完看是否符合红黑树的规则

2.当为空树插入时,新增节点为黑色节点;如果是非空树插入,新增节点必须是红色节点(若为黑色节点,则破坏规则4,更难处理)

3.⾮空树插⼊后,新增结点必须红⾊结点,如果⽗亲结点是⿊⾊的,则没有违反任何规则,插⼊结束

4.⾮空树插⼊后,新增结点必须红⾊结点,如果⽗亲结点是红⾊的,则违反规则3,此时须进行特殊处理

对情况4进行进一步分析:

例如在该树中插入一个新节点,其中cur表示新增节点,parent表示新增节点的父亲,grandpa表示新增节点的父亲的父亲,uncle表示parent的相邻节点。如何parent为黑则插入结束,如果parent为红则grandpa必定为黑,此时再将这棵树单独拿出来:

此时根据分析cur为红,parent为红,则grandpa必为黑,最终问题情况就要由uncle颜色来确定:

2.2.1变色

情况1:cur为红,parent为红,grandpa为黑,uncle存在且为红。

此时的情况就与上图的情况相同,这时新增节点为红色,那么只需要将parent节点与uncle节点都变为黑,grandpa变为红即可。

即将grandpa的黑色分散到两个子路径中,同时满足规则3与规则4。但要注意的是若此时grandpa为根节点则需将其再变为黑色;若不为根,则继续向上更新。

此为具体图分析,下面展示抽象图。

此时再回到更新后的树:

可以看到仍然存在违反规则3的情况,下面继续分析

情况2:cur为红,parent为红,grandpa为黑,uncle不存在或存在且为黑。

若uncle不存在则cur必为新增节点;若uncle存在且为黑则cur必不为新增节点(如上图),一定是通过变色变为红色。

此时必须要将parent变为黑才能解决连续红色的问题,但因uncle不存在或为黑,无法单纯通过变色解决问题,此时我们可以根据AVL树的旋转情况以grandpa为根进行旋转。

2.2.2单旋+变色

如果p是g的左,c是p的左,那么以g为旋转点进⾏右单旋,再把p变⿊,g变红即可。

如果p是g的右,c是p的右,那么以g为旋转点进⾏左单旋,再把p变⿊,g变红即可。

2.2.3双旋+变色

如果p是g的左,c是p的右,那么先以p为旋转点进⾏左单旋,再以g为旋转点进⾏右单旋,再把c变⿊,g变红即可。

如果p是g的右,c是p的左,那么先以p为旋转点进⾏右单旋,再以g为旋转点进⾏左单旋,再把c变⿊,g变红即可。

2.2.4插入代码

cpp 复制代码
bool insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
		return true;
	}

	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		parent = cur;
		if (kv.first < cur->_kv.first)
		{
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first)
		{
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}
	cur = new Node(kv);
	cur->_col = RED;
	if (kv.first < parent->_kv.first)
		parent->_left = cur;
	else
		parent->_right = cur;
		cur->_parent = parent;

	while (parent && parent->_col == RED)
	{
		Node* grandpa = parent->_parent;

		
		if (parent == grandpa->_left)
		{
			Node* uncle = grandpa->_right;
			//uncle存在且为红
			if (uncle && uncle->_col == RED)
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandpa->_col = RED;

				cur = grandpa;
				parent = cur->_parent;
			}

			//uncle不存在或uncle存在且为黑
			else
			{
				if (cur == parent->_left)
				{
					rotateR(grandpa);

					parent->_col = BLACK;
					grandpa->_col = RED;
				}

				else
				{
					rotateLR(grandpa);

					parent->_col = BLACK;
					grandpa->_col = RED;
				}
			}
		}

		else
		{
			Node* uncle = grandpa->_left;

			//uncle存在且为红
			if (uncle && uncle->_col == RED)
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandpa->_col = RED;

				cur = grandpa;
				parent = cur->_parent;
			}

			//uncle不存在或uncle存在且为黑
			else
			{
				if (cur == parent->_right)
				{
					rotateL(grandpa);

					parent->_col = BLACK;
					grandpa->_col = RED;
				}

				else
				{
					rotateRL(grandpa);

					parent->_col = BLACK;
					grandpa->_col = RED;
				}
			}
		}
	}

	_root->_col = BLACK;//确保根节点一直为黑

	return true;
}

2.3红黑树的查找

红黑树查找逻辑与二叉搜索树查找逻辑相同:

cpp 复制代码
//查找
Node* find(const pair<K, V>& x)
{
	Node* cur = _root;
	while (cur)
	{
		if (x.first < cur->_kv.first)
		{
			cur = cur->_left;
		}

		else if (x.first > cur->_kv.first)
		{
			cur = cur->_right;
		}

		else
		{
			return cur;
		}
	}

	return nullptr;
}

2.4红黑树的验证

这⾥获取最⻓路径和最短路径,检查最⻓路径不超过最短路径的2倍是不可⾏的,因为就算满⾜这个条件,红⿊树也可能颜⾊不满⾜规则,当前暂时没出问题,后续继续插⼊还是会出问题的。所以我们还是去检查4点规则,满⾜这4点规则,⼀定能保证最⻓路径不超过最短路径的2倍。

红黑树4点规则检查

  1. 规则1枚举颜⾊类型,天然实现保证了颜⾊不是⿊⾊就是红⾊。

  2. 规则2直接检查根即可

  3. 规则3前序遍历检查,遇到红⾊结点查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲的颜⾊就⽅便多了。

  4. 规则4前序遍历,遍历过程中⽤形参记录跟到当前结点的blackNum(⿊⾊结点数量),前序遍历遇到⿊⾊结点就++blackNum,⾛到空就计算出了⼀条路径的⿊⾊结点数量。任意⼀条路径⿊⾊结点数量作为参考值,依次⽐较即可。

代码如下:

cpp 复制代码
bool Check(Node* cur, int blackNum, const int blackNumRef)
{
	if (cur == nullptr)
	{
		if (blackNum != blackNumRef)
		{
			cout << "黑色节点的数量不相等" << endl;
			return false;
		}

		return true;
	}

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

	if (cur->_col == BLACK)
		++blackNum;

	return Check(cur->_left, blackNum, blackNumRef)
		&& Check(cur->_right, blackNum, blackNumRef);
}

//验证
bool isBalance()
{
	if (_root == nullptr)
		return true;

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

	// 黑色节点数量参考值
	Node* leftMost = _root;
	int blackRef = 0;
	while (leftMost)
	{
		if (leftMost->_col == BLACK)
			++blackRef;

		leftMost = leftMost->_left;
	}

	return Check(_root, 0, blackRef);
}

3.测试代码

cpp 复制代码
#include"RBTree.h"
#include<iostream>
using namespace std;

void TestRBTree1()
{
	RBTree<int, int> t;
	// 常规的测试用例
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	// 特殊的带有双旋场景的测试用例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		if (e == 14)
		{
			int x = 0;
		}

		t.insert({ e, e });
		//t.InOrder();
		//cout << "Insert:" << e << "->" << t.IsBalanceTree() << endl;
	}
	t.inOrder();
	cout << t.isBalance() << endl;
}

int main()
{
	TestRBTree1();

	return 0;
}
相关推荐
数字化顾问2 小时前
“AMQP协议深度解析:消息队列背后的通信魔法”之核心概念与SpringBoot落地实战
开发语言·后端·ruby
悠哉悠哉愿意2 小时前
【数据结构与算法学习笔记】双指针
数据结构·笔记·python·学习·算法
MoRanzhi12032 小时前
5. Pandas 缺失值与异常值处理
数据结构·python·数据挖掘·数据分析·pandas·缺失值处理·异常值处理
haing20192 小时前
使用matlab进行牛顿迭代求函数极值的方法
开发语言·matlab·牛顿迭代
kernelknight12 小时前
MATLAB For循环详解:从入门到精通的完整指南
开发语言·其他·matlab
Cx330❀3 小时前
《C++ STL:vector类(下)》:攻克 C++ Vector 的迭代器失效陷阱:从源码层面详解原理与解决方案
开发语言·数据结构·c++·经验分享·算法
user_huenquan3 小时前
胡恩全10.3作业
开发语言·c++
charlie1145141913 小时前
Windows 10系统编程——进程专题:枚举我们进程的状态
c++·windows·学习·操作系统·进程
bawangtianzun3 小时前
树的重心与直径 性质
数据结构·c++·学习·算法