【c++】红黑树的部分实现

hello~ 很高兴见到大家! 这次带来的是C++中关于红黑树这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页 : 默|笙


文章目录

  • 一、红黑树介绍
    • [1. 红黑树的概念](#1. 红黑树的概念)
    • [2. 红黑树的规则](#2. 红黑树的规则)
  • [二. 红黑树的结构](#二. 红黑树的结构)
  • [三. 插入](#三. 插入)
    • [1. 非空树插入会碰到的三种情况](#1. 非空树插入会碰到的三种情况)
      • [1. 1 仅变色(针对uncle为红色)](#1. 1 仅变色(针对uncle为红色))
      • [1. 2 变色 + 旋转(针对uncle为空或黑色节点)](#1. 2 变色 + 旋转(针对uncle为空或黑色节点))
        • [变色 + 右单旋](#变色 + 右单旋)
      • [变色 + 左单旋](#变色 + 左单旋)
      • [变色 + 左右双旋](#变色 + 左右双旋)
      • [变色 + 右左双旋](#变色 + 右左双旋)
  • 三、红黑树的检测
  • 四、源代码

一、红黑树介绍

1. 红黑树的概念

  1. 红黑树是一棵二叉搜索树,它的每一个节点相对于普通的二叉搜索树都增加了一个成员变量来存储节点的颜色,有红色和黑色两种;然后跟AVL树一样,拥有除_left和_right之外的第三个指向该节点父节点的_parent指针。它通过控制每一条从叶子到跟节点的颜色,来确保没有一条路径会比最短路径长出2倍,它是接近平衡的
  2. 相对于AVL树,它对于平衡度的要求会稍低一些,也就是它旋转的次数会少一些,效率也会高一些,倘若红黑树的节点个数为N,最短路径为h,那么有2^h - 1 < N < 2^2h - 1。意味着红黑树即便是走最坏情况增删查改时间复杂度是2*logN,即O(logN)。

2. 红黑树的规则

  1. 每个节点不是黑色就是红色。
  2. 根节点必须是黑色。
  3. 如果一个节点的颜色是红色,那么它的两个孩子必须得是黑色的。也就是说一棵红黑树里面每条路径上不会出现连续2个节点都是红色的情况
  4. 对于任意节点,从它到其所有的NULL节点的简单路径上,每一条路上的黑色节点个数都必须是相同的
  1. 通过对规则的分析,不难发现红黑树是如何去控制节点颜色来达到控制每一条路径的长度的目的:在极端条件下,最短路径就是节点全为黑色的路径,最长路径是红色节点和黑色节点交错出现(一黑一红间隔)的路径。若最短路径为bh(black height),最长路径就为2*bh。
  2. 虽然理论上红黑树可以达到的最短路径是bh和最长路径2bh,但在实际应用中它们并不是存在于每一棵红黑树里的,假设红黑树的某条路径长为x,那么有bh <= x <= 2bh。
  3. 根节点必须为黑色和规则3保证了红色节点绝对不会比黑色节点多。
  • 有的书里面如《算法导论》里补充了一条每个叶子节点(NIL)都是黑色的规则。这里的叶子节点非我们所熟知的那个叶子节点,而是空节点,有些书籍上面也把NIL叫做外部节点。NIL是为了方便准确的标识出所有路径。

二. 红黑树的结构

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

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
	{ };
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col = RED;
};

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

private:
	Node* _root = nullptr;
};
  1. 跟AVL树的节点结构相比,它没有平衡因子变量,而是有用来存储节点颜色的变量。<AVL博客>
  2. 我们使用枚举类型来存储节点颜色,这样既增加代码可读性,也能避免无效值。
  3. 每一个新插入的节点默认都是红色节点,因为不能破坏规则4相对于破坏规则3之后需要做出的处理太多,太复杂。

三. 插入

  1. 它的插入规则跟二叉搜索树一样,只是多了一个_parent指针需要处理,这里就不再做讲解。<二叉搜索树博客>
cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
	//处理空树的情况
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
	}
	//找到要插入的位置
	else
	{
		Node* cur = _root;
		Node* parent = nullptr;

		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);
		if (parent->_kv.first > kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;
		
		return true;
  1. 根节点的颜色做一下特殊处理,因为根节点一定得是黑色,节点初始化默认为红色
  2. 接下来对上面的插入代码的基础之上做一些改变。

1. 非空树插入会碰到的三种情况

  1. 非空树插入一个红色节点,它的父节点可能是红色,也有可能是黑色,如果是黑色节点,我们直接插入就行,插入结束。

  2. 但如果是红色节点,为了不违反红黑树的第三个规则,我们必须得做一些变换。 分析可以得知,该节点是红色节点,记为cur,cur的父节点也是红色节点,将其父节点记为parent,parent的父节点一定是黑色节点,将其父节点记为grandfather。这三个颜色是固定的,但是parent的兄弟节点的颜色是不定的,将其兄弟节点记为uncle。接下来我们会先根据兄弟节点的情况来做出不同的措施来符合规则三。就比如下图:

  3. 其兄弟节点有三种情况:红色节点,黑色节点,以及空节点。如果是红色节点,我们只需要进行变色处理;但如果是黑色节点或者是空节点,我们必须进行变色+旋转处理。

接下来cur简记为c,parent简记为p,grandfather简记为g,uncle简记为u。

1. 1 仅变色(针对uncle为红色)

  1. 现在的cur不一定是新插入的节点,它有可能原来是黑色的节点,不过因为下面方框里面节点的更新变成了红色。
  2. 从插入的节点到根节点这一路上,可能会有反反复复的更新,直到不再有连续的红色节点。就比如说变色这个过程,我们可以看到g节点变成了红色,如果它不是根节点,那么它的父节点一定是红色,那么它会是新的cur节点,并根据新的uncle的情况向上更新。
  3. 我们可以看到,我们将节点8和节点15的颜色从红色变成了黑色,将节点10的颜色从黑色变成了红色。能不能只把节点8和节点15的颜色变成黑色呢?这样似乎也能完美符合规则。
  4. 从节点10开始到下面NULL节点的每一条路径上,黑色节点的个数都是相同的。我们变色一定是不能够改变每一条路径上面黑色节点的数量的。只把节点8和节点15变成黑色肯定不行,这样从节点10到下面NULL节点的每一条路上就凭空多出了一个黑色节点。如果节点10是根节点那肯定没问题,但是,万一这只是某棵树的子树呢?
  5. 变色的过程可以总结为:将p和u节点变成黑色,将g节点变成红色。然后g节点变成新的cur节点,接着更新其他节点。然后将根节点变成黑色,毕竟根节点有可能在更新的过程中变成红色 ,这个可以放到所有更新的最后,不用每次更新完都去管一下_root的颜色。

1. 2 变色 + 旋转(针对uncle为空或黑色节点)

旋转具体实现过程看<AVL树博客>

变色 + 右单旋

当p为g的左孩子且cur为p的左孩子时,右单旋。

  1. 若方框里的黑色节点数量都为0,那么节点4是新插入的节点,u为空,不会存在更新过程。若不为0,那么一定存在从下往上的更新与变色过程。之后是统一的左单旋,然后将节点8的颜色变为黑色,节点10的颜色变为红色。
  2. 针对uncle为空或为黑色节点的情况,仅变色一定是行不通的,因为我们必须得遵循规则4。所以必须进行旋转操作。
  3. 根节点由节点10变成了节点8,由于节点10为黑色,节点8为红色,我们若将节点8的颜色改为红色,那么就能停止更新的过程,不再继续向上更新。这就是为什么不通过只改变节点4的颜色来达成目的的原因。
  4. 所以变色+右单旋的过程就是:右单旋之后将p改为黑色,g改为红色。
  5. 右单旋直接用AVL里删去平衡因子改变之后的代码就行。

变色 + 左单旋

当p为g的右孩子且cur为p的右孩子时,左单旋。

  1. 这里省略插入节点之后变色的步骤。左单旋以及下面的旋转不再详细讲解。
  2. 和右单旋一样,旋转之后将p变成黑色,将g变成红色

变色 + 左右双旋

p是g的左孩子且cur是p的右孩子时,进行左右双旋。

  1. 就是先进行一次左旋转,再进行一次右旋转。节点颜色变化跟单旋不一样,是cur变成黑色,g变成红色。

变色 + 右左双旋

当p为g的右孩子且cur为p的左孩子时,右左双旋。

  1. 就是先进行一次右旋转,再进行一次左旋转。节点颜色变化跟单旋不一样,是cur变成黑色,g变成红色。

插入完整代码:

cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
	//处理空树的情况
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
		return true;
	}
	//找到要插入的位置
	else
	{
		Node* cur = _root;
		Node* parent = nullptr;

		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);
		if (parent->_kv.first > kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;
		//父节点为红色的情况下需要进行处理
		if (parent->_col == RED)
		{
			while (parent && parent->_col == RED)
			{
				//记录节点
				Node* grandfather = parent->_parent;
				Node* uncle = nullptr;
				if (grandfather->_left == parent)
				{
					uncle = grandfather->_right;
				}
				else
				{
					uncle = grandfather->_left;
				}
				//uncle为红色的情况
				//仅变色
				if (uncle && uncle->_col == RED)
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;
					//更新
					cur = grandfather;
					parent = cur->_parent;
				}
				//uncle为黑色或为空的情况
				else
				{
					//右旋转
					if (grandfather->_left == parent && parent->_left == cur)
					{
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;

						break;
					}
					//左旋转
					else if (grandfather->_right == parent && parent->_right == cur)
					{
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;

						break;
					}//左右双旋
					else if (grandfather->_left == parent && parent->_right == cur)
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;

						break;
					}
					//右左双旋
					else
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;

						break;
					}
				}
			}
		}
		
	}
	//处理根节点颜色

	_root->_col = BLACK;

	return true;
}

三、红黑树的检测

  1. 检查一棵树是否为红黑树主要是检查一棵树有没有满足红黑树的那四个规则:
  1. 颜色不是黑就是红
  2. 根节点是黑
  3. 从根节点到NULL的路径上没有相邻的红色节点
  4. 从任意节点到其他NULL的路径上黑色节点数量都是相同的。
  1. 第一个规则不需要验证,颜色只可能是红色或黑色;第二个规则很好验证,看_root是否是黑色就行。我们来看第三个规则,要验证有没有相邻的红色节点,就需要遍历这整棵树,又因为一个节点能有两个孩子,验证难度比较大,而一个节点只能有一个父节点,我们通过验证其父节点和它颜色是否相同来验证规则3。
  2. 最难验证的就是规则4,我们需要计算黑色节点数量,增加一个传递黑色节点数量的参数BlackNum传递黑色节点数量,遇到黑色节点BlackNum++。以及需要一个用来比较的黑色节点个数的参考值leftMost(最左边那一条路径上的黑色节点数量)
  3. 节点为空的时候,就把BlackNum的值拿来跟参考值进行比较就行。

红黑树检测代码:

cpp 复制代码
bool IsBalance()
{
	if (_root->_col == RED)
	{
		return false;
	}
	int leftMost = 0;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_col == BLACK)
		{
			leftMost++;
		}
		cur = cur->_left;
	}
	return _check(_root, 0, leftMost);
}
bool _check(Node* cur, int BlackNum, const int leftMost)
{
	if (cur == nullptr)
	{
		if (BlackNum != leftMost)
		{
			return false;
			cout << "黑色节点的数量不相等" << endl;
		}
		else
			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, leftMost) && _check(cur->_right, BlackNum, leftMost);
}

四、源代码

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

enum Colour
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
	{ };
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col = RED;
};

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
		//处理空树的情况
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}
		//找到要插入的位置
		else
		{
			Node* cur = _root;
			Node* parent = nullptr;

			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);
			if (parent->_kv.first > kv.first)
			{
				parent->_left = cur;
			}
			else
			{
				parent->_right = cur;
			}
			cur->_parent = parent;
			//父节点为红色的情况下需要进行处理
			if (parent->_col == RED)
			{
				while (parent && parent->_col == RED)
				{
					//记录节点
					Node* grandfather = parent->_parent;
					Node* uncle = nullptr;
					if (grandfather->_left == parent)
					{
						uncle = grandfather->_right;
					}
					else
					{
						uncle = grandfather->_left;
					}
					//uncle为红色的情况
					//仅变色
					if (uncle && uncle->_col == RED)
					{
						uncle->_col = parent->_col = BLACK;
						grandfather->_col = RED;
						//更新
						cur = grandfather;
						parent = cur->_parent;
					}
					//uncle为黑色或为空的情况
					else
					{
						//右旋转
						if (grandfather->_left == parent && parent->_left == cur)
						{
							RotateR(grandfather);
							parent->_col = BLACK;
							grandfather->_col = RED;

							break;
						}
						//左旋转
						else if (grandfather->_right == parent && parent->_right == cur)
						{
							RotateL(grandfather);
							parent->_col = BLACK;
							grandfather->_col = RED;

							break;
						}//左右双旋
						else if (grandfather->_left == parent && parent->_right == cur)
						{
							RotateL(parent);
							RotateR(grandfather);
							cur->_col = BLACK;
							grandfather->_col = RED;

							break;
						}
						//右左双旋
						else
						{
							RotateR(parent);
							RotateL(grandfather);
							cur->_col = BLACK;
							grandfather->_col = RED;

							break;
						}
					}
				}
			}
			
		}
		//处理根节点颜色

		_root->_col = BLACK;

		return true;
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	bool IsBalance()
	{
		if (_root->_col == RED)
		{
			return false;
		}
		int leftMost = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				leftMost++;
			}
			cur = cur->_left;
		}
		return _check(_root, 0, leftMost);
	}
	int Height()
	{
		return _Height(_root);
	}

	int Size()
	{
		return _Size(_root);
	}

	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;
	}
