【C++高阶数据结构】红黑树

前言:

前面我们已经理解并实现了AVL树,不难发现:AVL树对其自身结构有非常严格的要求,即任意节点的左右子树高度差不能超过1,所以,又有人提出了红黑树这样的数据结构,但AVL树与红黑树都遵循二叉搜索树的规则。

🚀直通车:《我的数据结构专栏》

一、什么是红黑树?

1.1、红黑树概念

红黑树是一棵二叉搜索树,他的每个结点增加一个存储位来表示结点的颜色,可以是红色或者黑色。 通过对任何一条从根到叶子的路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路 径长出2倍,因而是接近平衡的。

1.2、红黑树规则

根结点为黑色;

• 每个结点不是黑色就是红色;

如果结点为红色,那么该节点的两个孩子节点为黑色,即任意一条路径上没有连续的红色节点

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

***思考:***红黑树如何确保最长路径不超过最短路径的2倍的?

答:从根结点开始的一条路径上只有n个黑色结点,由红黑树规则,两条路径上黑色结点数相同,且红色结点不连续,则当另一条路径上黑色结点与红色结点相间分布时,有最长长度为2n,这就保证了最长路径始终不超过最短路径的两倍

1.3、红黑树的效率

假设N是红黑树树中结点数量,h最短路径的长度,那么2^h − 1 <= N < 2^(2∗h) − 1 , 由此推出

h ≈ logN ,也就是意味着红黑树增删查改最坏也就是走最长路径 2 ∗ logN,那么时间复杂度还是 O(logN)

二、红黑树的实现

说明:我们以实现一个键值对(key_value)类型的红黑树,且数据不支持冗余。

2.1 红黑树节点结构定义

对于结点,我们需要一个pair来存储键值对;left指针指向左孩子结点;right指针指向右孩子结点;color变量存储结点颜色;后面插入结点时,如果需要调整平衡,则要频繁地访问父亲结点,所以还需要一个parent指针指向父亲结点(与AVL数相同)。

由于结点颜色只有黑或者红,而enum(枚举类型)可以用于定义**固定集合常量,**所以可以将结点颜色存储在一个枚举类型中。

cpp 复制代码
enum Color
{
	RED,
	BLACK
};

节点结构:

cpp 复制代码
template<class K,class V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

	Color _col;

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

2.2、红黑树的结构

cpp 复制代码
template<class K,class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
    // ...
private:
	Node* _root = nullptr; // 根结点
};

2.3、插入

当为空树时,插入节点作为根结点且颜色为黑;不为空时,插入结点就要满足红黑树的规则,如果插入黑色结点,则会打破规则四(对于任意一个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的黑色结点),所以,新插入的结点颜色一定为红色。

而对于新插入节点的父亲结点(parent)和父亲结点的父亲节点(grandfather)颜色,进一步分析,若parent为黑,则直接插入;若parent为红,则grandfather一定黑,插入新节点(红),违反规则,需要处理。此时,就需要根据父亲结点的兄弟结点的状态来进一步分情况讨论:

说明:下图中假设我们把新增结点标识为c (cur),c的父亲标识为p(parent),p的父亲标识为 g(grandfather),p的兄弟标识为u(uncle)。

当新插入结点的parent为红,即出现连续的红色结点,需要处理,大概分为下面两种情况:

情况一:u结点存在且为红

• u 为右孩子:

• u 为左孩子

情况一又根据 u 结点是左孩子还是右孩子分为两种情况,但是,对于这两种情况处理方式相同,即将 g 变为红,u 和 p 变为黑。但是,需要注意一点:g 变为红色后,也有可能 g 的父亲结点为红色,所以需要继续向上处理,即 c 指向 g ,p 和 g 同时更新,直到所有结点满足红黑树规则。

情况二:u结点存在为黑或不存在

此时 u 结点对于红黑树调整没有影响,但是需要考虑 p 和 c 结点的位置。

• p 为右孩子且 c 为右孩子(左单旋+变色)

• p 为右孩子且 c 为左孩子(双旋+变色)

• p 为左孩子且c 为左孩子(右单旋+变色)

• p 为左孩子且c 为右孩子(双旋+变色)

代码实现:

