C++——红黑树(带头结点)

红黑树的概念

红黑树的定义

红黑树是一种自平衡的二叉搜索树,具有以下几个重要特性和规则,确保其在插入、删除和查找操作中的时间复杂度保持在 (O(\log n))。

红黑树的基本概念

节点颜色:每个节点都有一个颜色属性,可以是红色或黑色。

节点结构:每个节点除了存储数据,还包含指向左右子节点和父节点的指针,以及颜色信息。

红黑树的性质

红黑树遵循以下五个性质:

节点颜色

  • 每个节点要么是红色,要么是黑色。

根节点

  • 根节点始终是黑色。

叶子节点

  • 所有的叶子节点(NIL节点,也称为哨兵节点)都是黑色。

红色节点的性质

  • 如果一个节点是红色,则其两个子节点必须是黑色(即没有两个连续的红色节点)。

黑色高度

  • 从任何节点到其每个叶子节点的所有路径都包含相同数量的黑色节点,这个数量称为该节点的“黑色高度”。

红黑树的优点

平衡性:由于上述性质,红黑树保持了相对平衡,能够在最坏情况下保证操作的时间复杂度为 (O(\log n))。

灵活性:红黑树允许插入和删除操作的灵活性,同时不会导致树的不平衡。

操作原理

在红黑树中进行插入和删除操作时,可能会违反上述性质,需要通过旋转和重新着色来恢复这些性质。主要操作包括:

旋转

  • 左旋和右旋是两种基本操作,用于调整树的结构,以保持平衡。

重新着色

  • 在插入或删除后,通过改变节点的颜色来恢复红黑树的性质。

例图:

在这里插入图片描述

红黑树的实现

红黑树的框架

红黑树的框架主要包括颜色, 节点,和树的封装三个部分,分开写方便调整
//颜色
enum Color
{
	Black,
	Red
};

template<class T>
struct RBTreeNode
{
	RBTreeNode(const T& data = T(), Color col = Red)
		: _pLeft(nullptr)
		, _pRight(nullptr)
		, _pParent(nullptr)
		, _data(data)
		, _col(col)
	{}

	RBTreeNode<T>* _pLeft;
	RBTreeNode<T>* _pRight;
	RBTreeNode<T>* _pParent;
	T _data;
	Color _col;   // 节点的颜色
};


template<class T>
class RBTree
{
	typedef RBTreeNode<T> Node;
public:
	RBTree()
	{
		_pHead = new Node;
		_pHead->_pLeft = _pHead;
		_pHead->_pRight = _pHead;
	}
	 // 在红黑树中插入值为data的节点,插入成功返回true,否则返回false
    // 本次实现红黑树不存储重复性元素
	bool Insert(const T& data);
    
    // 检测红黑树中是否存在值为data的节点,存在返回该节点的地址,否则返回nullptr
    Node* Find(const T& data);
    
    // 获取红黑树最左侧节点
	Node* LeftMost();
    
    // 获取红黑树最右侧节点
	Node* RightMost();
    
    // 检测红黑树是否为有效的红黑树,注意:其内部主要依靠_IsValidRBTRee函数检测
	bool IsValidRBTRee();
private:
	bool _IsValidRBTRee(Node* pRoot, size_t blackCount, size_t pathBlack);
    // 左单旋
	void RotateL(Node* par);
    // 右单旋
	void RotateR(Node* par);
    // 为了操作树简单起见:获取根节点
	Node*& GetRoot();
private:
	Node* _pHead;
}

红黑树的插入实现

头结点的作用

头结点并不是真正的根节点,而是作为一个边界条件,简化了边界检查的情况,可以使所有叶子节点指向它,用它的父节点指向真正的根节点

  1. 哨兵节点的功能
  • 简化边界条件处理:使用头节点作为哨兵节点,使得所有叶子节点都指向它。这意味着在树的遍历和操作中,不需要单独处理空指针或叶子节点的情况,从而简化了许多逻辑。
  1. 统一树结构
  • 保持平衡性:头节点作为树的根节点的一部分,可以确保树始终保持一个一致的结构,特别是在插入和删除操作时。每个节点都会有两个子节点(左和右),即使是最底层的节点也会指向头节点。
  1. 提升性能
  • 减少比较和判断:在查找、插入等操作中,直接返回头节点可以避免多次空指针检查,提高了操作效率。例如,在查找最小或最大节点时,可以直接从根节点开始,直到遇到头节点为止。
  1. 辅助树的验证
  • 有效性检查:在进行红黑树性质验证时,头节点可以作为一个起始点,帮助检查整个树是否满足红黑树的性质,如路径上的黑色节点数量相同等。
  1. 支持其他操作
  • 提供根节点访问:通过 _pHead,可以方便地访问树的根节点。这对于旋转操作、插入、删除等都是必需的。

