【数据结构进阶】手撕红黑树

在这里插入图片描述

🔥个人主页: Forcible Bug Maker
🔥专栏: C++ || 数据结构

目录

  • 🌈前言
  • 🔥红黑树的概念
  • 🔥手撕红黑树
    • 红黑树结点的定义
    • 红黑树主体需要实现的成员函数
      • ==红黑树的插入==
      • ==find==
      • ==Empty和Size==
      • ==拷贝构造==
      • ==析构函数和clear==
      • ==检测是否为合法红黑树==
      • ==Begin和End==
    • 红黑树的迭代器接口
      • ==* 解引用和 -> 访问==
      • ==operator++()==
      • ==operator- - ()==
      • ==迭代器比较相等==
  • 🌈结语

🌈前言

本篇博客主要内容:红黑树的介绍,以及底层代码逻辑和实现

刚刚接触编程的时候就听说有的大厂HR会让手撕红黑树。心中一直对这个数据结构保持着敬畏和向往,今天在将其撕出来后,用本篇博客加以记录,望周知。

🔥红黑树的概念

红黑树,也是一种二叉搜索树,但再每个结点上增加一个存储位置表示结点的颜色,可以是RED(红)或BLACK(黑)。通过对任何一条根到叶子的路径上各个结点的着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。
在这里插入图片描述
一颗红黑树,是具有如下性质的二叉搜索树:

  1. 每个结点不是红色就是黑色
  2. 根结点是黑色的
  3. 如果一个结点是红色,则它的两个孩子结点是黑色(即不会有连续的红结点)
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点NIL

其中,3和4是最重要的两点。

🔥手撕红黑树

红黑树结点的定义

红黑树的结点包含四个成员变量,模板类型T:可以存储K或者pair<K,V>类型,便于后期封装;三个指针:分别为指向左孩子结点的指针,指向右孩子结点的指针,指向父结点的指针;最后变量_col:枚举类型,可以存REDBLACK

enum Color
{RED,BLACK
};template<class T>
struct RBTreeNode
{RBTreeNode<T>(const T& t): _data(t), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED){}T _data;RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;Color _col;
};

红黑树主体需要实现的成员函数

// T: 可能是键值对<key,value>
//    可能是一个key
// 不论节点中存储的是<key, value> || key, 都是按照key来进行比较的
// KeyOfValue: 提取data中的Key
template<class K, class T, class KeyOfValue>
class RBTree
{typedef RBTreeNode<T> Node;
public:typedef RBTreeIterator<T, T&, T*> iterator;typedef RBTreeIterator<T, const T&, const T*> const_iterator;
public:RBTree() = default;RBTree(const RBTree<K, T, KeyOfValue>& t);// 插入值为data的节点// 返回值含义:iterator代表新插入节点   bool:代表释放插入成功std::pair<iterator, bool> Insert(const T& data);// Begin和End迭代器iterator Begin();iterator End();// 红黑树是否为空,是返回true,否则返回falsebool Empty()const;// 返回红黑树中有效节点的个数size_t Size()const;// 将红黑树中的有效节点删除void Clear();// 检测红黑树是否为有效的红黑树,注意:其内部主要依靠_IsValidRBTRee函数检测bool IsValidRBTRee()// 在红黑树中查找data,存在赶回该节点对应的迭代器,否则返回End()iterator Find(const T& data);~RBTree();
private:Node* _root;
};

红黑树的插入

红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:

  1. 首先是按照二叉搜索树的方式插入结点
  2. 检测插入结点后,红黑树的性质是否遭到破坏
    因为新结点的默认颜色是红色,因此:如果父结点的颜色是黑色,就没有违反性质,不需调整;担当插入结点的父节点也是红色时,就违反了性质3(不能有连在一起的红结点),此时需要对红黑树分情况讨论调整:

约定:cur为当前结点,p为父节点,g为祖父结点,u为叔叔结点

  • 情况一:cur为红,p为红,g为黑,u存在且为红

在这里插入图片描述
解决方式:将p,u变成黑色,g变成红色,然后把g改成cur,继续向上调整

  • 情况二:cur为红,p为红,g为黑,u不存在/u存在且为黑(这里需要做的其实就和AVL树中的单旋很像了,我们需要把u结点旋转下来,以维持平衡)

在这里插入图片描述

