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

文章目录

  • 红黑树的特性
  • 红黑树的时间复杂度
    • 推导:
    • 结论
    • 红黑树与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为nullptrcur = 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.mzph.cn/bicheng/12800.shtml

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

相关文章

阻抗控制理解之逆动态控制律

具有六个自由度的二阶机械系统&#xff0c;其特征是给定的质量、阻尼和刚度&#xff0c;称为机械阻抗。 用于运动控制的加速度解决方法&#xff0c;它旨在通过逆动力学控制律在加速度水平上解耦和线性化非线性机器人动力学。在与环境存在交互作用的情况下&#xff0c;控制律 考…

代码托管(二)git(4)冲突解决

一、pull更新代码冲突 二、cherry-pick冲突 1、冲突演示 本地check out到当前分支&#xff0c;双击目标分支&#xff0c;选择需要cherry-pick的commit&#xff0c;右键选择cherry-pick。 &#xff08;1&#xff09;如果没有冲突&#xff0c;会自动add--》并自动commit&#…

Sam Blackshear谈Move语言的创新

Sui上的Move编程语言融合了三项基本创新&#xff1a;面向对象的数据模型、可编程交易区块和动态字段。Mysten Labs的联合创始人兼首席技术官Sam Blackshear在五年前创建了Move&#xff0c;这是一种专门为在区块链上编写智能合约而设计的语言。这三项在Sui上的创新提升了Move的潜…

【nfs服务部署服务端和客户端搭建】

原理 NFS&#xff08;Network File System&#xff09;是文件服务器之一。它的功能是可以通过网络&#xff0c;让不同的机器、不同的操作系统可以彼此共享数据文件。 NFS服务器可以让服务端的共享目录挂载到本地端的文件系统中&#xff0c;其他服务器如果想访问共享目录&#…

单链表经典算法 面试题--力扣02.04

链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09;【点击即可跳转】 思路&#xff1a;创建新链表&#xff1a;大链表和小链表 将pcur节点小于x的值&#xff0c;尾插在小链表中 将pcur节点大于或等于x的值&#xff0c;尾插在大链表中 最终---- return lessHead->…

护眼 保护眼睛

高度近视600度的我&#xff0c;花了4个月时间度数降了50度&#xff0c;又花了一个多月的时间&#xff0c;双眼50度散光清零了。 然后就有人问我:散光都可以恢复吗&#xff1f; 我有点被问懵了&#xff0c;散光难道不容易恢复吗&#xff1f;直到我在网上查了一下&#xff0c;才发…

vue3和vite

vue3 1、vue3使如何实现效率提升的 客户端渲染效率比vue2提升了1.3~2倍 SSR渲染效率比vue2提升了2~3倍 1.1、静态提升 解释&#xff1a; 1. 对于静态节点&#xff08;如&#xff1a;<h1>接着奏乐接着舞</h1>&#xff09;&#xff0c;vue3直接提出来了&#xff…

whisper使用

whisper使用 1. 直接调用 语音识别2. 语种识别 whisper.detect_language()和whisper.decode()3. 指定要识别的语种做语音识别**whisper 源码的transcribe函数** 函数解析1. transcript.py2. tokenizer.py3. audio.py4. __ init__.py github: https://gitcode.com/openai/whispe…

【JAVA进阶篇教学】第十六篇:Java中AOP使用

博主打算从0-1讲解下java进阶篇教学&#xff0c;今天教学第十五篇&#xff1a;Java中AOP使用。 AOP&#xff08;Aspect-Oriented Programming&#xff09;是一种编程范式&#xff0c;它允许开发者在不修改源代码的情况下&#xff0c;对代码进行横切关注点的分离和增强。在 Java…

Togaf培训简介

Togaf简单讲 1.定义事物&#xff0c;方便大家互相理解 2.做好现状和愿景设计 3.做好现状到愿景的计划 1.togaf 首先是统一语言。大家互相能理解&#xff0c;比如各种定义。 togaf源自美国军方理论 2.没有架构设计&#xff0c;烟囱式系统是必然的。 就跟之前去政府办户籍一…

