从C++开始的编程生活(22)——红黑树

前言

本系列文章承接C++基础的学习,需要**++有C语言的基础++** 才能学会哦~

第22篇主要讲的是有关于C++的**++红黑树++** 和**++unordered的区别++** 。
C++才起步,都很简单!!

红黑树概念

是一棵二叉搜索树,每一个结点增加一个存储位表示结点颜色(红或黑)。通过颜色进行约束,使其没有一条路径比其他路径长2倍,从而接近平衡。

红黑树规则

①结点只有黑色或者红色

根结点是黑色的

红色结点的两个孩子必须是黑色(NULL也算是黑色的),也就是说任意一条路径不会有连续的红色结点。(约束红色结点的孩子)

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

(简单路径:从根走到空的、没有回退的路径)。

通过4条规则,实现 "最长路径 ≤ 2 * 最短路径"。

极端场景下:

假设每条路径有h个黑结点,

最长路径 = 2*h个结点,一黑一红排列,

最短路径 = h个结点,全黑。

即 "最长路径 ≤ 2 * 最短路径"

上图均为红黑树。

红黑树的效率

红黑树的增删查改效率是2logN ,比AVL树的logN稍高,但是时间复杂度都为O(logN)。但是红黑树通过颜色规则近似控制平衡,由于平衡控制的条件没有AVL树苛刻,所以旋转的次数会少很多

总的来看,红黑树的效率还是比AVL树稍高。

红黑树的模拟实现

插入

1.按照搜索树的规则插入。

2.空树插入,新结点必须为黑色;非空树插入,新结点必须为红色。

插入新结点后:

若父亲为黑色,未违反规则,插入结束;

若父亲为红色,违反规则③,此时要进行分析,并变色。

情况Ⅰ、Ⅱ起始图

如图:

插入了新结点4违反了规则③,其父亲结点6为红色,则祖父结点10必定为黑色(规则③),叔叔结点15黑、红、空皆可。

情况Ⅰ(变色)

若叔叔为红,让父亲和叔叔变黑,为了保持规则④,再让祖父变红。

再把祖父变为cur,继续按照过程Ⅰ进行更新,直到parent为黑色,就停止更新。

若祖父结点就是根结点,要把变红的祖父结点再变回黑色(如下图情况)。

情况Ⅱ(单旋+变色)

①叔叔为空

则cur必定为新增结点而不是更新上来的结点。

左边高,则右旋。

,右旋转后父亲和祖父变色

②叔叔为黑

则说明cur原本为黑,在cur的子树下插入了新结点后,由情况Ⅰ更新上来之后为红色的结点。

hb为子树中黑色节点的个数。

cur插入新结点后

cur的左右子树变为hb个黑色结点

左边黑色结点多,进行右单旋

大致结构如图,则右单旋

大致结构如图,则左单旋(略)

再变色

此时红黑树已平衡

情况Ⅲ(双旋+变色)

cur为红,parent为红,uncle为黑或不存在。

①叔叔为空

则cur一定为新增结点。

如图,有一个折点,就需要旋转两次,即双旋。

类似的左右结构,则 p 左旋,再 g 右旋

类似的右左结构,则 p 右旋,再 g 左旋

parent左旋,再祖父右旋

再进行变色,cur变黑,grandfather变红

此时红黑树已平衡

②叔叔为黑

与情况Ⅱ同理,此时说明cur是由情况Ⅰ更新上来的,而不是新增结点。

在原树下添加子树的示意即可,操作逻辑与①叔叔为空相同

查找

同搜索树的搜索方法。

代码示例:

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

enum Colour
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

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

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;
		}

		Node* parent = nullptr;
		Node* cur = nullptr;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(kv);
		cur->_col = RED;
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		cur->_parent = parent;
		//叔叔为红,父亲和叔叔变黑
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			if (grandfather->_left == parent)
			{
				Node* uncle = grandfather->_right;
				//叔叔存在且为红
				if (uncle && uncle->_col == RED)
				{
					//变色
					parent->_col == uncle->_col == BLACK;
					grandfather->_col = RED;

					//继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				//叔叔不存在或者叔叔存在且为黑
				else
				{
					if (cur == parent->_left)//单旋+变色
					{
						//父亲变黑,祖父变红
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else//双旋+变色
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandparent->_col = RED;
					}

					break;
				}
			}
			else//上述镜像操作
			{
				Node* uncle = grandfather->_left;
				//叔叔存在且为红
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->col = RED;
					//继续向上更新
					cur = grandfather;
					parent = cur->_parent;
				}
				//叔叔不存在,或叔叔为黑
				else
				{
					if (cur == parent->_parent)
					{
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather - _col = RED;
					}
					else
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break;
				}
			}
		}

		_root->_col == BLACK;
		return true;
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	//根据各路径的黑色结点数量来检验
	bool check(Node* root, int blackNum, const int refNum)
	{
		//前序遍历

		if (root == nullptr)//递归遍历走到了一条路径的进程
		{
			if (refNum != blackNum)
			{
				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)
		{
			blackNum++;
		}

		//左右子树都要检查
		return check(root->_left, blackNum, refNum)
			&& check(root->_right, blackNum, refNum);
	}

	bool IsBalance()
	{
		if (_root == nullptr)
		{
			return true;
		}
		if (_root->_col == RED)
		{
			return false;
		}

		//获取参考值
		int refNum = 0;
		Node* cur = _root;
		//只挑选一条路进行遍历即可
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				++refNum;
			}
			cur = cur->_left;
		}

		return check(_root, 0, refNum);
	}

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

	//使用搜索树的Find即可
	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;
	}