p为g的左孩子,cur为p的左孩子,进行右单旋;相反,p为g的右孩子,cur为p的右孩子,则进行左单旋。p变黑色,g变红色

  • 情况三(情况二的变体):cur为红,p为红,g为黑,u不存在/u存在且为黑(其实就是双旋,除了不用调整平衡因子,其他的和AVL树的双旋并无差别)

在这里插入图片描述

p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反,p为g的右孩子,cur为p的左孩子,则针对p做右单旋转。然后就转换成了情况2

针对每种情况进行相应的处理即可:

// 插入值为data的节点
// 返回值含义:iterator代表新插入节点   bool:代表释放插入成功
std::pair<iterator, bool> Insert(const T& data)
{if (_root == nullptr) {_root = new Node(data);_root->_col = BLACK;return std::make_pair(iterator(_root, _root), true);}KeyOfValue kov;Node* parent = nullptr;Node* cur = _root;while (cur) {if (kov(cur->_data) < kov(data)) {parent = cur;cur = cur->_right;}else if (kov(cur->_data) > kov(data)) {parent = cur;cur = cur->_left;}else return std::make_pair(iterator(cur, _root), false);}cur = new Node(data);Node* InsertNode = cur;if (kov(parent->_data) < kov(data)) {parent->_right = cur;cur->_parent = parent;}else {parent->_left = cur;cur->_parent = parent;}while (parent && parent->_col == RED && cur->_col == RED) {Node* grandParent = parent->_parent;Node* uncle = nullptr;//   g// p   uif (grandParent->_left == parent) {uncle = grandParent->_right;if (uncle == nullptr || uncle->_col == BLACK) {if (parent->_left == cur) {RotateR(grandParent);parent->_col = BLACK;grandParent->_col = RED;}else {RotateL(parent);RotateR(grandParent);cur->_col = BLACK;grandParent->_col = RED;}break;}else {parent->_col = BLACK;grandParent->_col = RED;uncle->_col = BLACK;cur = grandParent;parent = grandParent->_parent;}}//   g// u   pelse {uncle = grandParent->_left;if (uncle == nullptr || uncle->_col == BLACK) {if (parent->_right == cur) {RotateL(grandParent);parent->_col = BLACK;grandParent->_col = RED;}else {RotateR(parent);RotateL(grandParent);cur->_col = BLACK;grandParent->_col = RED;}break;}else {parent->_col = BLACK;grandParent->_col = RED;uncle->_col = BLACK;cur = grandParent;parent = grandParent->_parent;}}}_root->_col = BLACK;return std::make_pair(iterator(InsertNode, _root), true);
}

在红黑树中,由于不用调节平衡因子,双旋的复杂度大大降低,直接使用单旋并在插入过程中调整结点颜色即可。
旋转的具体内容在AVL树中(【数据结构进阶】AVL树)讲解过,这里就不赘述了。

// 左单旋
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;Node* parentParent = parent->_parent;if (subRL)subRL->_parent = parent;subR->_left = parent;subR->_parent = parentParent;parent->_right = subRL;parent->_parent = subR;if (parentParent == nullptr) {_root = subR;}else {if (parentParent->_left == parent) {parentParent->_left = subR;}elseparentParent->_right = subR;}
}// 右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;Node* parentParent = parent->_parent;if (subLR)subLR->_parent = parent;subL->_right = parent;subL->_parent = parentParent;parent->_left = subLR;parent->_parent = subL;if (parentParent == nullptr) {_root = subL;}else {if (parentParent->_left == parent) {parentParent->_left = subL;}elseparentParent->_right = subL;}
}

find

和二叉搜索树的查找规则相同。

// 在红黑树中查找data,存在赶回该节点对应的迭代器,否则返回End()
iterator Find(const T& data)
{KeyOfValue kov;Node* cur = _root;while (cur) {if (kov(cur->_data) < kov(data))cur = cur->_right;else if (kov(cur->_data) > kov(data))cur = cur->_left;else return iterator(cur, _root);}return iterator(nullptr, _root);
}

Empty和Size

Empty接口函数用来判断树是否为空;Size用来计算返回树结点的个数。

// 红黑树是否为空,是返回true,否则返回false
bool Empty()const
{return _root == nullptr;
}
// 返回红黑树中有效节点的个数
size_t Size()const
{return _Size(_root);
}size_t _Size(Node* root)
{return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
}

拷贝构造

RBTree(const RBTree<K, T, KeyOfValue>& t)
{_root = _Copy(t._root);
}Node* _Copy(Node* root)
{if (root == nullptr)return nullptr;Node* newRoot = new Node(root->_data);newRoot->_left = _Copy(root->_left);newRoot->_right = _Copy(root->_right);return newRoot;
}