抖店曝光率高,转化低,不知道怎么提升转化率?试试这四个方法

大家好&#xff0c;我是醒醒团队电商花花。 我们现在做抖音小店的商家或多或少都会遇到不出单&#xff0c;转化低的各种问题。 明明店铺的曝光不低&#xff0c;访客也不少&#xff0c;就是没转化。 下面我根据我们做店的经验&#xff0c;给大家分享一些问题所在&#xff0c;…

Git—安装及介绍

下载Git 官网地址&#xff1a;Git - Downloads (git-scm.com) 安装 双击安装包 点击 next 检查安装 桌面&#xff0c;右键鼠标&#xff0c;是否出现 Git GUI Here 和 Git Bash Here 打开Git Bash Here 输入命令 git --verison

NX/UG软件使用—策略OK回调环境变量

新建环境变量UGII_CAM_OPERATION_OK_EXIT&#xff0c;变量值为需要执行的xx.dll路径&#xff0c;设置后&#xff0c;重启NX&#xff0c;那么在每次点击策略OK之后&#xff0c;会自动执行xx.dll(注意这个dll需要在配置好的工具目录里&#xff0c;也就是说NX能手动加载这个dll)。…

finallyshell激活-支持所有版本(老版 + 最新版) + 所有平台(mac + windows)

一&#xff1a;打开finally shell的激活页面 二&#xff1a;点击离线激活 三&#xff1a;复制机器码&#xff0c;然后执行一下代码 原文&#xff1a;大哥原文&#xff0c;但是这个大佬是用java实现的&#xff0c;执行因为依赖的问题一直报错 基于以上问题&#xff0c;所以使…

YoLov9目标检测算法的使用

目录 一、环境安装 1、创建虚拟环境 2、安装依赖库 二、数据集准备 1、数据集的文件名 2、划分数据集 3、配置数据文件 4、修改模型结构文件的类别 5、下载模型预训练权重 三、训练 1、训练的三个文件介绍 2、训练 3、验证 4、检测单张图片 四、附录 1、训练参…

6. 神经网络的内积

目录 1. 准备知识 1.1 NumPy 的多维数组 1.2 矩阵乘法 1.2.1 矩阵乘法顺序 1.2.2 矩阵乘法范例 2. 神经网络的内积 2.1 使用场合 2.2 Python 实现 1. 准备知识 1.1 NumPy 的多维数组 大家应该对多维数组都很熟悉&#xff0c;我不再多言。在 NumPy 模块中&#xff0c;…

声纹识别在无人机探测上的应用

无人机在民用和军事领域的应用越来越广泛。然而&#xff0c;随着无人机数量的增加&#xff0c;"黑飞"现象也日益严重&#xff0c;对公共安全和隐私构成了威胁。因此&#xff0c;开发有效的无人机探测与识别技术变得尤为重要。及时发现黑飞无人机的存在进而对其型号进…

AI地名故事:鸦岗村

鸦岗村&#xff0c;位于广州市白云区石井镇&#xff0c;是一个历史悠久、文化底蕴深厚的村落。据《广州地名志》记载&#xff0c;南宋时期&#xff0c;南雄珠玑巷的凌氏家族迁移至此地&#xff0c;并在此建立村落。由于村子周边的山岗上常有乌鸦栖息&#xff0c;因此得名“鸦岗…

Redisson中分布式锁的实现原理

redisson版本&#xff1a;3.27.2 简介 锁归根结底就是对同一资源的竞争抢夺&#xff0c;不管是在单体的应用亦或者集群的服务中&#xff0c;上锁都是对同一资源进行修改的操作。至于分布式锁&#xff0c;那就是多个服务器或资源&#xff0c;同时抢占某一单体应用的同个资源了。…