红黑树的插入步骤(简易理解版带图)

关于二叉树的旋转详情请看下面这篇文章,此文中不会详细讲解旋转
AVL树

红黑树的插入过程涉及多个步骤,以确保树的性质得到维护。下面是详细的插入步骤:

  1. 插入新节点
  • 将待插入的节点作为红色节点插入到树中(遵循普通二叉搜索树的插入规则)。
    新节点的初始颜色设置为红色。
  1. 调整树的结构
  • 在插入完成后,可能会违反红黑树的性质,因此需要进行调整。主要需要检查以下情况,并进行修正:

2.1 父节点是黑色

  • 如果新节点的父节点是黑色,则树的性质没有被破坏,无需进一步调整。

在这里插入图片描述

2.2 父节点是红色

  • 如果新节点的父节点是红色,那么就会出现两个连续的红色节点(违反了红黑树的性质),此时需要进行调整。

在这里插入图片描述

  1. 根据叔叔节点的颜色进行调整
  • 找到新节点的祖父节点。

根据叔叔节点(父节点的兄弟)的颜色,分为以下几种情况:

3.1 叔叔节点是红色

在这里插入图片描述

  • 将父节点和叔叔节点都变为黑色。
  • 将祖父节点变为红色。
  • 将新节点指向祖父节点,重复检查祖父节点。(向上循环检查)
    在这里插入图片描述

3.2 叔叔节点是黑色或不存在

在这里插入图片描述

  • 根据新节点的位置(左子树或右子树),进行相应的旋转操作。
    3.2.1 左-左情况(LL)
    在这里插入图片描述

  • 如果新节点是父节点的左子节点,执行右旋转(将祖父节点旋转到右边)。
    在这里插入图片描述

3.2.2 右-右情况(RR)
在这里插入图片描述

  • 如果新节点是父节点的右子节点,执行左旋转(将祖父节点旋转到左边)。
    在这里插入图片描述

3.2.3 左右情况(LR)
在这里插入图片描述

  • 如果新节点是父节点的右子节点且父节点是祖父节点的左子节点,先对父节点执行左旋转,然后再对祖父节点执行右旋转。

在这里插入图片描述

3.2.4 右左情况(RL)

在这里插入图片描述

  • 如果新节点是父节点的左子节点且父节点是祖父节点的右子节点,先对父节点执行右旋转,然后再对祖父节点执行左旋转。
    在这里插入图片描述
  1. 将根节点设为黑色
  • 在进行完所有调整后,确保根节点的颜色为黑色,以保持红黑树的性质。

以上图都是简单版本,方便理解,加上子节点等也是差不多的,除此之外还需要考虑根节点改变的情况

红黑树的插入具体代码详解

1, 首先处理根节点插入的情况, GetRoot()函数获取真正的根节点指针的引用。

bool Insert(const T& data)
{
	Node*& proot = GetRoot();
	Node* newnode = new Node(data);
	if (proot == nullptr)
	{
		proot = newnode;
		proot->_col = Black;
		proot ->_pParent = _pHead;
		return true;
	}
	
}

2,处理已经有根节点的情况,找寻位置,并插入节点,注意链接父节点

bool Insert(const T& data)
{
	Node*& proot = GetRoot();
	Node* newnode = new Node(data);
	if (proot == nullptr)
	{
		proot = newnode;
		proot->_col = Black;
		proot ->_pParent = _pHead;
		return true;
	}

	Node* cur = proot;
	Node* par = nullptr;

	while (cur)
	{
		par = cur;
		if (cur->_data > data)
		{
			cur = cur->_pLeft;
		}
		else if (cur->_data < data)
		{
			cur = cur->_pRight;
		}
		else
		{
			delete newnode;
			return false;
		}
	}

	if (par->_data > data)
	{
		par->_pLeft = newnode;
	}
	else
	{
		par->_pRight = newnode;
	}

	cur = newnode;
	cur->_pParent = par;
}
	

3, 判断是否需要变色或者旋转,这时候需要祖父节点和叔叔节点,根据以上讲解的情况分情况处理。