析构函数和clear

// 将红黑树中的有效节点删除
void Clear()
{_Destroy(_root);_root = nullptr;
}~RBTree()
{_Destroy(_root);_root = nullptr;
}void _Destroy(Node* root)
{if (root == nullptr)return;_Destroy(root->_left);_Destroy(root->_right);delete root;
}

检测是否为合法红黑树

IsValidRBTRee中,首先算出一条路径上的黑结点个数digit_black,然后在每条路径递归到空结点时判断黑结点个数是否相等,即可验证性质4(所有路径上黑结点个数相等);递归的过程中,判断当前结点和父节点的颜色是否同时为红,即可验证性质3(没有连续的红结点)

// 检测红黑树是否为有效的红黑树,注意:其内部主要依靠_IsValidRBTRee函数检测
bool IsValidRBTRee()
{if (_root == nullptr)return true;Node* cur = _root;size_t digit_black = 0;while (cur) {if (cur->_col == BLACK)++digit_black;cur = cur->_left;}return _IsValidRBTRee(_root, digit_black, 0);
}bool _IsValidRBTRee(Node* pRoot, size_t blackCount, size_t pathBlack)
{if (pRoot == nullptr) {if (blackCount == pathBlack)return true;else return false;}if (pRoot->_col == RED && pRoot->_parent->_col == RED) {return false;}if (pRoot->_col == BLACK) {return _IsValidRBTRee(pRoot->_left, blackCount, pathBlack + 1)&& _IsValidRBTRee(pRoot->_right, blackCount, pathBlack + 1);}else {return _IsValidRBTRee(pRoot->_left, blackCount, pathBlack)&& _IsValidRBTRee(pRoot->_right, blackCount, pathBlack);}
}

Begin和End

这两个函数用于获取红黑树的头对象和尾迭代器。

// Begin和End迭代器
iterator Begin() 
{return iterator(_LeftMost(), _root);
}
iterator End()
{return iterator(nullptr, _root);
}// 获取红黑树最左侧节点
Node* _LeftMost()
{if (_root == nullptr)return nullptr;Node* parent = _root;Node* cur = parent->_left;while (cur) {parent = cur;cur = cur->_left;}return parent;
}

红黑树的迭代器接口

迭代器的好处是可以方便遍历,使数据结构的底层实现变的透明,从而降低代码编写的复杂程度。

template<class T, class Ref, class Ptr>
struct RBTreeIterator
{typedef RBTreeNode<T> Node;typedef RBTreeIterator<T, Ref, Ptr> Self;RBTreeIterator(Node* pNode,Node* root): _pNode(pNode),_root(root){}// 让迭代器具有类似指针的行为Ref operator*();Ptr operator->();// 然迭代器可以移动:前置/后置++  Self& operator++();Self operator++(int);// 然迭代器可以移动:前置/后置-- Self& operator--();Self operator--(int);// 让迭代器可以比较bool operator!=(const Self& s)const;bool operator==(const Self& s)const;private:Node* _pNode;Node* _root;
};

* 解引用和 -> 访问

// 让迭代器具有类似指针的行为
Ref operator*()
{return _pNode->_data;
}
Ptr operator->()
{return &(_pNode->_data);
}

operator++()

二叉树的中序遍历并不难实现,但是要实现从任意一个结点按中序遍历跑到下一个结点,这就有相当难度了。
具体逻辑为:

  1. 右子树不为空,访问右子树的最左结点。
  2. 右子树为空(代表当前结点所在子树访问完了),沿着到根节点的路线,孩子是父亲左的那个祖先结点,就是下一个要访问的结点。
// 迭代器可以移动:前置/后置++  
Self& operator++()
{if (_pNode->_right) {_pNode = _pNode->_right;while (_pNode->_left) _pNode = _pNode->_left;}else {Node* cur = _pNode;Node* parent = cur->_parent;while (parent && cur == parent->_right) {cur = parent;parent = parent->_parent;}_pNode = parent;}return *this;
}Self operator++(int)
{Node* rem = _pNode;if (_pNode->_right) {_pNode = _pNode->_right;while (_pNode->_left)_pNode = _pNode->_left;}else {Node* cur = _pNode;Node* parent = cur->_parent;while (parent && cur == parent->_right) {cur = parent;parent = parent->_parent;}_pNode = parent;}return Self(rem);
}

operator- - ()