private:
	int _Size(Node* root)
	{
		return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
	}

	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

	bool _check(Node* cur, int BlackNum, const int leftMost)
	{
		if (cur == nullptr)
		{
			if (BlackNum != leftMost)
				return false;
			else
				return true;
		}
		if (cur->_col == RED && cur->_parent && cur->_parent->_col == RED)
		{
			return false;
		}
		if (cur->_col == BLACK)
		{
			BlackNum++;
		}
		return _check(cur->_left, BlackNum, leftMost) && _check(cur->_right, BlackNum, leftMost);
	}
	void _InOrder(const Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}
	void RotateR(Node* parent)
	{
		//记录节点
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* parentParent = parent->_parent;

		//改变指针
		parent->_left = subLR;
		subL->_right = parent;
		if (parentParent == nullptr)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (parentParent->_left == parent)
			{
				parentParent->_left = subL;
			}
			else
			{
				parentParent->_right = subL;
			}
			subL->_parent = parentParent;
		}
		//避免subLR = nullptr出现空指针解引用的情况
		if (subLR)
		{
			subLR->_parent = parent;
		}
		parent->_parent = subL;
	}
	void RotateL(Node* parent)
	{
		//记录节点
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* parentParent = parent->_parent;

		//改变指针指向
		subR->_left = parent;
		parent->_right = subRL;
		if (parentParent == nullptr)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (parentParent->_left == parent)
			{
				parentParent->_left = subR;
			}
			else
			{
				parentParent->_right = subR;
			}
			subR->_parent = parentParent;
		}

		if (subRL)
		{
			subRL->_parent = parent;
		}
		parent->_parent = subR;
	}
	Node* _root = nullptr;
};

今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!

相关推荐
hsjkdhs2 小时前
C++之拷贝构造(浅拷贝与深拷贝)、this指针、内联函数
c++
轩情吖2 小时前
Qt常用控件之QSpinBox
开发语言·c++·qt·控件·桌面级开发·qspinbox·微调框
掘根2 小时前
【Qt】输入类控件2——SpinBox,DateEdit,TimeEdit,Dial,Slider
开发语言·qt
wshzrf2 小时前
【Java系列课程·Java学前须知】第3课 JDK,JVM,JRE的区别和优缺
java·开发语言·jvm
铅笔侠_小龙虾2 小时前
JVM 深入研究 -- 详解class 文件
java·开发语言·jvm
Humbunklung2 小时前
C# 使用应用RSA和ECC进行数字签名和签名验证
开发语言·c#·rsa·ecc
Larry_Yanan3 小时前
QML学习笔记(十四)QML的自定义模块
开发语言·笔记·qt·学习·ui
练习时长一年3 小时前
ApplicationContext接口实现(二)
java·开发语言
灵性花火3 小时前
针对多工程情况下,Qwidget的ui文件的Stylesheet找不到图片的问题
开发语言·qt