protected:
	//结点个数
	int _Size(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
		
		//左树结点+右树结点+自己本身
		return _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);

		//哪边高返回哪边,+1是加的根结点
		return leftHeight > rightHeight ? left + 1 : rightHeight + 1;
	}

	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		//先连接parent与LR结点
		parent->_left = subLR;
		if (subLR)//不为空时才调整其父指针
		{
			subLR->_parent = parent;
		}

		//记录祖父结点
		Node* ppNode = parent->_parent;

		//L结点变为根结点,根结点变为L的右结点
		subL->_right = parent;
		parent->_parent = subL;
		//旋转完成

		//处理祖父结点
		if (parent == _root)//若父结点为根,说明祖父结点为空
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else//祖父结点存在,连接祖父结点和父结点
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left == subL;
			}
			else
			{
				ppNode->_right = subL;
			}
			subL->_parent = ppNode;
		}
	}

	void RotateR(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_left = subRL;
		if (subRL)
		{
			subRL->_parent = parent;
		}

		Node* ppNode = parent->_parent;
		subR->_left = parent;
		parent->_parent = subR;

		if (ppNode == nullptr)
		{
			_root = sub;
			subR->_parent = nullptr;
		}
		else
		{
			if (parent == ppNode->_left)
			{
				ppNode->_left = subR;
			}
			else
			{
				
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}
	}

	//中序遍历
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_InOrder(root->_left);
		cout << root->_kv.first << ":" << _root->_kv.second << endl;
		_InOrder(root->_right);
	}
private:
	Node* _root = nullptr;
};

补充

unordered_set

cpp 复制代码
template<   class Key,
            class Hash = hash<Key>,
            class Pred = equal_to<Key>,
            class ALLoc = allocator<key>
        >class unordered_set;            

a.默认要求key支持转换为整型。不支持 or 有特殊需求得自行实现。

b.默认要求key支持相等的比较。不支持 or 有特殊需求得自行实现。

c.底层存储数据的内存由空间配置器申请。有特殊需求可以自行实现内存池。

d.底层由哈希桶实现,增删查平均效率为O( 1 ),迭代器遍历无序。

unordered_set与set的差异

①对Key要求不一样。set要求Key支持小于比较,unordered_set要求Key支持转成整型且支持等于比较(原因在后续文章会说)。

②迭代器有差异。set是双向迭代器,unordered_set是单向迭代器;set底层是红黑树,迭代器遍历有序且去重,unordered_set底层是哈希表,迭代器遍历无序且去重。

③性能有差异。大多数情况下,unordered_set的效率更高。红黑树增删查改的效率为O(logN),哈希表增删查改的效率为O(1)。

unordered_map

与unordered的特点类似。

与map的差异也和unordered_set与set的差异类似。

不过多赘述。

❤~~本文完结!!感谢观看!!接下来更精彩!!欢迎来我博客做客~~❤

相关推荐
Trouvaille ~2 小时前
【项目篇】从零手写高并发服务器(六):EventLoop事件循环——Reactor的心脏
linux·运维·服务器·c++·高并发·epoll·reactor模式
学嵌入式的小杨同学2 小时前
STM32 进阶封神之路(十八):RTC 实战全攻略 —— 时间设置 + 秒中断 + 串口更新 + 闹钟功能(库函数 + 代码落地)
c++·stm32·单片机·嵌入式硬件·mcu·架构·硬件架构
学嵌入式的小杨同学2 小时前
STM32 进阶封神之路(十七):RTC 实时时钟深度解析 —— 从时钟源到寄存器配置(底层原理 + 面试重点)
c++·stm32·单片机·嵌入式硬件·mcu·硬件架构·pcb
.select.2 小时前
STL下常见容器底层数据结构
开发语言·c++
于先生吖2 小时前
基于 Java 开发短剧系统:完整架构与核心功能实现
java·开发语言·架构
老鱼说AI2 小时前
CUDA架构与高性能程序设计:多维网格与数据
c++·人工智能·深度学习·神经网络·机器学习·语言模型·cuda
badhope2 小时前
GitHub超有用项目推荐:skill仓库--用技能树打造AI超频引擎
java·开发语言·前端·人工智能·python·重构·github
海边的梦2 小时前
救命!此电脑网络位置异常?AD域排错3步封神,DNS/NetLogon/GPO根因一键定位
服务器·开发语言·php
时寒的笔记2 小时前
js逆向入门03_会展中心案例&shuwei观察&ji思录
开发语言·前端·javascript