数据结构:红黑树讲解(C++)

红黑树

    • 1.前言
    • 2.红黑树简述
      • 2.1概念
      • 2.2性质
    • 3.红黑树的插入
      • 3.1关于新插入节点的颜色
      • 3.2节点的定义
      • 3.3插入新节点
      • 3.4判断插入后是否需要调整
      • 3.5插入后维持红黑树结构(重点)
        • 3.5.1cur、p、u为红,g为黑
        • 3.5.2cur、p为红,g为黑,u为空/u存在为黑
    • 4.一些简单的测试接口
    • 5.完整代码

1.前言

  • 本文旨在理解红黑树基本概念以及变色旋转规则,以理解C++mapset的底层原理,不会讲红黑树的删除操作。
  • 对于基本的旋转操作(单旋和双旋),本文不会展开讲,详细讲解在这里:
    AVL树旋转讲解。



2.红黑树简述

2.1概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是RedBlack。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保最长路径不超过最短路径两倍,因而是接近平衡的。


2.2性质

  1. 每个节点不是红色就是黑色。
  2. 根部节点是黑色的。(为了减少旋转次数,后面讲旋转大家就明白了)
  3. 对于一个红节点,它的孩子只能是黑色。(即一条路径上不能出现连续的红色节点)
  4. 每条路径都必须包含相同数量的黑色节点。

通过上面规则的限制,红黑树最长路径一定不会超过最短路径两倍,也就维持了高度的相对平衡
结合3、4来看下面的两条路径:
最长:黑、红、黑、红、黑、红…………
最短:黑、黑、黑…………



3.红黑树的插入

3.1关于新插入节点的颜色

对于新插入节点,我们设置为红色,原因是红黑树每条路径都必须包含相同数量的黑色节点(性质4),新插入红节点不一定破坏红黑树的结构,新插入黑色节点一定不符合性质4而且很难调整。
在这里插入图片描述


3.2节点的定义

//用枚举来定义颜色
enum Color
{RED,BLACK
};//这里直接实现key_value模型
template<class K, class V>
struct RBTreeNode
{RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;  //涉及到旋转,多加父亲指针来简化操作pair<K, V> _kv;  //存储键值对Color _col; //颜色RBTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv),_col(RED)  //新节点颜色为红色{}
};

3.3插入新节点

这里比较简单,按二叉搜索树的规则插入即可:

bool Insert(const pair<K, V>& kv)
{if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}Node* cur = _root;Node* parent = nullptr;while (cur){if (kv.first > cur->_kv.first) //待插入节点在右子树{parent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first)  //待插入节点在左子树{parent = cur;cur = cur->_left;}else  //相同{return false;}}cur = new Node(kv);if (kv.first > parent->_kv.first) //新节点在父亲右子树{parent->_right = cur;}else  //新节点在父亲左子树{parent->_left = cur;}cur->_parent = parent;  //记得更新父亲指针/// 变色旋转维持红黑树结构(暂时省略)  //_root->_col = BLACK; //可能改变根部颜色,保持根部为黑色return true;
}

3.4判断插入后是否需要调整

其实红黑树插入后只需要看当前节点和父亲的颜色即可,其中新节点一定为红。

  1. 父亲为黑,符合规则,不需要调整。
  2. 父亲为红,此时出现红红的连续节点,需要进行调整。

3.5插入后维持红黑树结构(重点)

为了方便叙述,我们做如下定义:

  1. cur表示当前节点
  2. p表示cur父亲节点
  3. u表示叔叔节点
  4. g表示祖父(p和u的父亲)节点
3.5.1cur、p、u为红,g为黑

在这里插入图片描述
代码:

while (parent && parent->_col == RED)  //父亲为红就调整,调整到根部要结束
{Node* granderfather = parent->_parent;  //祖父//需要对叔叔进行操作,需要判断叔叔是祖父的左还是右if (parent == granderfather->_left)  //父亲是祖父的左子树{Node* uncle = granderfather->_right;if (uncle && uncle->_col == RED) //叔叔不为空并且叔叔为红,变色即可{uncle->_col = parent->_col = BLACK;granderfather->_col = RED; //当前子树可能为部分,继续向上调整cur = granderfather;parent = cur->_parent;}else  //叔叔为空或为黑色{ //先省略}}else  //父亲是祖父的右子树{Node* uncle = granderfather->_left;if (uncle && uncle->_col == RED)  //叔叔不空并且为红{parent->_col = uncle->_col = BLACK;granderfather->_col = RED;  //当前可能为部分子树,需要继续上调cur = granderfather;parent = cur->_parent;}else  //叔叔为空或为黑色{// 先省略}}
}

