C++AVL树拓展之红黑树原理及源码模拟

前言:我们之前已经从零开始掌握AVL树http://t.csdnimg.cn/LaVCCicon-default.png?t=N7T8http://t.csdnimg.cn/LaVCC

现在我们将继续学习红黑树的原理并且实现插入等功能,学习本章的前提要求是掌握排序二叉树和AVL树,本章不再提及一些基础知识,防止本文结构臃肿,对二叉排序树和AVL树有兴趣的可以阅读上面链接的文章,很多人可能说既生瑜何生亮,有了AVL树为什么还要红黑树,当然是因为红黑树的效率更高啦,AVL树的平黑太过于依赖平衡因子,稍微不平衡就会旋转,而大量的旋转自然降低了效率,红黑树相对AVL树没有那么平衡,旋转次数也少了,但是查询效率略微的降低就减少了不少的旋转,何乐而不为呢?更何况C++是一门以高效出名的语言。

目录

一,红黑树的基本准则

二,红黑树为什么是平衡的

三,代码实现

1)敲前准备

2)查找

3)插入

4)迭代器


一,红黑树的基本准则

希望大家先记住红黑树本质还是一颗二叉排序树。在二叉排序树的基础上,AVL树是加了平衡因子,来保持树的结构平衡,红黑树则是通过给每个结点标记颜色达到相对平衡。(为什么要平衡是为了提高查询效率,不懂看链接博客)

1)每个结点的颜色不是黑色就是红色

2)红黑树根节点的颜色是黑色的(这条规定会在后面平衡的调整那里给出原因,现在记住即可)

3)红黑树上不能出现两个相邻的红色结点(红黑树平衡的重要准则)

4)每个叶子结点都是黑色的。(注意这里的叶子结点指的是NULL结点)

5)每条路径上的黑色结点的数目都是一样多的

6)最短路径小于最长路径的两倍(这个其实不是原则,是一个推论,下面会讲解,不必纠结)

二,红黑树为什么是平衡的

接下来我们将讨论一下为什么红黑树是平衡的。

讨论这个性质我们要从上面说的红黑树的基本准则入手。红黑树不过三种情况我们分类讨论

1)结点的颜色全是黑色

如果红黑树的结点颜色全是黑色,那么这棵树必定是一个完全二叉树,因为如果不是完全二叉树,红黑树的结点有全是黑色,那就违背了上面的第五条原则(每条路径上的黑色结点的数目都是一样多的)。

得出来红黑树的结点全是黑色的则次数必定平衡。

2)除了根节点其他结点都是红色

这种情况只有四种情况,我直接给大家画出来,记住不能违背上面的第三个准则(红黑树上不能出现两个相邻的红色结点)。

     

如果再插入结点必然出现黑色结点,不满足我们这种情况了。

3)既有红色结点也有黑色结点

首先根据上面的准则,每条路径上的黑色结点数目一样,红色结点不能相邻出现,也就是两个红色结点之间必然有若干个黑色结点,然而每条路径上黑色结点的数目已经固定了,我们现在看极端情况,也就是最短的路径一个红色结点也没有,最长的路径上每个红色结点之间只有一个黑色结点。

从上面的图可以得到最长路径绝对不会超过最短路径的两倍,因为红色结点的数目不会超过黑色结点 ,当然上面是把路径单独列出来了来,实际上是树状结构。

综上所述红黑树的是一个相对平衡的二叉树。

三,代码实现

1)敲前准备

首先我们需要一个标记位来记录当前结点的颜色,我们采用枚举类型,可读性强

enum color {red,black
};

结点里面的内容应该包括什么呢?data存储数据,三个指针,一个parent指针,一个leftchild和rightchild指针,结构体里面应该包括我们刚刚的枚举。

template<class T>
struct RBTreeNode {RBTreeNode(T data) {_pParent = NULL;_pLeft = NULL;_pRight = NULL;_data = data;c = red;}RBTreeNode() {_pParent = NULL;_pLeft = NULL;_pRight = NULL;c = red;}color c;RBTreeNode* _pParent;RBTreeNode* _pLeft;RBTreeNode* _pRight;T _data;
};

那么大致框架就搭起来了

