[数据结构]红黑树的原理及其实现

news2024/9/24 3:21:30

文章目录

  • 红黑树的特性
  • 红黑树的时间复杂度
    • 推导:
    • 结论
    • 红黑树与AVL树比较
  • 红黑树的插入
    • 红黑树的节点定义
    • 调整策略
      • 思考情况2:
      • 思考情况3:
  • 代码实现
    • myBTRee.h
    • test.cpp

红黑树的特性

红黑树最常用的平衡二叉搜索树。跟AVL树不同的是,红黑树是依靠节点的颜色来维护平衡的。虽然任意节点不具备严格平衡,但是数据的查找、插入、删除等操作效率依旧出色。下面是红黑树的一些特性:

  1. 每个节点的颜色要么是红色要么是黑色
  2. 根节点的颜色是黑色
  3. 如果一个节点的颜色是红色的,那么它的两个孩子节点的颜色一定是黑色的
  4. 任意节点到其所能到达的叶子节点之间的黑色节点的数量相同
  5. 叶节点是黑色的空节点

根据以上红黑树的特性,我们可以总结出以下结论:

结论:红黑树中最长路径的节点数量不超过最短路径节点数量的2倍
证明:假设每条路径黑色节点的数量为n(假设不包括空叶子节点),则红色节点的数量最多是n。任意路径节点数量最少为n(只有黑节点),最多为2*n

这样一来,任意一条路径的长度之差都保证在了一个有限的范围内,这也是红黑树具有平衡性的原因。

红黑树的时间复杂度

由于红黑树底层还是一颗二叉搜索树,根据二叉搜索树的特性:查找的效率取决于树的高度

推导:

现假设红黑树的某一路径黑色节点数量为bh。由于任意路径黑色节点数目相同,我们可以把路径上所有的红色节点删除,将删除节点的父节点和其子节点相连,于是我们得到了一颗纯黑色节点的树(四叉树),如下图:
在这里插入图片描述
(该图截自b站动画讲编程)
这颗黑树的高度显然就是bh,且我们可以得到之前红黑树的节点数最少为2^bh-1(最少就是全是黑色)。假设这棵红黑树的节点总数为N,则N>=2^bh-1,两边取对数得bh<=log(N+1)(以2为底)。

设h为红黑树得最大高度,则有h=2*hb=2log(N+1)(最长路径节点数是2*bh)

结论

红黑树的高度最大为2log(N+1),N表示树的节点总数。这个证明基于红黑树的性质。
得到了节点数与树的高度的关系,我们也就能得到红黑树查找数据的效率为O(logN)。此外,红黑树的插入效率和删除效率取决于查找效率,也是O(logN)。

红黑树与AVL树比较

相对来说,红黑树的内部实现细节没有AVL树这么复杂,虽然AVL树保证具备严格平衡性,但是其插入元素时调整平衡的操作相对也就繁琐。插入和删除的时候,AVL树可能会进行更多的旋转操作来维持平衡,导致实际的执行时间可能高于红黑树

总的来说,红黑树适用于读写均衡的场景,插入和删除操作较为高效。而AVL树适用于查询频繁的场景,查询效率更高,但是插入和删除的维护成本较高。即使是这样,两者的差别依旧不大,查找、删除、插入的时间复杂度都是OlogN。

红黑树的插入

上面我们已经了解了红黑树的特性,接下来谈谈红黑树插入数据时是如何维护上述特性的。

红黑树的节点定义

与AVL实现类似,红黑树的节点依旧有三个指针分别指向父节点、左孩子节点、右孩子节点。且有一个变量表示当前节点的颜色。初始默认是红色。为什么默认设置为红色呢?这是因为插入节点时,新节点一定与空叶子节点相邻。而根据红黑树的性质,空叶子节点的颜色是黑色,这就使得,如果新插入节点是黑色,那么每插入一次这条路径就一定会多出来一个黑色节点,即一定要调整。虽然新节点是红色也可能会需要调整,但影响会少很多。

template<class K,class V>
struct RBTreeNode {
	RBTreeNode<T, V>* _parent;
	RBTreeNode<T, V>* _left;
	RBTreeNode<T, V>* _right;
	pair<K, V> _kv;//键值对

	Colour _col;//颜色

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

	}
};