对于旋转调整平衡我们在前面的AVL树中已经做了详细地介绍,如果有什么问题,大家可以移步:【数据结构】AVL树:从原理到旋转平衡艺术

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 (kv.first < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first)
		{
			parent = cur;
			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* grandfather = parent->_parent;

        // -------------处理所有 p 为左孩子的情况---------------------------
		if (parent == grandfather->_left)
		{
			//     g
			//   p   u
			Node* uncle = grandfather->_right;
			
			if (uncle && uncle->_col == RED) // ----------叔叔存在且为红(情况一)
			{
				// -----------------变色--------------------------
				grandfather->_col = RED;
				parent->_col = BLACK;
				uncle->_col = BLACK;

				// -----------------继续向上处理--------------------
				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 // ---------------------------处理所有 p 为右孩子的情况
		{
			//     g
			//   u   p
			Node* uncle = grandfather->_left;

			if (uncle && uncle->_col == RED) // ----------叔叔存在且为红(情况一)
			{
				// 变色
				grandfather->_col = RED;
				parent->_col = BLACK;
				uncle->_col = BLACK;

				// 继续向上调整
				cur = grandfather;
				parent = cur->_parent;
			}

			else // -----------------------------------叔叔不存在或者为黑(情况二)
			{
				if (cur == parent->_right) 
				{
					//     g
					//   u   p
					//         c
					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;
}
左旋:

相比于AVL树,旋转代码只需要去掉修改平衡因子的代码即可

cpp 复制代码
void RotateL(Node* parent)
{
	Node* SubR = parent->_right;
	Node* SubRL = SubR->_left;
	Node* pParent = parent->_parent;

    // --------------连接parent与SubRL----------------
	parent->_right = SubRL;
	if (SubRL)
		SubRL->_parent = parent;
    // --------------连接parent与SubR----------------
	SubR->_left = parent;
	parent->_parent = SubR;
    
    // -----------------------连接SubR与pParent
	if (pParent == nullptr) // -------------------判断parent是否为根结点
	{
		_root = SubR;
		SubR->_parent = nullptr;
	}
	else
	{
		if (parent == pParent->_left)
		{
			pParent->_left = SubR;
		}
		else
		{
			pParent->_right = SubR;
		}
		SubR->_parent = pParent;
	}
}
右旋:
cpp 复制代码
void RotateR(Node* parent)
{
	Node* SubL = parent->_left;
	Node* SubLR = SubL->_right;
	Node* pParent = parent->_parent;
    
    // ----------------------连接SubLR与parent-----------------------
	parent->_left = SubLR;
	if (SubLR)
		SubLR->_parent = parent;
 
    // ----------------------连接SubL与parent-----------------------
	SubL->_right = parent;
	parent->_parent = SubL;

    // -----------------------连接SubL与pParent
	if (parent == _root) // -------------------判断parent是否为根结点
	{
		_root = SubL;
		SubL->_parent = nullptr;
	}
	else
	{
		if (pParent->_left == parent)
		{
			pParent->_left = SubL;
		}
		else
		{
			pParent->_right = SubL;
		}
		SubL->_parent = pParent;
	}
}

2.4、查找

遍历红黑树即可

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.5、判断平衡(验证)

cpp 复制代码
bool IsBalanceTree()
{
	if (_root == nullptr) //--------------空树也是红黑树
	{
		return true;
	}
	if (_root->_col == RED) // -------------根结点为红色
	{
		return false;
	}
	int hb = CountBlackNode(); // --------------获取基准值
	return _IsbalanceTree(_root,0, hb); // ------------调用子函数
}

// ---------------按左边或者最右边一条路径来统计黑色节点的数量,作为标准----------------------
int CountBlackNode()
{
	Node* cur = _root;
	int count = 0;
	while (cur)
	{
		if (cur->_col == BLACK)
		{
			count++;
		}
		cur = cur->_left;
	}
	return count;
}

// 将每一条路径上的黑色节点数作为一个参数,将上面得到的黑色节点数(基准)作为参数用来和blacknum做对比
bool _IsbalanceTree(Node* root,int balackNum,int hb)
{
	if (root == nullptr)
	{
		// ------------------前序遍历走到空时,意味着一条路径走完了---------------------
		if (balackNum != hb)
		{
			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)
	{
		balackNum++;
	}

	return _IsbalanceTree(root->_left, balackNum, hb) && _IsbalanceTree(root->_right, balackNum, hb);
}
相关推荐
Qiuner3 小时前
《掰开揉碎讲编程-长篇》重生之哈希表易如放掌
数据结构·算法·leetcode·力扣·哈希算法·哈希·一文读懂
艾莉丝努力练剑3 小时前
【C++模版进阶】如何理解非类型模版参数、特化与分离编译?
linux·开发语言·数据结构·c++·stl
立志成为大牛的小牛3 小时前
数据结构——二十五、邻接矩阵(王道408)
开发语言·数据结构·c++·学习·程序人生
cici158743 小时前
基于MATLAB的ADS-B接收机卫星与接收天线初始化实现
算法·matlab
编程岁月4 小时前
java面试-0215-HashMap有序吗?Comparable和Comparator区别?集合如何排序?
java·数据结构·面试
木井巳4 小时前
[Java数据结构与算法]详解排序算法
java·数据结构·算法·排序算法
美狐美颜SDK开放平台4 小时前
直播美颜SDK功能开发实录:自然妆感算法、人脸跟踪与AI美颜技术
人工智能·深度学习·算法·美颜sdk·直播美颜sdk·美颜api
缓风浪起5 小时前
【力扣】2011. 执行操作后的变量值
算法·leetcode·职场和发展
gsfl5 小时前
双指针算法
算法·双指针