enum color {red,black
};
template<class T>
struct RBTreeNode {RBTreeNode(T data) {_pParent = NULL;_pLeft = NULL;_pRight = NULL;_data = data;c = red;}RBTreeNode() {_pParent = NULL;_pLeft = NULL;_pRight = NULL;c = red;}color c;RBTreeNode* _pParent;RBTreeNode* _pLeft;RBTreeNode* _pRight;T _data;
};
template<class T>
class RBTree
{
private:Node* _pHead; //哨兵位size_t _size;//结点个数
};
2)查找

查找还是一个老套路,大于当前结点找右边,小于当前结点找左边,直到找到或者为空,属实是老生常谈了,这里不过多介绍。

// 检测红黑树中是否存在值为data的节点,存在返回该节点的地址,否则返回nullptrNode* _Find(const T& data) {Node* cur = _pHead->_pParent;//从根节点开始while (cur&&cur!=_pHead) {if (data < cur->_data)  //小于找左边cur = cur->_pLeft;else if (data > cur->_data) {   //大于找右边cur = cur->_pRight;}elsereturn cur;   //找到返回}return NULL;  //找不到}
3)插入

插入的第一件事就是找到应该插入的位置,这个简单,这个逻辑和查找一样。插入之后的颜色应该是红色还是黑色值得商榷,但仔细考虑,如果插入黑色的话就违背了每条路径上的黑色结点个数相等的原则,插入红色则可能碰到连续的红色结点,那到底是插入红色还是黑色呢?我们现在来讨论一下。

如果插入黑色结点的话,那么完全是牵一发而动全身,因为根据结点规则每条路径上的黑色结点的数目都是一样多的,我们需要把所有路径的黑色结点数目全部增加一个,这显然不是一个明智之举。那我们只剩下一个选择了,插入的新结点默认为红色结点,接下来我们需要分情况讨论。

1)插入结点的父亲结点是黑色,如果是黑色插入红色节点不需要改变任何结点,因为完全满足红黑树的规则,既没有连续的红色结点,每条路径的黑色结点数也都相同。

2)如果是父亲是红色的结点呢?

     注:圆形代表一个结点,长方形代表很多种可能

这种情况我们需要看parent的兄弟结点的颜色了,接下来又要分情况讨论

1)兄弟节点是红色,这种情况我们把两个兄弟节点全变成黑色,把爷爷结点变成红色,然后继续递归往上,往上有两种可能,一种是一直递归到根节点,然后根节点变成红色,最后我们强制把根节点变成黑色就行了,并不会违背任何原则。当然可能中途兄弟节点是黑色,这个时候我们需要使用下面情况2的旋转来弥补了。

2)兄弟节点是黑色的时候,证明单纯靠变色已经无法将这颗红黑树拉上正途了,我们不得已采取暴力手段旋转了,旋转结果仍然需要遵守红黑树原则。这里面又分为好几种情况

旋转具体详细过程,参考我的往期博客

http://t.csdnimg.cn/a13umicon-default.png?t=N7T8http://t.csdnimg.cn/a13um

1)左旋(之所以每个节点下面都可能有节点是因为,新插入的节点不可能碰到这种情况,只可能是情况1向上递归解决的时候出现的)

void RotateL(Node* pParent){Node* pSubR = pParent->_pRight;Node* pSubRL = pSubR->_pLeft;pParent->_pRight = pSubRL; //防止访问空结点if (pSubRL)pSubRL->_pParent = pParent;pSubR->_pLeft = pParent;Node* pPParent = pParent->_pParent;pSubR->_pParent = pPParent;pParent->_pParent = pSubR;if (pPParent == _pHead)     //根节点单独处理_pHead->_pParent = pSubR;else{if (pParent == pPParent->_pLeft)pPParent->_pLeft = pSubR;elsepPParent->_pRight = pSubR;}}

2)右旋

 

void RotateR(Node* pParent){Node* pSubL = pParent->_pLeft;Node* pSubLR = pSubL->_pRight;pParent->_pLeft = pSubLR;if (pSubLR)       //防止访问空结点pSubLR->_pParent = pParent;pSubL->_pRight = pParent;Node* pPParent = pParent->_pParent;pParent->_pParent = pSubL;pSubL->_pParent = pPParent;if (pPParent == _pHead)      //根节点单独处理_pHead->_pParent = pSubL;else{if (pParent == pPParent->_pLeft)pPParent->_pLeft = pSubL;elsepPParent->_pRight = pSubL;}}

3)右旋加左旋

