【C++进阶】红黑树的复仇(红与黑的爱恨厮杀)

2024-04-04 17:20

本文主要是介绍【C++进阶】红黑树的复仇(红与黑的爱恨厮杀),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

🪐🪐🪐欢迎来到程序员餐厅💫💫💫

          主厨:邪王真眼

主厨的主页:Chef‘s blog  

 所属专栏:c++大冒险
 

 总有光环在陨落,总有新星在闪烁


引言:

之前我们学习了 AVL树,不得不惊叹于他那近乎绝对的平衡,然而也惋惜于插入删除效率的低下,今天要讲的红黑树则是以相对的平衡换来了插入删除效率的大幅提高,可谓是各有千秋

ps:建议先看过AVL树后再来学习红黑树:

带你手撕AVL树 


一. 红黑树的概念

         红黑树是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

二 红黑树的性质

  • 1. 每个结点不是红色就是黑色
  • 2. 根节点是黑色的 
  • 3. 如果一个节点是红色的,则它的两个孩子结点是黑色的 
  • 4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点 
  • 5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
思考:
为什么满足以上性质,红黑树就能保证:最长路径中节点个数不会超过最短路径节点 个数的2倍?

推导:

  • 性质1不必多说
  • 性质2与后面的旋转有关
  • 性质3表明不能有连续的红色结点
  • 性质4表明理论最短路径就是纯黑节点路径

综上:

            我们可以认为事先建造好一颗纯黑节点的满二叉树,再在两个黑节点之间插入红节点,则理论最长路径就是一黑一红交替,不超过最短路径的二倍。

 三.红黑树的节点讲解及模拟

enum Color
{RED,BLACK
};
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(pair<K, V>& kv = pair<K, V>()):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _col(RED)
};

代码讲解:

  • 1.我们枚举出了Color
  • 2.除左右指针外,还有父亲指针,使得可以向上回溯
  • 3.用pair对象存储K值V值
  • 4.增加了颜色的成员变量,且默认颜色为红色

提问:

           为什么结点的颜色初始化为红色呢?

回答:

           因为插入新节点时(不为根部),如果插入黑色,一定破坏性质4,导致每条路径黑结点数目不同;而如果插入红色,有可能不会破坏性质3,所以结点初始化为红色。

四.红黑树模拟

4.1 成员变量

template<class K, class V>
class RBTree
{
protected:typedef RBTreeNode<K, V> Node;
public://函数
protected :Node* _root;
};

4.2插入

与搜索二叉树以及AVL树相比,红黑树的默认成员函数和遍历相差不大,所以这里重点讲插入