调整策略

如果新插入一个节点,破坏了红黑树的性质,那么我们需要进行调整。具体需要调整的情况如下:

  1. 如果插入节点是根节点:那么只需要将根节点变黑
  2. 插入节点的叔叔节点是红色:父亲节点和叔叔节点变成黑色,爷爷变成红色,且爷爷节点变成新插入节点
  3. 插入节点的叔叔节点是黑色(为空也是黑色):需要先旋转,然后变色

上述2,3情况一定是出现两个相邻节点是红色。

思考情况2:

为什么我们要把父亲节点和叔叔节点变成黑色,爷爷变成红色,且爷爷节点变成新插入节点?为什么这样做一定能维护红黑树的性质呢?
在这里插入图片描述

  • 首先,把父亲节点变黑是因为相邻两个节点是红色,那为什么要把叔叔节点也变色呢
    这是因为新节点插入的这条路径可能会多一个黑色节点。为了让所有路径黑色节点数都能加一,每次调节父节点颜色的同时,也要调节叔叔节点颜色。这样一来,即使最后新节点插入的这条路径黑色节点数加一,其它所有路径的黑色节点也能同步加一。

  • 其实将爷爷节点变色也是为了维护所有路径黑色节点数一致这一特性
    设想我们不将爷爷节点变成红色,此时整个红黑树确实没有出现“相邻的红色节点”,但是经过爷爷节点的路径的黑色节点数目就会比其它路径黑色节点数目多(parent和uncle都是变黑了)。除非此时爷爷节点是根节点(所有路径都经过根节点)。所以我们选择继续向上调整,把爷爷节点变成红色,并更新cur指针指向grandparent节点,看是否还会出现“红红”的俩节点。把爷爷节点变红是基于贪心思路:先不让路径的黑色节点数加一试试能不能平衡

在这里插入图片描述

什么时候停止调整呢?父节点的颜色是黑色为止。此时红黑树平衡

思考情况3:

如果叔叔节点本来就是黑色的呢?(空节点也是黑色)此时无论如何调节叔叔节点颜色,都有可能使得其它路径(比如叔叔这条路径)黑色节点数少于当前新节点插入路径的黑色节点数。此时考虑旋转。旋转的方式和AVL树是一致的。
旋转一共有四种方式:左旋、右旋、左右双旋、右左双旋。
细分情况3,一共有以下四种情况:
1.curparent的左孩子,parentgrandparent的左孩子。
在这里插入图片描述
将grandparent节点右旋转,旋转之后parent顶替了原来的grandparent,并变成黑色。grandparent成为parent的右孩子,变成红色
在这里插入图片描述
旋转是不会改变二叉搜索树的特性的。那么思考这样一个问题,旋转之后还是平衡的红黑树吗?答案是–是的。因为我们观察旋转之后经过parent的所有路径的黑色节点数压根就没有变。

2.curparent的右孩子,parentgrandparent的右孩子
这种情况和情况1一样,只不过方向相反,是左旋。不做过多徐述。
在这里插入图片描述
3.curparent孩子,parentgrandparent孩子

此时这种情况对grandparent节点进行左单旋还是右单旋都不能保证红黑树平衡,于是考虑双旋,即旋转两次。
在这里插入图片描述
先对parent节点左旋,发现左旋之后就变成了情况1
在这里插入图片描述
此时我们就可以像情况1一样,对grandparent进行右旋,对cur节点和grandparent节点进行变色
在这里插入图片描述
这样红黑树就平衡了,整个过程并没有增加路径的黑色节点的数量,因此也就不需要继续向上调整。

  1. curparent孩子,parentgrandparent孩子
    思路跟情况3一致,只不过旋转方向相反。向对parent节点进行右旋,再对grandparent节点进行左旋,再对cur和grandparent节点进行变色。

在这里插入图片描述

代码实现

myBTRee.h

封装了红黑树的类模板,包括各种功能的声明于定义

#pragma once

#include<vector>
#include<iostream>
using namespace std;

enum Colour{
	RED,
	BLACK
};

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

	Colour _col;//颜色

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

	}
};

