C++ 红黑树:红黑树的插入及应用(map与set的封装)

目录

红黑树

红黑树的概念

红黑树的性质

红黑树节点的定义

一、如果默认给黑色

二、如果默认给红色

红黑树的插入操作

1.按搜索树的规则进行插入

2.检测新节点插入后,红黑树的性质是否造到破坏

情况一:cur为红,parent为红,grandfather为黑,uncle存在且为红

情况二:cur 为红,parent 为红,grandfater 为黑,uncle不存在 / uncle存在且为黑

①cur 为红,parent 为红,grandfater 为黑,uncle不存在

②cur 为红,parent 为红,grandfater 为黑,uncle存在且为黑

③情况二的变形:双旋的情况

3.红黑树的插入的完整代码

红黑树的验证(红黑树插入实现的完整代码)

RBTree.h

test.cpp

红黑树与AVL树的比较

性能比较

红黑树模拟实现STL中的map与set

单树构建

红黑树模板参数的作用

结构改造与仿函数

myMap.h

mySet.h

RBTree.h

改造的过程中的问题(仿函数的使用)

​编辑仿函数的使用

myMap.h

mySet.h

RBTree.h

迭代器的实现

myMap.h

RBTree.h

operator++ 和operator--的实现

map模拟实现细节

operator[] 的实现

map和set模拟实现的完整代码

myMap.h

mySet.h

RBTree.h

test.cpp


红黑树

红黑树的概念

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


红黑树的性质

  • 左根右
    • 红黑树是二叉搜索树,节点值满足左子树小于根节点、根节点小于右子树的大小关系,方便查找等操作。
  • 根叶黑
    • 根节点和叶子节点(空节点)颜色为黑色,这有助于统一规则来维护树的平衡。
  • 不红红
    • 红黑树中不能有相邻的红色节点,防止树的局部过度生长而失去平衡。
  • 黑路同
    • 从根节点到叶子节点的所有路径上,黑色节点的数量是一样的,以此来限制路径长度差,保证树接近平衡。

有了这些性质,红黑树就能保证其最长路径中节点个数不会超过最短路径节点个数的2倍。

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

     因为 “黑路同” 的性质,无论哪条路径,黑色节点数量是固定的。在最短路径全是黑色节点的情况下,节点数量就是黑色节点的数量。

 

而在最长路径的极端情况(一黑一红交替)下,节点数量最多也就是黑色节点数量的两倍(每一个黑色节点后最多跟一个红色节点)。

 

所以综合来看,满足红黑树的这些性质(左根右、根叶黑、不红红、黑路同),就能保证其最长路径中节点个数不会超过最短路径节点个数的 2 倍。

注意:我们在数黑色节点个数的时候,每一条路径都需要算进去,把空节点当成黑色的叶子节点。

为什么要把空节点当成黑色节点也算成每条路径上的个数???

  • 红黑树的核心平衡规则是 “黑路同”,即从根节点到每个叶子节点的所有路径上,黑色节点的数量是相同的。如果不算空节点,这个规则就会被破坏。
  • 例如,考虑一个简单的红黑树结构,根节点为黑色,它有左右两个子树。左子树是一个完整的二叉树结构,右子树的最底层节点没有子节点(即有空节点)。如果我们在计算路径上黑色节点数量时不算空节点,那么从根节点到左子树最底层叶子节点的路径上黑色节点数量可能和到右子树非空节点的路径上黑色节点数量相同,但这忽略了右子树其实还可以延伸到空节点。算上空节点后,才能保证在任何情况下,所有路径(不管是否有实际节点延伸)上的黑色节点数量都能统一衡量,从而真正维护 “黑路同” 的性质。


红黑树节点的定义

我们这里实现 key-value 型的,在红黑树的节点中,就是多了一个标记颜色,通过标记颜色来调整红黑树的结构。

这里和AVL树一样还是给出三叉链的形式,因为当插入节点时候,如果破坏了红黑树的性质需要去找该节点的父节点及祖父节点。

//节点的颜色
enum Color
{
	BLACK,
	RED,
};
template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	pair<K, V> _kv; //保存每个节点的值
	Color _col; //每个节点都需要标记颜色
	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)   //默认给成红色
	{}
};

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

一、如果默认给黑色

 

若新插入节点默认设为黑色,很可能破坏红黑树 “黑路同” 性质(当插入是根节点时不会破坏,所以叫很可能),使某路径黑色节点数变多,导致整个树平衡被破坏,且几乎所有路径相关节点都需调整,维护难度和工作量极大。

二、如果默认给红色

 

