C++ 红黑树(详解 完整代码)

目录

前言 

一、红黑树

1.1  红黑树的概念

1.2  红黑树的性质

1.3  红黑树节点的定义

1.4  红黑树的插入操作

1.4.1  按照二叉搜索的树规则插入新节点

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

1.4.2.1  情况一: cur为红,p为红,g为黑,u存在且为红

 1.4.2.2  情况二: cur为红(插入在两边),p为红,g为黑,u不存在/u存在且为黑

 1.4.2.3  情况三: cur为红(插入在中间),p为红,g为黑,u不存在/u存在且为黑

1.4.2.4  总结

二、红黑树完整代码以及验证

2.2  红黑树的删除

三、红黑树与AVL树的比较

五、 红黑树的应用

拓展


前言 

红黑树涉及旋转,旋转部分讲解在:C++ AVL树(详解 完整代码)-CSDN博客

一、红黑树

1.1  红黑树的概念

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

1.2  红黑树的性质

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

        2. 根节点是黑色的          

        3. 如果一个节点是红色的,则它的两个孩子结点是黑色的 

        4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点 

        5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

1.3  红黑树节点的定义

template<class T,class V>
// 这里也可以用struct
// struct默认是公有的,class默认私有所以加上public
class RBTreeNode
{
public:
	RBTreeNode* _pLeft;
	RBTreeNode* _pRight;
	RBTreeNode* parent;
	pair<T, V> _kv;
	colour col;

	RBTreeNode(const pair<T, V>& kv)
		:_pLeft(nullptr)
		, _pRight(nullptr)
		, parent(nullptr)
		, _kv(kv)
		, col(RED)
	{}
};

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

        因为插入一个红色结点 只影响当前路径,插入黑色这条路径的黑色节点数量变化,那么为了满足条件,别得路径也要修改,极其麻烦

1.4  红黑树的插入操作

红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:

1.4.1  按照二叉搜索的树规则插入新节点

if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->col = BLACK;
			return true;
		}

		// 不为空,先找到插入节点
		Node* cur = _root;
		Node* parent = nullptr;

		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_pRight;
			}
			else if(cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_pLeft;
			}
			else
			{
				return false;
			}
		}

		//	插入新节点(初始化为红色)并连接
		cur = new Node(kv);
		cur->col = RED;

		cur->parent = parent;
		if (cur->_kv.first < parent->_kv.first)
		{
			parent->_pLeft = cur;
		}
		else
		{
			parent->_pRight = cur;
		}

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

        因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色没有违反红黑树任何 性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连 在一起的红色节点,此时需要对红黑树分情况来讨论:

        约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

1.4.2.1  情况一: cur为红,p为红,g为黑,u存在且为红

        如果g是根节点,调整完成后,需要将g改为黑色如果

        g是子树,g一定有双亲,且g的双亲如果是红色,需要继续向上调整 (此时g相当于cur)

        解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。 

 1.4.2.2  情况二: cur为红(插入在两边),p为红,g为黑,u不存在/u存在且为黑

说明:  u的情况有两种
        1.如果u节点不存在,则cur一定是新插入节点,因为如果cur不是新插入节点则cur和p一定有一个节点的颜色是黑色,就不满足性质(4):每条路径黑色节点个数相同。
        2.如果u节点存在,则其一定是黑色的,那么cur节点原来的颜色一定是黑色的现在看到其是红色的原因是因为cur的子树在调整的过程中将cur节点的颜色由黑色改成红色。

解决方法:

        p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,

        p为g的右孩子,cur为p的右孩子,则进行左单旋转

        然后 p、g变色--p变黑,g变红

 1.4.2.3  情况三: cur为红(插入在中间),p为红,g为黑,u不存在/u存在且为黑

解决方法:

        p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反,

        p为g的右孩子,cur为p的左孩子,则针对p做右单旋转

此时就变成了情况2

1.4.2.4  总结

        // 如果为空树,创建新节点即可
        // 情况1: cur为红,parent为红,g为黑,u存在且为红
        // 情况2: cur为红(插入在两边),parent为红,g为黑,u不存在/u存在且为黑
        // 情况3: cur为红(插入在中间),p为红,g为黑,u不存在/u存在且为黑


        // 情况 二三可以合并 分为u不存在/u存在且为黑

        // 总结:所以都只需要判断Uncle存不不在,若存在是红还是黑皆可以了
        // 因此分为p在g的左还是右,在判断Uncle存不不在

解释:

       

 if(父亲在祖父的左)
{
    if(u存在且为红,为情况1)
    //下面为情况2和3
    ​​​​else (u不存在,u存在为黑)
    {
         if(插入在父亲的左:右旋)
        if(插入在父亲的右:左右双旋)
    }
           
}
 if(父亲在祖父的右)
{
    if(u存在且为红,为情况1)
    //下面为情况2和3
    ​else (u不存在,u存在为黑)
    {
            if(插入在父亲的右:左旋)
            if(插入在父亲的左:右左双旋)
    }   ​​​​​​​ 
}