template<class K,class V>
class RBTree {
	typedef RBTreeNode<K, V> Node;
	typedef Node* pNode;
public:
	bool Insert(const pair<K, V>& kv) {
		if (_root == nullptr) {
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}
		//找到一个合适的插入位置
		pNode cur = _root;
		pNode parent = nullptr;
		while (cur) {
			if (cur->_kv.first > kv.first) {
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_kv.first < kv.first) {
				parent = cur;
				cur = cur->_right;
			}
			else {
				return false;
			}
		}
		//此时cur为nullptr
		cur = new Node(kv);
		if (cur->_kv.first < parent->_kv.first) {//插入节点
			parent->_left = cur;
			cur->_parent = parent;
		}
		else {
			parent->_right = cur;
			cur->_parent = parent;
		}

		//插入之后需要调节颜色平衡

		while (parent && parent->_col == RED) {
			//找叔叔节点
			pNode grandparent = parent->_parent;//祖父节点一定不为空,因为父节点为红色
			if (parent == grandparent->_left) {//如果叔叔在右边
				pNode uncle = grandparent->_right;
				//叔叔存在且为红,直接都变成黑色
				if (uncle && uncle->_col == RED) {
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandparent->_col = RED;
					//继续往上处理
					cur = grandparent;//cur往上跳两个节点
					parent = cur->_parent;
				}//叔叔节点不存在或者为黑色
				else {
					if (cur == parent->_left) {
						RotateR(grandparent);
						parent->_col = BLACK;
						grandparent->_col = RED;
					}
					else if (cur == parent->_right) {
						RotateL(parent);
						RotateR(grandparent);
						cur->_col = BLACK;
						grandparent->_col = RED;
					}
					break;
				}
			}
		    //如果叔叔在左边
			if (parent == grandparent->_right) {
				pNode uncle = grandparent->_left;
				//叔叔存在且为红,直接都变成黑色
				if (uncle && uncle->_col == RED) {
					parent->_col = BLACK;
					uncle->_col = BLACK;

					//继续往上处理
					grandparent->_col = RED;
					cur = grandparent;//cur往上跳两个节点
					parent = cur->_parent;
				}//叔叔节点不存在或者为黑色
				else {
					if (cur == parent->_left) {
						RotateR(parent);
						RotateL(grandparent);
						cur->_col = BLACK;
						grandparent->_col= RED;
					}
					else if (cur == parent->_right) {
						RotateL(grandparent);
						parent->_col = BLACK;
						grandparent->_col = RED;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return true;
	}

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

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		subL->_right = parent;

		Node* ppNode = parent->_parent;
		parent->_parent = subL;

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

			subL->_parent = ppNode;
		}
	}

	void RotateL(Node* parent)//左旋
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

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

		parent->_parent = subR;

		if (parent == _root)
		{
			_root = subR;
			_root->_parent= nullptr;
		}
		else
		{
			if (ppNode->_right == parent)
			{
				ppNode->_right = subR;
			}
			else
			{
				ppNode->_left = subR;
			}
			subR->_parent = ppNode;
		}
	}
	bool IsBalance() {
		if (_root->_col == RED)return false;

		int targetnum = 0;
		pNode cur = _root;
		while (cur) {
			if (cur->_col == BLACK)targetnum++;
			cur = cur->_left;
		}
		return _Check(_root, targetnum, 0);
	}
	void InOrder() {
		_InOrder(_root);
	}
private:

	bool _Check(pNode root,int targetnum,int blacknum) {
		if (root == nullptr) {
			if (blacknum != targetnum) {//路径的黑色节点数不相等
				return false;
			}
			else return true;
		}
		if (root->_left && root->_col == RED && root->_left->_col == RED)return false;
		if (root->_right && root->_col == RED && root->_right->_col == RED)return false;
		
		if (root->_col == BLACK)blacknum++;

		return _Check(root->_left,targetnum,blacknum) && _Check(root->_right,targetnum,blacknum);
	}

	void _InOrder(pNode root) {
		if (root == nullptr) {
			return;
		}
		_InOrder(root->_left);
		cout << root->_kv.first << " " << root->_kv.second << endl;
		_InOrder(root->_right);
	}

	pNode _root=nullptr;

};

test.cpp

用于测试代码的正确性。给出一组随机数,插入到红黑树中之后进行平衡检查。平衡检查内容为,检查是否出现连个相邻的红色节点,且所有路径的黑节点·数目是否相等。
代码:

void TestRBTree2()
{
	const int N = 100000;
	vector<int> v;
	v.reserve(N);
	srand((unsigned int)time(0));

	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
		//cout << v.back() << endl;
	}