这时候要判断当前迭代器是否指向尾End(),同时判断树是否为空,这就要用到传入迭代器对象中的_root了。在找到End()的前一个结点之后,按照和operator++()相反的逻辑即可实现operator--()
具体逻辑为:

  1. 左子树不为空,访问左子树的最右结点。
  2. 左子树为空(代表当前结点所在子树访问完了),沿着到根结点的路线,孩子是父亲右的那个祖先结点,就是下一个要访问的结点。
// 迭代器可以移动:前置/后置-- 
Self& operator--()
{if (_pNode == nullptr) {if (_root == nullptr)assert(false);Node* parent = _root;Node* cur = parent->_right;while (cur) {parent = cur;cur = cur->_right;}_pNode = parent;return *this;}if (_pNode->_left) {_pNode = _pNode->_left;while (_pNode->_right)_pNode = _pNode->_right;}else {Node* cur = _pNode;Node* parent = cur->_parent;while (parent && cur == parent->_left) {cur = parent;parent->_parent;}_pNode = parent;}return *this;
}Self operator--(int)
{if (_pNode == nullptr) {if (_root == nullptr)assert(false);Node* parent = _root;Node* cur = parent->_right;while (cur) {parent = cur;cur = cur->_right;}_pNode = parent;return Self(nullptr);}Node* rem = _pNode;if (_pNode->_left) {_pNode = _pNode->_left;while (_pNode->_right)_pNode = _pNode->_right;}else {Node* cur = _pNode;Node* parent = cur->_parent;while (parent && cur == parent->_left) {cur = parent;parent->_parent;}_pNode = parent;}return Self(rem);
}

迭代器比较相等

底层就是用指针作比较。

// 让迭代器可以比较
bool operator!=(const Self& s)const
{return _pNode != s._pNode;
}bool operator==(const Self& s)const
{return _pNode == s._pNode;
}

🌈结语

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
红黑树的底层实现到这里就要结束了,本篇的数据结构较为复杂,模板的使用也有很多容易出错的点,需要多加体会。感谢大家的支持♥

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

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

相关文章

Redis和Mysql如何保持数据一致性

一般情况下&#xff0c;Redis是用来实现应用和数据库之间读操作得缓存层&#xff0c;主要目的是减少数据库IO&#xff0c;还可以提升数据的IO性能。 当应用程序需要去读取某个数据时&#xff0c;会首先尝试去Redis里面加载&#xff0c;如果命中就直接返回&#xff0c;如果没有…

C++ 操作Git仓库

代码 #include "common.h" #include "args.c" #include "common.c"enum index_mode {INDEX_NONE,INDEX_ADD };struct index_options {int dry_run;int verbose;git_repository* repo;enum index_mode mode;int add_update; };/* Forward declar…

vue项目Nginx部署启动

1.vue打包 &#xff08;1&#xff09;package.json增加打包命令 "scripts": {"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js --host 10.16.14.110","start": "npm run dev","un…

Halcon 边缘提取(亚像素)

Halcon提供多种边缘提取算法。像素提取方法有常用的边缘提取算子或深度学习分割模型等。考虑到精度问题可能需要提取亚像素边缘。当然也可以提取轮廓&#xff1a;线、圆、椭圆等。本文只讨论提取轮廓。 1 基本概念 正常情况下&#xff0c;无需特殊操作即可提取边缘轮廓。 1…

Linux-4:Shell编程——基础语法(50%-100%)

目录 前言 一、数组 1.数组定义 2.关联数组 3.数组长度 二、运算符 1.算术运算符 2.关系运算符 3.布尔运算符 4.逻辑运算符 5.字符串运算符 6.文件测试运算符 三、read命令 1.接收用户输入 2.开启转义 3. -p 输入提示 4. -s 静默模式 -t 设置超时时间 5.读取…

Fiddler学习笔记

目录 前言 简介 原理 界面 前言 测试可以使用fiddler工具&#xff0c;通过抓包的方式修改前端参数和模拟后端返回&#xff0c;快速定位缺陷。 简介 Fiddler是HTTP协议调试代理工具&#xff0c;可以记录并检查所有客户端和服务器之间的HTTP和HTTPS请求&#xff0c;允许监视…

算法训练1

01背包问题 背包状态方程----动态规划 二维dp 使用 f[i][j] max(f[i-1][j] ,f[i-1][j - w[i]] v[i]); 伪代码&#xff1a; int dp[100][100]; void test6() {int n; //装备数量int m; //背包容量int v[105], w[105]; //前面空间&#xff0c;后面价值for (int i 1; i &l…