4)左旋加右旋

 双旋代码复用单旋就行了

插入代码:

bool _Insert(const T& data) {if (_Find(data)) {cout << "元素已经存在" << endl;return false;}//插入第一个元素的时候if (_pHead->_pParent == _pHead) {Node* root = new Node(data);root->c = black;root->_pParent = _pHead;_pHead->_pParent = root;_pHead->_pLeft = root;_pHead->_pLeft = root;return 1;}Node* cur = _pHead->_pParent;Node* parent=cur;//找该插入的位置while (cur&&cur!=_pHead) {parent = cur;if (cur->_data > data) {cur = cur->_pLeft;}else if (cur->_data < data) {cur = cur->_pRight;}else {cout << "值:" << data << "已经存在" << endl;return 0;}}//插入cur = new Node(data);if (parent->_data > data) {parent->_pLeft = cur;cur->_pParent = parent;}else {parent->_pRight = cur;cur->_pParent = parent;}//调整Node* gparent = parent->_pParent;Node* uncle = _pHead;while (gparent&&parent->c != black) {if (gparent->_pLeft == parent) {uncle = gparent->_pRight;}else {uncle = gparent->_pLeft;}if (!uncle || uncle->c == black)break;else {uncle->c = black;gparent->c = red;parent->c = black;}cur = gparent;;parent = cur->_pParent;gparent = parent->_pParent;}if (cur == parent->_pLeft && parent == gparent->_pLeft && (uncle == NULL || uncle->c == black)) {RRotate(gparent); //左旋情况parent->c = black;gparent->c = red;}if (cur == parent->_pRight && parent == gparent->_pRight && (uncle == NULL || uncle->c == black)) {LRotate(gparent);  //右旋情况parent->c = black;gparent->c = red;}if (cur == parent->_pLeft && parent == gparent->_pRight && (uncle == NULL || uncle->c == black)) {RRotate(parent); //右左双旋LRotate(gparent);cur->c = black;gparent->c = red;}if (cur == parent->_pRight&& parent == gparent->_pLeft && (uncle == NULL || uncle->c == black)) {LRotate(parent);  //左右双旋RRotate(gparent);cur->c = black;gparent->c = red;}_pHead->_pLeft = LeftMost();_pHead->_pRight = RightMost();RightMost()->_pRight = _pHead;_pHead->_pParent->c = black;_size++;return 1;}
4)迭代器

迭代器属于老生常谈了,就是运算符重载,我们这里不做过多讲解,但是我们这里面有两个难点,就是++,--拿的是哪个结点?

首先看4的下一个下一个结点是什么(也就是++)?如果右子树不为空的话,下一个结点是右子树的最左结点。

那7的下一个结点是什么呢?当右子树为空时,一直递归向上直到这个这颗子树是某个结点的左孩子,这个结点就是下一个结点。