	size_t begin2 = clock();
	RBTree<int, int> t;
	for (auto e : v)
	{
		t.Insert(make_pair(e, e));
	}
	size_t end2 = clock();
	cout << t.IsBalance() << endl;
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1678116.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenAI 推出革命性新模型 GPT-4o:全能AI的新纪元

GPT-4o 模型的推出预示着人工智能领域的又一次飞跃&#xff0c;它将如何改变我们的世界&#xff1f; 在人工智能的快速发展浪潮中&#xff0c;OpenAI 再次站在了技术革新的前沿。2024年5月14日&#xff0c;OpenAI 宣布了其最新旗舰模型 GPT-4o&#xff0c;这不仅是一个简单的版…

GitHub操作

远程库-GitHub GitHub网址 GitHub是全球最大的远程库 1. 创建远程库 2. 远程仓库操作 2.1 创建远程仓库别名 git remote -v 查看当前所有远程库地址别名 git remote add 别名 远程地址 设置远程库地址别名 案例操作 起一个别名会出现两个别名&#xff0c;是因为既可以拉取…

艺人百度百科怎么创建

创建艺人百度百科是一个相对复杂的过程&#xff0c;需要遵循一定的步骤和规则。以下是百科优化网yajje整理的艺人百度百科创建指南&#xff1a; 了解百度百科的创建流程 在创建艺人百度百科页面之前&#xff0c;首先需要了解百度百科的创建流程。可以通过访问百度百科的官方网…

uniapp如何打包预约上门按摩APP

uniapp如何打包预约上门按摩APP&#xff1f; 开发工具&#xff1a;HBuilderX 一、创建移动应用 1、 点击此处微信开放平台 2、点击【管理中心 - 移动应用 - 创建移动应用】填写资料后等待审核 app运行流程图 签名如何获取&#xff1a; 1&#xff09;先把打包好的app安装在手…

防爆安检系统市场规模保持增长态势 行业将向智能化方向发展

防爆安检系统市场规模保持增长态势 行业将向智能化方向发展 防爆安检系统&#xff0c;是指为了防止爆炸物品及其他危险物品进入特定区域而设置的一套完整的设备系统&#xff0c;细分产品包括金属探测器、生物安全检测设备、爆炸物探测器等。防爆安检系统能够有效检测并识别出潜…

单文件组件,为什么要使用 SFC

介绍 Vue 的单文件组件 (即 *.vue 文件&#xff0c;英文 Single-File Component&#xff0c;简称 SFC) 是一种特殊的文件格式&#xff0c;使我们能够将一个 Vue 组件的模板、逻辑与样式封装在单个文件中。下面是一个单文件组件的示例&#xff1a; <script setup> impor…

【Qt】之【CMake】Error : The source.. does not match the soused

QT中cmak编译出现CMake Error: The source… does not match the soused 分析 前提是该项目是从另一个路径的项目复制过来的&#xff0c;编写代码时发现无论怎样修改代码&#xff0c;运行后都没有任何变化&#xff0c;以为是qtbug&#xff0c;重构重启都没用&#xff0c;最后…

Spring注解驱动开发

1、Spring注解驱动开发图解

旅游集市数仓建设

旅游集市数仓建设 小白如何从0到1成为大数据工程师 目录 旅游集市数仓建设 1.上传数据 2.可能用到的UDF函数 3.创建所需数据库及表 1&#xff09;ODS层 ①ods_oidd ②ods_wcdr ③ods_ddr ④ods_dpi 2&#xff09;DWD层 ①dwd_res_regn_mergelocation_msk_d ②dwm_s…

2024年5月面试知识点梳理

2024年5月面试知识点梳理 资料来源Java基础泛型注解异常反射SPI机制Java集合CollectionMap 并发基础线程并发关键字并发集合Lock核心类并发集合核心类原子类核心类线程池核心类ScheduledThreadPoolExecutorForkJoinPoolFokJoinTask JUC原子类: CAS, Unsafe和原子类详解JUC 工具…

【Linux】linux | 配置系统日志 | 安全日志 | 操作日志 | 登录日志

一、诉求 1、linux服务器开启日志功能&#xff0c;并记录10个月的登录 二、操作 1、进入目录 cd /etc 2、编辑配置 vi logrotate.conf 3、复制配置 /var/log/wtmp {monthlycreate 0664 root utmpminsize 1Mrotate 10 }/var/log/btmp {missingokmonthlycreate 0600 root …

多客陪玩系统,陪玩系统源码,线下搭子,爆改家政整理师等功能,陪玩预约系统 定制化陪玩系统,陪玩软件APP小程序H5游戏陪玩成品软件源码

简述 陪玩系统源码是指一款游戏陪玩平台的程序代码。陪玩系统通常是一个在线平台&#xff0c;可以让用户通过该平台找到愿意为他们提供游戏陪玩服务的人员&#xff0c;从而帮助他们在游戏中取得更好的成绩。这种系统通常包括客户端和服务器端两个部分&#xff0c;客户端用于用…

【强训笔记】day23

NO.1 思路&#xff1a;直接计算结果&#xff0c;先计算怪物可以抗几次攻击&#xff0c;再计算勇士受到的伤害&#xff0c;如果勇士的攻击力大于等于怪物的血量&#xff0c;那么就可以击杀无数只&#xff0c;如果勇士的血量正好是受到攻击的整数倍&#xff0c;那么击杀的怪物数…

服务攻防——应用协议软件,设备平台

向日葵利用 vnc利用5900端口 当为none就可以直接连接&#xff0c;而其他几种密码也能破解 可以使用hydna来尝试爆破 teamviewer(cve2020-13699) 让对方点击这个网站&#xff0c;就会 触发 zabbix 端口10051 cve2020 手工 点击这个 找到cookie 然后不需要密码就能进…

手心经常出汗、多汗是怎么回事?

点击文末领取揿针的视频教程跟直播讲解 手心爱出汗是一种怎样的体验&#xff1f; 手握着鼠标&#xff0c;一会儿就变得湿漉漉&#xff0c;小心翼翼拿着手机&#xff0c;防止它从满是汗的手掌滑出去……其实手心总出汗可能是一种病。 很多人都有手掌心出汗的症状。如果年轻健康…

【leetcode面试经典150题】-26. 删除有序数组中的重复项

26. 删除有序数组中的重复项 1 题目介绍1 个人解题思路1.1 解题代码1.2 思路解析 2、分析官方题解2.1 快慢双指针 1 题目介绍 给你一个 非严格递增排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新…

佛山信息学真题 桂城-2021-五年级1.2

一、两位数&#xff08;GC4091&#xff09; GC4091 GC.2021.五年级.01.两位数http://43.139.152.26/d/DH_Trial/p/GC4091 题目描述 小明正在思考一个数学问题&#xff1a;有一个两位数&#xff0c;它的十位数大于等于个位数。现已知它的十位数和个位数之和为a&#xff0c;十位…

在pycharm添加pyqt5外部工具插件

一&#xff1a;查看环境所在位置以及安装pyqt5库 1、打开anaconda&#xff0c;输入以下命令&#xff0c;查看环境名&#xff0c;以及环境所在位置。 conda info --envs 从图中得知以下信息&#xff0c;下面根据自己实际情况&#xff0c;记住环境名和路径 ①环境名是&#xf…

Google IO 2024有哪些看点呢?

有了 24 小时前 OpenAI 用 GPT-4o 带来的炸场之后&#xff0c;今年的 Google I/O 还未开始&#xff0c;似乎就被架在了一个相当尴尬的地位&#xff0c;即使每个人都知道 Google 将发布足够多的新 AI 内容&#xff0c;但有了 GPT-4o 的珠玉在前&#xff0c;即使是 Google 也不得…

数据结构_链表基本操作的实现_代码_例题

一、基本操作实现 1.按位序插入&#xff08;带头节点&#xff09; 2.按位序插入&#xff08;不带头节点&#xff09; 3.指定结点的后插操作 4.指定结点的前插操作 5.按位序删除&#xff08;带头节点&#xff09; 6.指定结点的删除 7.按位查找&#xff0c;返回第i个元素&…