ONLYOFFICE文档:为企业和开发者带来强大的文档编辑功能

本文给大家介绍一个开源项目&#xff1a;ONLYOFFICE文档&#xff0c;它能够为文档编辑、多人协作提供强大支持。无论你是个人使用&#xff0c;还是企业、商业开发&#xff0c;都能找到适合你的版本。 关于 ONLYOFFICE 文档 ONLYOFFICE 文档是一套功能强大的文档编辑器&#x…

微信小程序获取AppSecret的步骤

文章目录 微信小程序获取AppSecret的步骤&#xff1a;注意&#xff1a; 微信公众平台 小程序的密钥&#xff08;或称为AppSecret&#xff09;是用于加密解密、验证服务器身份等安全操作的敏感信息。不同的平台&#xff08;如微信小程序、支付宝小程序、百度智能小程序等&am…

vulhub:Apache解析漏洞apache_parsing

在Apache1.x/2.x中Apache 解析文件的规则是从右到左开始判断解析&#xff0c;如果后缀名为不可识别文件解析&#xff0c;就再往左判断。如 1.php.xxxxx 漏洞原理 Apache HTTPD 支持一个文件拥有多个后缀&#xff0c;并为不同后缀执行不同的指令。比如如下配置文件 AddType te…

【C#】.net core 6.0 webapi 使用core版本的NPOI的Excel读取数据以及保存数据

欢迎来到《小5讲堂》 这是《C#》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 背景读取并保存NPOI信息NPOI 插件介绍基本功能示例代码写入 Excel 文件…

算法小白的进阶之路(力扣1~5)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 非常期待和您一起在这个小…

花几千上万学习Java,真没必要!(三十九)

1、BufferedReader的使用&#xff1a; 测试代码&#xff1a; package test.com; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class FileReadToList { pu…

使用 openai 和 langchain 调用自定义工具完成提问需求

我们提供了一个函数&#xff0c;接受传入运算的字符串&#xff0c;返回运算的结果。 现在的需求是&#xff0c;我们问 gpt 模型&#xff0c;由于模型计算能力并不好&#xff0c;他要调用计算函数&#xff0c;根据计算结果&#xff0c;回答我们的问题。 使用 openai 实现&#…

发布NPM包详细流程

制作 首先需要制作一个npm包。 按照以下步骤依次执行。 mkdir my-npm-package cd my-npm-package npm init 相信这一步不需要过多的解释&#xff0c;就是创建了一个文件夹&#xff0c;然后初始化了一下文件夹。 然后在生成的package.json文件夹中更改一下自己的配置&…

优化冗余代码:提升前端项目开发效率的实用方法

目录 前言代码复用与组件化模块化开发与代码分割工具辅助与自动化结束语 前言 在前端开发中&#xff0c;我们常常会遇到代码冗余的问题&#xff0c;这不仅增加了代码量&#xff0c;还影响了项目的可维护性和开发效率。还有就是有时候会接到紧急业务需求&#xff0c;要求立马完…

这两个大龄程序员,打算搞垮一个世界软件巨头!

大家都知道&#xff0c;Adobe是多媒体和数字内容创作者的绝对王者&#xff0c;它的旗下有众多大家耳熟能详的软件&#xff1a;Photoshop、Illustrator、Premiere Pro、After Effects、InDegign、Acrobat、Animate等等。 这些软件使用门槛很高&#xff0c;价格昂贵&#xff0c;安…

遗传算法与深度学习实战——生命模拟及其应用

遗传算法与深度学习实战——生命模拟及其应用 0. 前言1. 康威生命游戏1.1 康威生命游戏的规则1.2 实现康威生命游戏1.3 空间生命和智能体模拟 2. 实现生命模拟3. 生命模拟应用小结系列链接 0. 前言 生命模拟是进化计算的一个特定子集&#xff0c;模拟了自然界中所观察到的自然…

大模型之多模态大模型技术

本文作为大模型综述第三篇,介绍语言大模型多模态技术。 不同于语言大模型只对文本进行处理,多模态大模型将文本、语音、图像、视频等多模态数据联合起来进行学习。多模态大模型融合了多种感知途径与表达形态, 能够同时处理和理解来自不同感知通道(例如视觉、听觉、语言和触…

麒麟系统查看和修改ip

查看ip ifconfig ifconfig enp0s3 192.168.1.110