新插入节点默认设为红色时:

 
  • 若父节点是黑色,不破坏现有性质,无需调整。
  • 若父节点是红色,虽违反 “不红红” 性质,但仅需对这条有问题的路径做调整,比如旋转、变色等,相比默认黑色时对所有路径调整,难度和工作量小很多。
 

所以从维护平衡及减少调整工作量看,节点默认颜色设为红色更合理。


红黑树的插入操作

1.按搜索树的规则进行插入

二叉搜索树的实现,我们已经写了无数遍了,红黑树的插入和二叉搜索的插入是一样的。

enum Color
{
	BLACK,
	RED,
};
template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	pair<K, V> _kv; //保存每个节点的值
	Color _col; //每个节点都需要标记颜色
	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)
	{}
};
template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
		//1、先按搜索树的规则进行插入
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;  // 插入的根节点变成黑色
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(kv);
		if (kv.first > parent->_kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
		//新增节点,给红色
		cur->_col = RED;
		//如果红黑树的性质遭到破坏...进行处理
		//....
		return true;
	}
private:
	Node* _root = nullptr;
};

2.检测新节点插入后,红黑树的性质是否造到破坏

我们要控制最长路径不超过最短路径的2倍,主要是控制红黑树的这几条性质就可以了,不违反这些规则就搞定了。

cur为当前节点,parent 为父节点,grandfather 为祖父节点,uncle 为叔叔节点

如果插入节点的时候,破坏了红黑树的性质,调整红黑树的关键看叔叔节点。

为什么当红黑树被破坏需要调整时,关键是看叔叔节点呢??

  1. 基于红黑树的性质和结构

    • 红黑树有 “不红红” 的性质,即不能出现连续的红色节点。当插入一个节点(设为cur)后,如果cur和它的父节点(parent)都是红色,就违反了这个性质,需要进行调整来恢复红黑树的平衡。
    • 在红黑树的结构中,parent节点是grandfather节点(祖父节点)的子节点,uncle节点是grandfather节点的另一个子节点(parent的兄弟节点)。uncle节点的颜色能反映出grandfather节点这一层及其子节点的颜色分布情况,这对于恢复平衡至关重要。
  2. 叔叔节点颜色决定调整策略

    • 叔叔节点为红色时
      • 如果uncle节点是红色,这意味着在grandfather节点这一层,有两个红色子节点(parentuncle)。这种情况下,红黑树在这局部区域的颜色分布是比较 “红” 的。
      • 为了恢复平衡,我们可以将parentuncle节点都变为黑色,grandfather节点变为红色。这样做不会改变从根节点到叶子节点的任何路径上的黑色节点数量(因为黑色节点数量的变化是在同一层内部进行调整的),很好地维护了 “黑路同” 的性质。同时,也解决了curparent同为红色的问题,恢复了 “不红红” 的性质。
    • 叔叔节点为黑色时
      • uncle节点是黑色时,说明grandfather节点这一层及其子节点的颜色分布不均匀。这种不均匀可能是由于新插入节点cur的位置导致了局部结构失衡。
      • 此时不能简单地通过变色来解决问题。因为如果只进行变色,可能会改变路径上的黑色节点数量,破坏 “黑路同” 的性质。所以需要进行旋转操作来调整树的结构。旋转的方向(左旋或右旋)和后续的变色操作取决于curparentgrandfather节点之间的位置关系。通过旋转和适当的变色,可以重新调整节点的位置关系,使红黑树恢复平衡,满足 “不红红” 和 “黑路同” 等性质。

因此,在插入节点破坏红黑树性质后,叔叔节点的颜色就像是一个 “指示器”,它能够帮助我们确定应该采取何种策略(变色还是旋转)来快速、有效地恢复红黑树的平衡。

情况一:cur为红,parent为红,grandfather为黑,uncle存在且为红

具象图:

插入节点破坏红黑树性质,叔叔节点为红色时:

 
  • 不能动新插入的 cur 节点颜色,否则类似直接插入黑色节点,易违 “黑路同” 性质。
  • 把 parent 和 uncle 变黑,能让相关两条路径各增一个黑节点,维持 “黑路同”。
  • 祖父节点多为非根情况,不变色会使它所在路径多一个黑节点,所以要变红,保证无连续红节点且黑节点数不变。
 

若祖父是根,变色后把根变回黑色就行。若祖父不是根,再看祖父的父亲颜色,黑则结束,红则继续往上处理。

可能需要继续往上处理:

抽象图:

通过上面这些情况,当父亲为红色,且叔叔存在为红,就可以构出抽象图了。

注意:需要注意的是,当和下面抽象图反方向的位置时,处理也是一样的。三者的相对位置没有改变。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值