3.5.2cur、p为红,g为黑,u为空/u存在为黑

下面是一会要用到的旋转接口:

void RotateL(Node* parent)  //左单旋,rotate->旋转
{Node* SubR = parent->_right;Node* SubRL = SubR->_left;  //这个有可能为空Node* ppnode = parent->_parent;  //原来父亲的父亲parent->_right = SubRL;if (SubRL)  SubRL->_parent = parent;SubR->_left = parent;parent->_parent = SubR;if (ppnode == nullptr)  //旋转的是整颗树{_root = SubR;SubR->_parent = nullptr;}else  //旋转的是部分{if (ppnode->_left == parent) //是左子树{ppnode->_left = SubR;}else  //是右子树{ppnode->_right = SubR;}SubR->_parent = ppnode;}
}void RotateR(Node* parent)  //右单旋细节处理和左单旋差不多
{Node* SubL = parent->_left;Node* SubLR = SubL->_right;  //这个有可能为空Node* ppnode = parent->_parent;parent->_left = SubLR;if (SubLR)  SubLR->_parent = parent;SubL->_right = parent;parent->_parent = SubL;if (ppnode == nullptr)  //旋转的是整颗树{_root = SubL;SubL->_parent = nullptr;}else  //旋转部分{if (ppnode->_left == parent)  //是左子树{ppnode->_left = SubL;}else  //右子树{ppnode->_right = SubL;}SubL->_parent = ppnode;}
}

涉及旋转情况比较复杂,分开讨论:

(1)p为g的左孩子,cur为p的左孩子
在这里插入图片描述


(2)p为g的左孩子,cur为p的右孩子

在这里插入图片描述


(3)p为g的右孩子,cur为p的右孩子

在这里插入图片描述


(4)p为g的右孩子,cur为p的左孩子

在这里插入图片描述

整合一下(1、2、3、4)得到下面的调整代码:

//到这里插入新节点的工作完成,下面进行结构调整:
while (parent && parent->_col == RED)  //父亲为红就调整,调整到根部要结束
{Node* granderfather = parent->_parent;  //祖父if (parent == granderfather->_left)  //父亲是祖父的左子树,p为g的左孩子{Node* uncle = granderfather->_right;if (uncle && uncle->_col == RED) //叔叔不为空并且叔叔为红,变色即可{uncle->_col = parent->_col = BLACK;granderfather->_col = RED; //当前子树可能为部分,继续向上调整cur = granderfather;parent = cur->_parent;}else  //叔叔为空或为黑色{ //     g//   p   u// cif (cur == parent->_left)  //当前为父亲的左子树,cur为p的左孩子{RotateR(granderfather);granderfather->_col = RED;parent->_col = BLACK;}else   //当前为父亲的右子树,cur为p的右孩子{//    g//  p   u//    c//左右双旋RotateL(parent);RotateR(granderfather);granderfather->_col = RED;cur->_col = BLACK;}break;  //这两种情况调整完可以结束}}else  //父亲是祖父的右子树,p为g的右孩子{Node* uncle = granderfather->_left;if (uncle && uncle->_col == RED)  //叔叔不空并且为红{parent->_col = uncle->_col = BLACK;granderfather->_col = RED;  //当前可能为部分子树,需要继续上调cur = granderfather;parent = cur->_parent;}else  //叔叔为空或为黑色{if (cur == parent->_right)  //当前为父亲的右,cur为p的右孩子{//    g//  u   p//        c//左旋RotateL(granderfather);parent->_col = BLACK;granderfather->_col = RED;}else  //当前为父亲的左,cur为p的左孩子{//   g// u   p//   c//右左双旋RotateR(parent);RotateL(granderfather);cur->_col = BLACK;granderfather->_col = RED;	}break;  //这两种情况调整完可以结束}}
}
_root->_col = BLACK; //保持根部为黑色



4.一些简单的测试接口

void InOrder()   //中序遍历,验证是否为二叉搜索树
{_InOrder(_root);cout << endl;
}void _InOrder(Node* root)
{if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);
}// 根节点->当前节点这条路径的黑色节点的数量
bool Check(Node* root, int blacknum, const int refVal)  
{if (root == nullptr)  //到根部看看当前路径黑色节点和标准值是否一致{//cout << balcknum << endl;if (blacknum != refVal){cout << "存在黑色节点数量不相等的路径" << endl;return false;}return true;}/检查子比较复杂,可以反过来去检查红节点父是否为黑色if (root->_col == RED && root->_parent->_col == RED)  {cout << "有连续的红色节点" << endl;return false;}if (root->_col == BLACK){++blacknum;  //为黑节点加一}return Check(root->_left, blacknum, refVal)&& Check(root->_right, blacknum, refVal);
}bool IsBalance()
{if (_root == nullptr)return true;if (_root->_col == RED)return false;//参考值,即先算出一条路径的黑色节点数int refVal = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK){++refVal;}cur = cur->_left;}int blacknum = 0;return Check(_root, blacknum, refVal);
}