4.2.1插入过程:

  1. 以普通二叉搜索树的方式进行插入
  2. 根据插入后的不同情况进行调整 
	bool Insert(pair<K, V>& kv){if (_root == nullptr){_root = new Node(val);return true;}else{Node* cur = _root;Node* parent = nullptrwhile (cur){parent = cur;if (cur->_val > val)cur = cur->left;else if (cur->_val < val)cur = cur->_right;elsereturn false;}cur = new Node(val);if (parent->_val.first > cur->_val.first){parent->_left = cur;}else{parent->_parent = cur;}cur->_parent = parent;///从此处开始进行插入后的调整while (parent && parent->_col == RED){Node* grandparent = parent->_parent;Node* uncle = nullptr;if (grandparent->_left == parent)uncle = grandparent->_right;elseuncle = grandparent->_left;if (uncle && uncle->_col == RED){parent->_col = BLACK;uncle->_col = BLACK;grandparent->_col = RED;cur = grandparent;parent = cur->_parent;}else if (grandparent->_left == parent){if (parent->_left = cur){RotateR(grandparent);grandparent->_col = RED;parent->_col = BLACK;}else{RotateL(parent);RotateR(grandparent);grandparent->_col = RED;cur->_col = BLACK;}}else{if (parent->_right = cur){RotateL(grandparent);grandparent->_col = RED;parent->_col = BLACK;}else{RotateR(parent);RotateL(grandparent);grandparent->_col = RED;cur->_col = BLACK;}}void RotateL(AVLNode * parent)//左旋{Node* grandparent = parent->_parent;Node* ChildR = parent->_right;if (grandparent){if (grandparent->_left == parent)grandparent->_left = ChildR;elsegrandparent->_right = ChildR;}else_root = ChildR;ChildR->_parent = grandparent;parent->_right = ChildR->_left;ChildR->_left->_parent = parent;ChildR->_left = parent;parent->_parent = ChildR;ChildR->_bf = parent->_bf = 0;}void RotateR(AVLNode * parent)//右旋{Node* grandparent = parent->_parent;Node* ChildL = parent->_left;if (grandparent){if (grandparent->_left == parent)grandparent->_left = ChildL;elsegrandparent->_right = ChildL;}else_root = ChildL;ChildL->_parent = grandparent;//两两一组进行改变parent->_left = ChildL->_right;ChildL->_right->_parent = parent;ChildL->_right = parent;parent->_parent = ChildL;//ChildL->_bf = parent->_bf = 0;}void RotateRL(AVLNode * parent)//双旋,先右旋在左旋{Node* ChildR = parent->_right;int bf = ChildR->_left->_bf;RotateR(ChildR);RotateL(parent);if (bf == 0){parent->_bf = 0;ChildR->_bf = 0;ChildR->_left->_bf = 0;}else if (bf == 1){parent->_bf = -1;ChildR->_bf = 0;ChildR->_left->_bf = 0;}else if (bf == -1){parent->_bf = 0;ChildR->_left->_bf = 0;ChildR->_bf = 1;}else{assert(false);}}void RotateLR(AVLNode * parent)//双旋,先左旋,再右旋{Node* ChildL = parent->_left;int bf = ChildL->_right->_bf;RotateR(ChildL);RotateL(parent);if (bf == 0){parent->_bf = 0;ChildL->_bf = 0;ChildL->_right->_bf = 0;}else if (bf == 1){parent->_bf = 0;ChildL->_bf = -1;ChildL->_right->_bf = 0;}else if (bf == -1){parent->_bf = 1;ChildL->_right->_bf = 0;ChildL->_bf = 0;}else{assert(false);}}void Inorde(AVLNode * root, vector<pair<K, V>>&v){if (root == nullptr)return;Inorde(root->_left, v);v.push_back(root->_val);Inorde(root->_right, v);}}}}

插入后调整的分析:

  • 1.像AVL树一样,大框架也是向上回溯,判断循环进行条件是父亲节点不为空且父亲节点颜色为红.因为新节点的默认颜色是红色,如果其双亲节点的颜色是黑色,没有违反红黑树任何 性质,则不需要调整;
  • 2当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

    🍒🍒4.2.2情况一:

cur为红,p为红,g为黑,u存在且为红 

解决方式:
              将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。


🍒🍒4.2.3情况二:

cur为红,p为红,g为黑,u不存在/u存在且为黑,p是g的左孩子,cur是p的左孩子

解决方案:

  1. 先对grandparent进行右单旋
  2. 再将parent变黑,grandparent变红


🍒🍒4.2.4情况三

cur为红,p为红,g为黑,u不存在/u存在且为黑,p是g的左孩子,cur是p的右孩子

  重点提醒:

               可以发现左单旋后就变成了情况二

解决方案:

  1. 先对parent进行左单旋
  2. 再对grandparent进行右单旋
  3. 最后将cur变黑,grandparent变红,这里将cur变黑而不是parent是因为左单旋后cur取代了parent的位置


🍒🍒4.2.5情况四:

cur为红,p为红,g为黑,u不存在/u存在且为黑,p是g的右孩子,cur是p的右孩子

解决方案:

  1. 先对grandparent进行左单旋
  2. 再将parent变黑,grandparent变红


🍒🍒4.2.6情况五:

cur为红,p为红,g为黑,u不存在/u存在且为黑,p是g的右孩子,cur是p的左孩子

重点提醒:

               可以发现右单旋后就变成了情况四

解决方案:

  1. 先对parent进行右单旋
  2. 再对grandparent进行左单旋
  3. 最后将cur变黑,grandparent变红,这里将cur变黑而不是parent是因为左单旋后cur取代了parent的位置

5.红黑树的验证

红黑树的检测分为两步:

1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)

void Inorde(AVLNode * root, vector<pair<K, V>>&v)
{if (root == nullptr)return;Inorde(root->_left, v);v.push_back(root->_val);Inorde(root->_right, v);
}

2. 检测其是否满足红黑树的性质

bool IsBalance(Node*root)
{//空树也是红黑树if (root == nullptr)return true;//违反性质2if (root->_col == RED){cout << "树的根节点应该是黑色,可该树却是红色" << endl;return false;}//计算一条路径黑节点数量Node* cur = root;int num = 0;while (cur){if (cur->_col == BLACK)num++;cur = cur->_left;}return _IsBlance(root, num);
}
IsBalance(Node* root, size_t num, size_t cur_num)
{if (root == nullptr){//违反性质4if (num != cur_num){cout << "对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点,但该树却不是" << endlreturn false;}elsereturn true;}if (root->_col == BLACK)num++;//违反性质3if (root->_parent && root->_parent == RED && root->_col == RED){cout << "如果一个节点是红色的,则它的两个孩子结点是黑色的,可该树却出现了连续的红色节点" << endl;return false;}return IsBalance(root->_left, num, cur_num) && IsBalance(root->_right, num, cur_num);
}

6. 红黑树与AVL树的比较

       红黑树和 AVL 树都是高效的平衡二叉树, 增删改查的时间复杂度都是O(log N) ,红黑树不追 求绝对平衡,其只需保证 最长路径不超过最短路径的2倍 降低了插入和旋转的次数 所以在经常进行增删的结构中性能比 AVL 树更优,而且红黑树实现比较简单,所以实际运用中红 黑树更多。

7. 红黑树的应用

  • 1. C++ STL库 -- map/set、mutil_map/mutil_set 
  • 2. Java 库
  • 3. linux内核
  • 4. 其他一些库

创作不易,点赞关注支持一下吧

这篇关于【C++进阶】红黑树的复仇(红与黑的爱恨厮杀)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/876390

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

[MySQL表的增删改查-进阶]

🌈个人主页:努力学编程’ ⛅个人推荐: c语言从初阶到进阶 JavaEE详解 数据结构 ⚡学好数据结构,刷题刻不容缓:点击一起刷题 🌙心灵鸡汤:总有人要赢,为什么不能是我呢 💻💻💻数据库约束 🔭🔭🔭约束类型 not null: 指示某列不能存储 NULL 值unique: 保证某列的每行必须有唯一的值default: 规定没有给列赋值时的默认值.primary key:

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