struct RBTreeIterator
{typedef RBTreeNode<T> Node;typedef typename RBTreeIterator<T> Self;
public:
Self& operator++() {if (_pNode->_pRight != NULL) //右子树不为空的情况下{_pNode = _pNode->_pRight;if (_pNode->_pParent->_pParent == _pNode) {RBTreeIterator<T> ret(_pNode);return ret;}while (_pNode->_pLeft != NULL)_pNode = _pNode->_pLeft;RBTreeIterator<T> ret(_pNode);return ret;}while (_pNode != _pNode->_pLeft) {       //一直递归向上直到这个这颗子树是某个结点的左孩子if (_pNode->_pParent->_pParent == _pNode) {RBTreeIterator<T> ret(NULL);return ret;}_pNode = _pNode->_pParent;}RBTreeIterator<T> ret(_pNode->_pParent);return ret;}
};

那--呢?也就是上一个结点。例如4,当左孩子不为空时,左子树的最右结点就是你的上一个结点。

如果最子树为空呢?例如5,那就一直向上递归,直到这颗子树是某个结点的右孩子,这个结点就是上一个结点。

struct RBTreeIterator
{typedef RBTreeNode<T> Node;typedef typename RBTreeIterator<T> Self;
public:	
Self& operator--() {if (_pNode->_pLeft != NULL) { //左子树为空的情况_pNode = _pNode->_pLeft;while (_pNode->_pRight) {_pNode = _pNode->_pRight;}Self a(_pNode);return a;}else {   //一直向上递归,直到这颗子树是某个结点的右孩子while (_pNode->_pParent->_pRight != _pNode) {if (_pNode->_pParent->_pParent == _pNode) {RBTreeIterator<T> ret(NULL);return ret;}_pNode = _pNode->_pParent;}Self a(_pNode->_pParent);return a;}}
};

其他的运算符重载没啥难度,大家完全可以靠自己敲出来。

这篇博客花了作者大量心思,希望大家你点赞+收藏+转发。如果博客有不对的地方,可以评论区讨论。

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

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

相关文章

国产数据库中统计信息自动更新机制

数据库中统计信息描述的数据库中表和索引的大小数以及数据分布状况&#xff0c;统计信息的准确性对优化器选择执行计划时具有重要的参考意义。本文简要整理了下传统数据库和国产数据库中统计信息的自动更新机制&#xff0c;以加深了解。 1、数据库统计信息介绍 优化器是数据库…

纯小白蓝桥杯备赛笔记--DAY5(竞赛常用库函数)

文章目录 大小写转换islower和isupper&#xff1a;检查一个字符是否是小写或者大写。Tolower和toupper函数&#xff1a;ASCII码&#xff1a; 二分查找二分查找的前提&#xff1a;库函数只能对数组进行二分查找&#xff0c;且数组中的元素是单调的。binary_search函数&#xff1…

【C++第五课-C/C++内存管理】C/C++的内存分布、new/delete、new和delete的实现原理

目录 C/C的内存分布new/deletenew内置类型使用new自定义类型使用newnew失败 delete内置类型使用delete自定义类型使用delete new和delete的实现原理new[] 和delete[]的补充知识 定位new&#xff08;了解&#xff09;常见面试题 C/C的内存分布 频繁的new/delete堆容易产生内存碎…

【数据处理包Pandas】DataFrame数据的基本操作

目录 一、DataFrame数据的查询&#xff08;一&#xff09;查询单行数据&#xff08;二&#xff09;查询多行数据&#xff08;三&#xff09;查询列数据&#xff08;四&#xff09;查询指定的行列数据 二、DataFrame数据的编辑&#xff08;一&#xff09;增加数据&#xff08;二…

拦截器未生效的问题

记录一下自己出现的一个问题 配置好拦截器后 protected void addInterceptors(InterceptorRegistry registry) {log.info("开始注册自定义拦截器...");registry.addInterceptor(jwtTokenUserInterceptor).addPathPatterns("/**").excludePathPatterns(&q…

【Java 多线程】从源码出发,剖析Threadlocal的数据结构

文章目录 exampleset(T value)createMap(t, value);set(ThreadLocal<?> key, Object value)ThreadLocalMap和Thread的关系 全貌 ThreadLocal是个很重要的多线程类&#xff0c;里面数据结构的设计很有意思&#xff0c;很巧妙。但是我们平时使用它的时候常常容易对它的使用…

怎么开发高可靠、能处理多种复杂问题、处理各种异常情况的爬虫程序

开发高可靠、能处理多种复杂问题、处理各种异常情况的爬虫程序是一个综合性的任务&#xff0c;涉及多个方面的技术和策略。以下是一些关键步骤和最佳实践&#xff0c;可以帮助你实现这样的爬虫程序&#xff1a; 明确需求和目标&#xff1a; 在开始编写代码之前&#xff0c;明确…

图片转换成base64如何在html文件中使用呢

在HTML文件中使用Base64编码的图片非常简单。Base64编码是一种将二进制数据转换为ASCII字符串的方法,这使得可以直接在网页上嵌入图片数据,而无需引用外部图片文件。以下是如何在HTML中使用Base64编码的图片的步骤: 步骤 1: 将图片转换为Base64编码 首先,你需要将图片文件…

Photoshop笔记大全

文章目录 PS常用快捷键Shift+工具代码PS名词解释PS影子制作PS图像优化PS常用快捷键 Ctrl+R:调出标尺 Ctrl+T:调整变形。拖动时,按住ctrl键,任意变形。按住Shift键,等比例变形。按Esc键,恢复。 Alt+delete:前景色填充 Ctrl+delete:背景色填充 ctrl+x:剪切 ctrl+v…

WPF+Prism 模块化编程(一)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 WPFPrism 模块化编程&#xff08;一&#xff09; 一、Prism项目创建安装二、将项目升级为Prism项目三、将Prism项目升级为支持模块化编程项目 一、Prism项目创建安装 1、新建…

四道题搞懂二叉树深度

二叉树的最大深度&#xff08;LeetCode104&#xff09; 先表示左树的深度&#xff0c;再表示右树的深度。再进行条件判断 class solution {public int maxDepth(TreeNode root) {if (root null) {return 0;}int leftDepth maxDepth(root.left);int rightDepth maxDepth(ro…

zookeeper常见命令详解2

1、version 展示客户端版本 [zkshell: 1] version ZooKeeper CLI version: 3.6.0-SNAPSHOT-29f9b2c1c0e832081f94d59a6b88709c5f1bb3ca, built on 05/30/2019 09:26 GMT2、whoami 展示出所有已经添加&#xff08;addauth&#xff09;用户的信息 [zkshell: 1] whoami Auth sch…

JAVA面试大全之集合IO篇

目录 1、集合 1.1、Collection 1.1.1、集合有哪些类? 1.1.2、ArrayList的底层?

算法系列--动态规划--特殊的状态表示--分析重复子问题

&#x1f495;"轻舟已过万重山!"&#x1f495; 作者&#xff1a;Lvzi 文章主要内容&#xff1a;算法系列–算法系列–动态规划–特殊的状态表示–分析重复子问题 大家好,今天为大家带来的是算法系列--动态规划--特殊的状态表示--分析重复子问题 一.组合总数IV 链接…

蓝桥集训之游戏

蓝桥集训之游戏 核心思想&#xff1a;博弈论 区间dp 设玩家1的最优解为A 玩家2的最优解为B 1的目标就是使A-B最大 2的目标就是使B-A最大 当玩家1取L左端点时 右边子区间结果就是玩家2的最优解B-A 即当前结果为w[L] – (B-A) 当玩家1取R右端点时 左边子区间结果就是玩家2的最…

Mybatis-特殊SQL的执行

1. 模糊查询 在MyBatis中进行模糊查询时&#xff0c;有以下三种常见的实现方式&#xff1a; 1.1. 错误示范 先来个准备操作&#xff0c;并做一个错误示例 根据姓名&#xff0c;模糊查询用户&#xff0c;(x小x) 更新数据表 SQLMapper.java package com.sakurapaid.mybatis3…

力扣top100-两数之和

思路&#xff1a; 用 hashMap 存储遍历过的元素和对应的索引。 每遍历一个元素&#xff0c;看看 hashMap 中是否存在满足要求的目标数字。 所有事情在一次遍历中完成&#xff08;用了空间换取时间&#xff09;。 // twoSum 函数接受一个整数数组 nums 和一个目标值 target&am…

Win10 搭建FTP存储服务器站点【超详细教程】

目录 第一步&#xff1a;打开控制面板>程序 第二步&#xff1a;win10左下角搜索IIS并打开 第三步&#xff1a;右键网站&#xff0c;选择添加FTP站点 第四步&#xff1a;添加FTP站点名称 第五步&#xff1a;添加IP地址和端口 第六步&#xff1a;身份验证与授权信息 第…

Next.js 客户端组件跨页面跳转时锚点不生效,scrollIntoView 不起作用

项目场景 点击页脚“联系我们”链接&#xff0c;跳转到对应页面的“联系我们”模块。 问题描述 在页脚处“联系我们”代码如下&#xff1a; <a href"/about_us#contactUs">联系我们</a>在“联系我们”的组件中写以下代码&#xff1a; export default…

浅谈Spring体系的理解

浅谈Spring知识体系 Spring Framework架构图Spring家族技术生态全景图XMind汇总 本文不涉及细节&#xff0c;主要回答两个问题&#xff1a; Spring家族技术生态全景图有哪些Spring Framework架构下每个模块有哪些东西&#xff0c;以及部分模块之间的关联关系 Spring Framework架…