二、红黑树完整代码以及验证

红黑树的检测分为两步:

        1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)

        2. 检测其是否满足红黑树的性质

#pragma once

enum colour
{
	RED,
	BLACK
};

template<class T,class V>
class RBTreeNode
{
public:
	RBTreeNode* _pLeft;
	RBTreeNode* _pRight;
	RBTreeNode* parent;
	pair<T, V> _kv;
	colour col;

	RBTreeNode(const pair<T, V>& kv)
		:_pLeft(nullptr)
		, _pRight(nullptr)
		, parent(nullptr)
		, _kv(kv)
		, col(RED)
	{}
};

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

	// 在红黑树中插入值为data的节点,插入成功返回true,否则返回false
	// 注意:为了简单起见,本次实现红黑树不存储重复性元素
	bool Insert(const pair<T, V>& kv)
	{
		// 如果为空树,创建新节点即可
		// 情况1: cur为红,parent为红,g为黑,u存在且为红
		// 情况2: cur为红(插入在两边),parent为红,g为黑,u不存在/u存在且为黑
		// 情况3: cur为红(插入在中间),p为红,g为黑,u不存在/u存在且为黑
		// 情况 二三可以合并 分为u不存在/u存在且为黑

		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->col = BLACK;
			return true;
		}

		// 不为空,先找到插入节点
		Node* cur = _root;
		Node* parent = nullptr;

		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_pRight;
			}
			else if(cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_pLeft;
			}
			else
			{
				return false;
			}
		}

		//	插入新节点(初始化为红色)并连接
		cur = new Node(kv);
		cur->col = RED;

		cur->parent = parent;
		if (cur->_kv.first < parent->_kv.first)
		{
			parent->_pLeft = cur;
		}
		else
		{
			parent->_pRight = cur;
		}

		//	如果p为黑,在他下面直接插入红就结束了
		if (parent->col == BLACK)
		{
			return true;
		}

		// 其余为p为红,两个红色相连,需要变更颜色
		// 父亲不存在也就是说,更新到根了,不需要在更新了,
		while (parent && parent->col == RED)
		{
			// 情况1: cur为红(插入在两边中间都可以),parent为红,g为黑,u存在且为红
			// 解决方式将p 和 U变为黑色,g变为红色,然后将 cur = g 继续向上更新颜色
			// 由于p为红,g一定为黑,

			// 情况二: cur为红,p为红,g为黑,u不存在 / u存在且为黑
			// 若uncle不存在,则cur就是新增节点

			// 总结:所以都只需要判断Uncle存不不在,若存在是红还是黑皆可以了
			// 因此分为p在g的左还是右,在判断Uncle存不不在

			Node* grandfather = parent->parent;
			if (parent == grandfather->_pLeft)
			{
				//       g
				//    p     u
				//   c (C)
				Node* uncle = grandfather->_pRight;
				// 情况1: cur为红(插入在两边中间都可以),parent为红,g为黑,u存在且为红
				// 解决方式将p 和 U变为黑色,g变为红色,然后将 cur = g 继续向上更新颜色
				// 由于p为红,g一定为黑,
				if (uncle && uncle->col == RED)
				{
					// 变色
					uncle->col = BLACK;
					parent->col = BLACK;
					grandfather->col = RED;

					// 继续往上更新处理
					cur = grandfather;
					parent = cur->parent;
				}
				//  情况二: cur为红,p为红,g为黑,u不存在 / u存在且为黑
				else
				{
					if (cur == parent->_pLeft)
					{
						// 单旋
						//     g
						//   p
						// c
						RotateR(grandfather);

						parent->col = BLACK;
						grandfather->col = RED;
					}
					else
					{
						// 双旋
						//     g
						//   p
						//     c
						RotateL(parent);
						RotateR(grandfather);

						cur->col = BLACK;
						grandfather->col = RED;
					}
					//情况二不需要更新,旋转变色后,并没有改变路径上黑色结点的数量
					// 跳出循环 
					break;
				}
			}
			else if(parent == grandfather->_pRight)
			{
				//      g
				//   u     p 
				//       (c) c

				// 情况1: cur为红(插入在两边中间都可以),parent为红,g为黑,u存在且为红
				// 解决方式将p 和 U变为黑色,g变为红色,然后将 cur = g 继续向上更新颜色
				// 由于p为红,g一定为黑,
				Node* uncle = grandfather->_pLeft;
				if (uncle && uncle->col == RED)
				{
					// 变色
					parent->col = uncle->col = BLACK;
					grandfather->col = RED;

					// 继续往上处理
					cur = grandfather;
					parent = cur->parent;
				}
				//  情况二: cur为红,p为红,g为黑,u不存在 / u存在且为黑
				else
				{
					//      g
					//    u   p 
					//          c
					//
					if (cur == parent->_pRight)
					{
						RotateL(grandfather);
						parent->col = BLACK;
						grandfather->col = RED;
					}
					else
					{
						//      g
						//    u    p 
						//        c
						RotateR(parent);
						RotateL(grandfather);
						cur->col = BLACK;
						grandfather->col = RED;
					}
					// 情况二不需要更新, 旋转变色后, 并没有改变路径上黑色结点的数量
					// 跳出循环 
					break;
				}
			}
		}
		// while结束以后
		// 以防更新到根,且把根变为了红色,直接把根变黑即可
		_root->col = BLACK;
		return true;
	}

	// 检测红黑树中是否存在值为data的节点,存在返回该节点的地址,否则返回nullptr
	Node* Find(const T& data)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < data)
			{
				cur = cur->_pRight;
			}
			else
			{
				cur = cur->_pLeft;
			}
		}
		return cur;
	}

	// 获取红黑树最左侧节点
	Node* LeftMost()
	{
		Node* cur = _root;
		while (cur->_pLeft)
		{
			cur = cur->_pLeft;
		}
		return cur;
	}

	// 获取红黑树最右侧节点
	Node* RightMost()
	{
		Node* cur = _root;
		while (cur->_pRight)
		{
			cur = cur->_pRight;
		}
		return cur;
	}

	 //检测红黑树是否为有效的红黑树,注意:其内部主要依靠_IsValidRBTRee函数检测
	bool IsValidRBTRee()
	{
		if (_root == nullptr)
			return true;

		//根必须为黑色
		if (_root->col == RED)
			return false;

		//参考值,选择最左边路径,
		// 因为每条路径黑色节点数量一样,
		// 用这条路径的数量与其他路径的数量对比即可
		int refVal = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->col == BLACK)
				++refVal;

			cur = cur->_pLeft;
		}

		int blacknum = 0;
		return _IsValidRBTRee(_root, blacknum, refVal);
	}