此时要注意边界检查和死循环, 旋转之后要结束循环,还要记得变色。一下代码主要分为有叔叔节点且为红色和另一种情况处理,在此之下在细分。
bool Insert(const T& data)
{
	Node*& proot = GetRoot();
	Node* newnode = new Node(data);
	if (proot == nullptr)
	{
		proot = newnode;
		proot->_col = Black;
		proot ->_pParent = _pHead;
		return true;
	}

	Node* cur = proot;
	Node* par = nullptr;

	while (cur)
	{
		par = cur;
		if (cur->_data > data)
		{
			cur = cur->_pLeft;
		}
		else if (cur->_data < data)
		{
			cur = cur->_pRight;
		}
		else
		{
			delete newnode;
			return false;
		}
	}

	if (par->_data > data)
	{
		par->_pLeft = newnode;
	}
	else
	{
		par->_pRight = newnode;
	}

	cur = newnode;
	cur->_pParent = par;
	
	Node* ppar = par->_pParent;
	Node* uncle = (ppar->_pLeft == par) ? ppar->_pRight : ppar->_pLeft;
	while (par != _pHead && par->_col == Red)
	{
		if (uncle && uncle->_col == Red)
		{
			//    pp
			//  p    u
			//   c
			//叔叔节点存在并且为红色,改变颜色,不需要旋转
			par->_col = Black;
			uncle->_col = Black;
			ppar->_col = Red;

			//改变现在节点
			cur = ppar;
			par = cur->_pParent;
			ppar = par->_pParent;
			uncle = (ppar->_pLeft == par) ? ppar->_pRight : ppar->_pLeft;
		}
		else
		{
			//叔叔节点不存在或者为黑色,需要旋转
			// ll型
			//     pp
			//   p
			//  c
			if (par->_pLeft == cur && ppar->_pLeft == par)
			{
				RotateR(ppar);
				par->_col = Black;
				ppar->_col = Red;
				break;
			}
			// lr型
			//     pp
			//   p
			//     c
			else if (ppar->_pLeft == par && par->_pRight == cur)
			{
				RotateL(par);
				RotateR(ppar);

				cur->_col = Black;
				ppar->_col = Red;
				break;
			}
			// rr型
			//     pp
			//		    p
			//			    c
			else if (ppar->_pRight == par && par->_pRight == cur)
			{
				RotateL(ppar);

				par->_col = Black;
				ppar->_col = Red;
				break;
			}
			// rl型
			//     pp
			//		    p
			//		  c
			else if (ppar->_pRight == par && par->_pLeft == cur)
			{
				RotateR(par);
				RotateL(ppar);

				cur->_col = Black;
				ppar->_col = Red;
				break;
			}
		}
	}

	GetRoot()->_col = Black;
	return true;
}

红黑树的旋转代码

详细讲解请看上一篇AVL树的文章

// 左单旋
void RotateL(Node* par)
{
	assert(par);
	Node*& proot = GetRoot();
	Node* ppar = par->_pParent;
	Node* rchild = par->_pRight;
	Node* rlchild = rchild->_pLeft;

	rchild->_pLeft = par;
	par->_pRight = rlchild;

	rchild->_pParent = ppar;
	par->_pParent = rchild;
	if (rlchild)
		rlchild->_pParent = par;

	if (par == proot)
	{
		proot = rchild;
		rchild->_pParent = _pHead;
	}
	else
	{
		if (ppar->_pLeft == par)
		{
			ppar->_pLeft = rchild;
		}
		else
		{
			ppar->_pRight = rchild;
		}
	}
}
// 右单旋
void RotateR(Node* par)
{
	assert(par);
	//    p
	//  l
	//n	   lr
	Node*& proot = GetRoot();
	Node* ppar = par->_pParent;
	Node* lchild = par->_pLeft;
	Node* lrchild = lchild->_pRight;

	lchild->_pRight = par;
	par->_pLeft = lrchild;
	//    l
	// l     p
	//    lr

	lchild->_pParent = ppar;
	par->_pParent = lchild;
	if (lrchild)
		lrchild->_pParent = par;

	if (par == proot)
	{
		proot = lchild;
		lchild->_pParent = _pHead;
	}
	else
	{
		if (ppar->_pLeft == par)
		{
			ppar->_pLeft = lchild;
		}
		else
		{
			ppar->_pRight = lchild;
		}
	}
}

红黑树的查验

红黑树的检查通过先取一路记录黑色节点数量,然后采用递归处理每条路线检查是否黑色节点数量一致,并且为了方便,稍微封装了一下,外界调不用参数

	bool IsValidRBTRee()
	{
		size_t pathblack = 0;

		Node* root = GetRoot();
		Node* par = root;
		while (par)
		{
			if (par->_col == Black)
				pathblack++;
			par = par->_pLeft;
		}

		return _IsValidRBTRee(root, 0, pathblack);
	}
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 == Black)
		{
			blackCount++;
		}

		return _IsValidRBTRee(pRoot->_pLeft, blackCount, pathBlack)
			&& _IsValidRBTRee(pRoot->_pRight, blackCount, pathBlack);
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值