5.完整代码

#pragma once
#include <iostream>
#include <utility>
using namespace std;//用枚举来定义颜色
enum Color
{RED,BLACK
};//这里直接实现key_value模型
template<class K, class V>
struct RBTreeNode
{RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;  //涉及到旋转,多加父亲指针来简化操作pair<K, V> _kv;  //存储键值对Color _col; //颜色RBTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv),_col(RED)  //新节点颜色为红色{}
};template<class K, class V>
class RBTree
{
public:typedef RBTreeNode<K, V> Node;bool Insert(const pair<K, V>& kv){if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}Node* cur = _root;Node* parent = nullptr;while (cur){if (kv.first > cur->_kv.first) //待插入节点在右子树{parent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first)  //待插入节点在左子树{parent = cur;cur = cur->_left;}else  //相同{return false;}}cur = new Node(kv);if (kv.first > parent->_kv.first) //在右子树{parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;while (parent && parent->_col == RED)  //父亲为红就调整,调整到根部要结束{Node* granderfather = parent->_parent;  //祖父if (parent == granderfather->_left)  //父亲是祖父的左子树{Node* uncle = granderfather->_right;if (uncle && uncle->_col == RED) //叔叔不为空并且叔叔为红,变色即可{uncle->_col = parent->_col = BLACK;granderfather->_col = RED; //当前子树可能为部分,继续向上调整cur = granderfather;parent = cur->_parent;}else  //叔叔为空或为黑色{ //     g//   p   u// cif (cur == parent->_left)  //当前为父亲的左子树{RotateR(granderfather);granderfather->_col = RED;parent->_col = BLACK;}else   //当前为父亲的右子树{//    g//  p   u//    c//左右双旋RotateL(parent);RotateR(granderfather);granderfather->_col = RED;cur->_col = BLACK;}break;}}else  //父亲是祖父的右子树{Node* uncle = granderfather->_left;if (uncle && uncle->_col == RED)  //叔叔不空并且为红{parent->_col = uncle->_col = BLACK;granderfather->_col = RED;  //当前可能为部分子树,需要继续上调cur = granderfather;parent = cur->_parent;}else  //叔叔为空或为黑色{if (cur == parent->_right)  //当前为父亲的右{//    g//  u   p//        c//左旋RotateL(granderfather);parent->_col = BLACK;granderfather->_col = RED;}else  //当前为父亲的左{//   g// u   p//   c//右左双旋RotateR(parent);RotateL(granderfather);cur->_col = BLACK;granderfather->_col = RED;	}break;}}}_root->_col = BLACK; //保持根部为黑色return true;}/// //
/// /
/// 	测试代码void InOrder()   //中序遍历,验证是否为二叉搜索树{_InOrder(_root);cout << endl;}void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}// 根节点->当前节点这条路径的黑色节点的数量bool Check(Node* root, int blacknum, const int refVal)  {if (root == nullptr)  //到根部看看当前路径黑色节点和标准值是否一致{//cout << balcknum << endl;if (blacknum != refVal){cout << "存在黑色节点数量不相等的路径" << endl;return false;}return true;}/检查子比较复杂,可以反过来去检查红节点父是否为黑色if (root->_col == RED && root->_parent->_col == RED)  {cout << "有连续的红色节点" << endl;return false;}if (root->_col == BLACK){++blacknum;  //为黑节点加一}return Check(root->_left, blacknum, refVal)&& Check(root->_right, blacknum, refVal);}bool IsBalance(){if (_root == nullptr)return true;if (_root->_col == RED)return false;//参考值,即先算出一条路径的黑色节点数int refVal = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK){++refVal;}cur = cur->_left;}int blacknum = 0;return Check(_root, blacknum, refVal);}int Height(){return _Height(_root);}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;}Node* Find(K key){return _Find(key, _root);}Node* _Find(K key, Node* root){if (root == nullptr)return nullptr;if (key > root->_kv.first) //在右子树{return _Find(key, root->_right);}else if (key < root->_kv.first) //在左子树{return _Find(key, root->_left);}else  //找到了{return root;}}private:Node* _root = nullptr;void RotateL(Node* parent)  //左单旋,rotate->旋转{Node* SubR = parent->_right;Node* SubRL = SubR->_left;  //这个有可能为空Node* ppnode = parent->_parent;  //原来父亲的父亲parent->_right = SubRL;if (SubRL)  SubRL->_parent = parent;SubR->_left = parent;parent->_parent = SubR;if (ppnode == nullptr)  //旋转的是整颗树{_root = SubR;SubR->_parent = nullptr;}else  //旋转的是部分{if (ppnode->_left == parent) //是左子树{ppnode->_left = SubR;}else  //是右子树{ppnode->_right = SubR;}SubR->_parent = ppnode;}}void RotateR(Node* parent)  //右单旋细节处理和左单旋差不多{Node* SubL = parent->_left;Node* SubLR = SubL->_right;  //这个有可能为空Node* ppnode = parent->_parent;parent->_left = SubLR;if (SubLR)  SubLR->_parent = parent;SubL->_right = parent;parent->_parent = SubL;if (ppnode == nullptr)  //旋转的是整颗树{_root = SubL;SubL->_parent = nullptr;}else  //旋转部分{if (ppnode->_left == parent)  //是左子树{ppnode->_left = SubL;}else  //右子树{ppnode->_right = SubL;}SubL->_parent = ppnode;}}
};

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

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

相关文章

【【VDMA彩条显示实验之三 之 RGB LCD 彩条显示实验 】】

VDMA彩条显示实验之三 之 RGB LCD 彩条显示实验 VDMA彩条显示实验之三 之 RGB LCD 彩条显示实验 LCD 的构造是在两片平行的玻璃基板当中放置液晶盒&#xff0c;下基板玻璃上设置 TFT&#xff08;薄膜晶体管&#xff09;&#xff0c;上基板玻璃上设置彩色滤光片&#xff0c;通…

Flutter 应用启动从闪屏页短暂黑屏再到第一个页面

由于应用初始状态启动会有白屏现象&#xff0c;便使用 flutter_native_splash 2.3.5 插件生成了启动相关的配置&#xff0c;并且按照示例使用了 import package:flutter_native_splash/flutter_native_splash.dart;void main() {WidgetsBinding widgetsBinding WidgetsFlutte…

牛客 —— 链表中倒数第k个结点(C语言,快慢指针,配图)

目录 1. 思路1&#xff1a;倒数第K个节点&#xff0c;就是整数第N-K1的节点 2. 思路2&#xff1a;快慢指针 1. 思路1&#xff1a;倒数第K个节点&#xff0c;就是整数第N-K1的节点 链表中&#xff0c;一共有N个节点&#xff0c;如果我们想要得出倒数第K个节点&#xff0c;我们…

图像倾斜角度求取-Radon变换

Radon算法 Radon&#xff08;拉东&#xff09;算法是一种通过定方向投影叠加&#xff0c;找到最大投影值时角度&#xff0c;从而确定图像倾斜角度的算法。具体过程如图所示 图1 Radon变换算法 Radon计算示例 对于纹理方向明显的图像&#xff0c;如图2所示&#xff0c;可以通…

【设计模式】聊聊模板模式

原理和实现 设计模式的原理和实现是比较简单的&#xff0c;难的是掌握具体的应用场景和解决什么问题。而模板模式是为来解决复用和拓展两个问题。 模板模式在一个方法中定义好一个算法框架&#xff0c;然后将某些步骤推迟到子类中实现&#xff0c;子类可以在不修改父类流程的时…

qsort使用举例和qsort函数的模拟实现

qsort使用举例 qsort是C语言中的一个标准库函数&#xff0c;用于对数组或者其他数据结构中的元素进行排序。它的原型如下&#xff1a; void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); 我们可以去官网搜来看一看&#xff1a;…

lxml基本使用

lxml是python的一个解析库&#xff0c;支持HTML和XML的解析&#xff0c;支持XPath解析方式&#xff0c;而且解析效率非常高 XPath&#xff0c;全称XML Path Language&#xff0c;即XML路径语言&#xff0c;它是一门在XML文档中查找信息的语言&#xff0c;它最初是用来搜寻XML文…

2024全网最新最全的Pytest接口自动化测试框架教程

pytest编写的规则&#xff1a; 1、测试文件以test_开头&#xff08;以_test结尾也可以&#xff09; 2、测试类以Test开头&#xff0c;并且不能带有__init__方法 3、测试函数以test_开头 4、断言必须使用assert pytest.main([-s,-v]) &#xff1a;用来执行测试用例 -s 打印prin…

卷积神经网络(CNN)天气识别

文章目录 前期工作1. 设置GPU&#xff08;如果使用的是CPU可以忽略这步&#xff09;我的环境&#xff1a; 2. 导入数据3. 查看数据 二、数据预处理1. 加载数据2. 可视化数据3. 再次检查数据4. 配置数据集 三、构建CNN网络四、编译五、训练模型六、模型评估 前期工作 1. 设置GP…

EEPROM与Flash的区别

EEPROM与Flash的区别 EEPROMEEPROM内部功能框图实现写入数据内部结构存储管在充电或放电状态下有着不同的阈值电压 问题点EEPROM是如何失效的呢&#xff1f;为何EEPROM不能做大呢&#xff1f; ------------------------------------------------------------------------------…

Java多线程(3)

Java多线程(3) 深入剖析Java线程的生命周期&#xff0c;探秘JVM的线程状态&#xff01; 线程的生命周期 Java 线程的生命周期主要包括五个阶段&#xff1a;新建、就绪、运行、阻塞和销毁。 **新建&#xff08;New&#xff09;&#xff1a;**线程对象通过 new 关键字创建&…

tamarin运行

首先我们找到安装tamarin的文件位置&#xff0c;找到以后进入该文件夹下 ubuntuubuntu:~$ sudo find / -name tamarin-prover /home/linuxbrew/.linuxbrew/var/homebrew/linked/tamarin-prover /home/linuxbrew/.linuxbrew/Cellar/tamarin-prover /home/linuxbrew/.linuxbrew/…

mac下vue-cli从2.9.6升级到最新版本

由于mac之前安装了 vue 2.9.6 的版本&#xff0c;现在想升级到最新版本&#xff0c;用官方给的命令&#xff1a; npm uninstall vue-cli -g 发现不行。 1、究其原因&#xff1a;从vue-cli 3.0版本开始原来的npm install -g vue-cli 安装的都是旧版&#xff0c;最高到2.9.6。安…

基于Netty实现的简单聊天服务组件

目录 基于Netty实现的简单聊天服务组件效果展示技术选型&#xff1a;功能分析聊天服务基础设施配置&#xff08;基于Netty&#xff09;定义组件基础的配置&#xff08;ChatProperties&#xff09;定义聊天服务类&#xff08;ChatServer&#xff09;定义聊天服务配置初始化类&am…

后端接口错误总结

今天后端错误总结&#xff1a; 1.ConditionalOnExpression(“${spring.kafka.exclusive-group.enable:false}”) 这个标签负责加载Bean&#xff0c;因此这个位置必须打开&#xff0c;如果这个标签不打开就会报错 问题解决&#xff1a;这里的配置在application.yml文件中 kaf…

Linux Docker图形化工具Portainer如何进行远程访问?

文章目录 前言1. 部署Portainer2. 本地访问Portainer3. Linux 安装cpolar4. 配置Portainer 公网访问地址5. 公网远程访问Portainer6. 固定Portainer公网地址 前言 Portainer 是一个轻量级的容器管理工具&#xff0c;可以通过 Web 界面对 Docker 容器进行管理和监控。它提供了可…

Flutter最新稳定版3.16 新特性介绍

Flutter 3.16 默认采用 Material 3 主题&#xff0c;Android 平台预览 Impeller&#xff0c;DevTools 扩展等等 欢迎回到每季度一次的 Flutter 稳定版本发布&#xff0c;这次是 Flutter 3.16。这个版本将 Material 3 设为新的默认主题&#xff0c;为 Android 带来 Impeller 预览…

SpringBoot使用DevTools实现后端热部署

&#x1f4d1;前言 本文主要SpringBoot通过DevTools实现热部署的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是青衿&#x1f947; ☁️博客首页&#xff1a;CSDN主页放风讲故事 &#x1f304;每日一句&…

Windows使用ssh远程连接(虚拟机)Linux(Ubuntu)的方法

步骤 1.Windows下载一个SSH客户端软件 要使用SSH连接&#xff0c;当然得先有一个好用的客户端软件才方便。 我这里使用的是WindTerm&#xff0c;一个开源免费的SSH连接工具&#xff0c;用什么软件不是重点。 这里默认你已经生成过SSH的密钥了&#xff0c;如果没有&#xff0c…

C语言 字符函数汇总,模拟实现各字符函数(炒鸡详细)

目录 求字符串长度 strlen 示例 模拟实现strlen 长度不受限制的字符串函数 strcpy 示例 模拟实现strcpy strcat 模拟实现strcat strcmp 示例 模拟实现strcmp 长度受限制的字符串函数介绍 strncpy 示例 模拟实现strncpy strncat 示例 模拟实现strncat s…