private:
	bool _IsValidRBTRee(Node* pRoot, size_t blackCount, size_t pathBlack)
	{
		if (pRoot == nullptr)
		{
			if (blackCount != pathBlack)
			{
				cout << "存在黑色节点数量不相等的路径" << endl;
				return false;
			}
			return true;
		}
		// 判断是否有连续的红结点
		// 父亲找孩子有两个方向比较麻烦
		// 用孩子找父亲只有一个方向
		if (pRoot->col == RED && pRoot->parent->col == RED)
		{
			cout << "有连续的红色节点" << endl;
			return false;
		}

		if (pRoot->col == BLACK)
		{
			++blackCount;
		}

		return _IsValidRBTRee(pRoot->_pLeft, blackCount, pathBlack)
			&& _IsValidRBTRee(pRoot->_pRight, blackCount, pathBlack);
	}
	// 左单旋
	void RotateL(Node* pParent)
	{
		Node* subR = pParent->_pRight;
		Node* subRL = subR->_pLeft;

		pParent->_pRight = subRL;
		subR->_pLeft = pParent;

		subR->parent = pParent->parent;
		pParent->parent = subR;

		if (subRL)
			subRL->parent = pParent;

		if (pParent == _root)
			_root = subR;
		else
		{
			if (subR->parent->_pLeft == pParent)
			{
				subR->parent->_pLeft = subR;
			}
			else
			{
				subR->parent->_pRight = subR;
			}
		}
	}
	// 右单旋
	void RotateR(Node* pParent)
	{
		Node* subL = pParent->_pLeft;
		Node* subLR = subL->_pRight;

		pParent->_pLeft = subLR;
		subL->_pRight = pParent;

		subL->parent = pParent->parent;
		pParent->parent = subL;

		if (subLR)
			subLR->parent = pParent;

		if (pParent == _root)
			_root = subL;
		else
		{
			if (subL->parent->_pLeft == pParent)
			{
				subL->parent->_pLeft = subL;
			}
			else
			{
				subL->parent->_pRight = subL;
			}
		}
	}
	// 为了操作树简单起见:获取根节点
	Node*& GetRoot()
	{
		return _root;
	}
private:
	Node* _root = nullptr;
};

2.2  红黑树的删除

有兴趣的同学可参考:《算法导论》或者《STL源码剖析》

红黑树 - _Never_ - 博客园

三、红黑树与AVL树的比较

        红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O($log_2 N$),红黑树不追 求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数, 所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红 黑树更多。

五、 红黑树的应用

1. C++ STL库 -- map/set、mutil_map/mutil_set

2. Java 库

3. linux内核

4. 其他一些库

拓展

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值