我的创作纪念日——《惊变365天》
机缘
不知不觉距离上次256天纪念日又悄悄地过去了100多天了,同时距离鼠鼠第一次创作已经过去了整整365天了。100天,一周年多么有意义的时刻啊!
通过前几次的纪念日,相信大家对于鼠鼠为什么开始写博客都已经非常熟悉了,那么这个板块鼠鼠就浅谈一下鼠鼠从入学到现在的学习历程吧。这样大家既能了解鼠鼠的过去,还能从鼠鼠的经历中吸取教训(っ﹏-)。
看过鼠鼠前几篇博客的都知道鼠鼠是在大一结束后的那个暑假才开始写博客的。大二下半学期才开始慢慢稍有起色。整个大一期间呢,鼠鼠并没有接触博客创作。大一上半学期刚入学时,鼠鼠先学了 C 语言。记得当时的鼠鼠的启蒙书是《明解 C 语言》,感慨不愧是日本作家的作品,排版特别清晰美观,每章结束还有总结,也特别贴心。不过就是书的后半部分内容有些仓促,示例明显比前面少了很多。另外,谭浩强那本书的后半部分写得也是拉跨,尤其是文件管理部分,表述得模棱两可,就似乎没打算详细讲解这部分的内容,以上内容仅供参考的意思。
过了一段时间,鼠鼠在 B 站上了解到了比特教育的鹏哥讲的 C 语言课程,于是就重新跟着视频学习了 “指针 + 文件管理” 的内容。鹏哥可以说是鼠鼠的C语言启蒙老师了,从他那里鼠鼠不仅学到了C语言知识,还了解了整个计算机行业的发展态势和大学生的个人生涯规划,真心的特别感谢他,让我对未来有了方向。记得当时鹏哥说过:如果是为了就业,熟练掌握 C++ 或 Java 中的一门语言就可以了。虽然那时鼠鼠还没接触过 C++,但心里已经暗下决心:C++ 就是鼠鼠的本命语言,鼠鼠要使用一辈子,鼠鼠最终一定要熟练掌握C++。
学完了C语言之后鼠鼠就开始学习数据结构了,B站上也没有什么资源,那时候鼠鼠主要是看书,还记得看的那本书是《大话数据结构》,据说是本好书,但是鼠鼠学的模模糊糊被劝退了好几次,当时鼠鼠就是闭门造车,缺少实际代码的练习,浪费了很多时间,还天真的认为明白几个名词,就算是掌握了数据结构,哈哈往事不堪回首。(。•ˇ‸ˇ•。)!
就在这迷迷瞪瞪中鼠鼠学完了数据结构,鼠鼠又是马不停蹄的开始学习C++了,学习C++当然就要找关于找C++最好的书籍了,没错鼠鼠当时就直接抱着《C++ prime》开始看了下去了。没过多久鼠鼠在看这本书的过程中就陷入了“人格分裂”,因为这本书更像是一本字典、一本工具书,内容非常详细,但就是非常详细,导致:1. 前面的简单的内容中也会夹杂着与其相关的复杂的内容,2. 书太厚了,以至于鼠鼠完全不能建立正反馈,鼠鼠一直摇摆在坚持与放弃之间,最终选择了放弃。
转眼间时间也就来到了大一的下半学期,鼠鼠又开始打算重拾 C++ 了。巧的是,学校下半学期正好开了 C++ 这门课,发的是一本国内编写的教材,内容比较单薄。书里完全没涉及 C++11 的 “异常 + 智能指针” 相关知识,STL 部分也只是简单带过,示例少得可怜(鼠鼠真的很看重代码示例,觉得一个好的代码示例胜过千言万语)。所以鼠鼠当时很快就翻完了这本书,但是并没能真正学全 C++ 的核心内容。
后来在 B 站刷到黑马的关于C++的教学视频,视频有4千多万的播放量,看着有这么多的播放量,就好奇点进去看了一下,确实讲得不错。鼠鼠对其中 STL 部分的讲解印象尤其深刻 ——STL 主要就是讲各种容器的使用,但是很多容器其实有相通之处,按理说完整讲透一个,后面的只需说明区别就行。可那位老师竟然能不厌其烦地把每个容器的用法从头敲了一遍,换作是我光听都要腻了,他却坚持敲完了全程。当时真心觉得这门课讲得特别好,可惜也是没有涉及C++11部分的内容,并且STL的讲解也只是停留在表面的使用教学,并未深入原理。也能理解毕竟人家还是要赚钱的嘛,能做到这个份上已经超过不少学校和教材了。
看到这里,大家可能会好奇:“难道你都不敲代码的吗?” 哈哈,没听错,鼠鼠当时是真的不敲代码!鼠鼠当时心里是这么想的:书上的代码我都能看懂,视频里讲的知识点也能听明白,不实际动手敲应该也没什么关系。没记错的话,就是在这种毫不回顾和练习的情况下,鼠鼠当时只用了两周就把 C++ “学完” 了。当时自鼠感觉良好,却殊不知自己已经埋下了恶果。(。•́︿•̀。)!
很快,鼠鼠就为这种行为付出了惨痛的代价。“学完” C++ 后,鼠鼠就兴致勃勃地去刷 LeetCode 上的编程题了,没想到噩梦就此开始 —— 因为鼠鼠对数据结构掌握得本就不扎实,C++ 又缺乏实际练习,那些题目根本无从下手。哪怕是题目给的答案示例代码,鼠鼠都得翻来覆去研究好久才能弄明白是什么意思。
后来鼠鼠又试着学了点 Linux 的内容,选的也是知名图书《鸟哥的linux私房菜》,可是这本书实在是太厚了,最后还是没能坚持下来。当时 B 站上也没什么深入讲解 Linux 的视频,学了一点皮毛后这件事也就不了了之了。
听到这儿,小伙伴们可能会问:“那你大二都干了些什么呀?” 说实话,整个大二一年,鼠鼠几乎没学什么“新内容”。大二上半学期,鼠鼠一边硬着头皮刷算法题,一边啃《操作系统导论》这本书 —— 别提了,那简直是段痛苦的回忆!也正因如此,鼠鼠对外国计算机教材彻底产生了心理阴影,再也不想碰了。其实在这之前,鼠鼠还试过看《深入理解计算机系统》(简称 CSAPP),那本更是把鼠鼠劝退得最快的一本书了,鼠鼠只翻了二三十页就看不下去了。书里全是数学相关的内容,当时边看边吐槽:“这书真有那么好吗?我怎么一点也没觉得?”
到最后我才想明白:书没问题,我也没问题,问题出在推荐这本书的人身上。鼠鼠现在真想在那些推荐帖下面补一句:《深入理解计算机系统》确实是计算机领域的神书,但真的不建议初学者入手!
从一开始卯足了劲往前冲,到后来在一次次无法获得正反馈的煎熬中不断放弃,鼠鼠的斗志一点点被磨没了,也渐渐开始迷茫,最后彻底迷失了方向。就这样迷茫到了大二下半学期,鼠鼠终于做了个现在想来都挺果断的决定:报名参加了培训班,下定决心跟着专业老师的节奏,从数据结构开始到项目实战完完整整地重新学一遍。(ノ>ω<)ノ
收获
可能会有小伙伴们会好奇,鼠鼠在这 100 天里都收获了些什么呢?
先说说大家能看到的成绩:目前鼠鼠的博客获赞量已经有 4000 多,收藏量 3600 多,粉丝数 1560,评论加互动也超过了 3000。和 100 天前相比,这些数据都有了明显增长,这段时间也算是鼠鼠作为博主以来成长最快的阶段了。
除此之外,鼠鼠现在也能稳定登上创作者周榜了,还会断断续续出现在 C++ 内容榜上 —— 不过每次名次都比较靠后,这里忍不住吐槽一句:C++ 内容榜真的比数据结构与算法内容榜难上太多了,有时候 1000 热度都只能排到末尾甚至也上不了~
当然,这些都只是表面的收获。毕竟写博客的初心,才是衡量收获的核心标准。从这一点来说,鼠鼠真的做到了!鼠鼠写的这些博客,不仅在后续的学习中实实在在帮到了自己,也给其他小伙伴提供了参考。能达成最初的目标,鼠鼠现在真的觉得收获满满。
日常
关于鼠鼠的日常,相信大家多少会有点好奇。其实平时除了吃饭睡觉,鼠鼠几乎都在钻C++开发的学习上了,具体点说就是:“看视频听新课 + 课后写新博客 + 水课看旧博客”。这时候可能有小伙伴会问:“那你不玩游戏就算了,也不上学校的课吗?” 哈哈,实话说,鼠鼠平时基本是能翘课就翘课的,不能翘就原地表演遁地早退大法了哈哈。(ㆆᴗㆆ)キリッ✧
还有小伙伴可能会疑惑:“你为什么这么拼啊!”,其实原因很简单一是鼠鼠的学校一般,所以鼠鼠很清楚只能靠自己拼了;二是因为之前走了太多弯路,学习上病急乱投医,看似热热闹闹忙了两年,结果啥实质性进展都没有,还空耗了大把时间 —— 正是因为有过那样的教训,鼠鼠现在才更想好好拼一把。
想必大家现在也看到了鼠鼠的日常中有一重要的一环就是:写博客。
写博客其实对于鼠鼠来说是特别耗精力的事,但不得不说,这份精力付出绝对是值得的。鼠鼠觉得写博客就有点像记笔记,既保留了 “方便自己回顾复习” 的优点,又多了 “能和他人分享交流” 的价值。
不过,鼠鼠明白想让博客真正发挥作用,核心在于 “回顾复习”,否则效果会大打折扣。这就像高中时流行的错题本 —— 记错题本身肯定有用,但必须建立在定期回顾的基础上;要是不复习,记错题的行为就会变成老师罚抄错题似的,这样的话就算是再抄一遍,最后还是会忘得一干二净。
鼠鼠觉得写博客应该也是这么个道理,如果不能定时进行复习回顾,写博客最终也会变成单纯的 “文字搬运”,辛辛苦苦写完就丢在一边,既记不住知识点,也发挥不了真正的价值。
所以鼠鼠的日常,除了写新博客,就是回头翻看旧博客,让每一篇内容都能真正发挥价值。这里分享鼠鼠特别喜欢一句话:“学而不思则罔,思而不学则殆。”
以鼠鼠的“鼠光”来看:
- 学习新知识后,及时整理总结、写成博客,这才能称得上是真正的 “学”;
- 定期回顾旧知识,从中提炼精髓内化成自己的东西,甚至从旧知识里悟出新感悟,这才算是真正的 “思”。
目前鼠鼠还做不到 “学思并举、融汇贯通”,所以日常里就在努力抓好 “学” 和 “思” 这两件事,希望能让之后的学习更高效。
成就
从上次 256 天纪念日到现在,鼠鼠结束了《数据结构初阶》之后,主要分享的内容围绕《C++ 初阶》和《C++ 进阶》展开。这段时间里,鼠鼠陆续写了几十篇博客,每一篇都会确保包含充足的代码示例 —— 这些示例都是鼠鼠结合老师课堂所讲、AI 辅助生成,再经过鼠鼠严格筛选后敲定的优质示例哦。
因为鼠鼠始终觉得,一个好的代码示例胜过千言万语。如果非要从中选一段印象最深的,那一定是这段时间里鼠鼠写过的最具挑战性的红黑树源码了。
#pragma once//任务1:包含需要使用的头文件
#include <iostream>
using namespace std;//任务2:定义红黑树节点的“颜色枚举”
enum Colour
{RED, //红色节点BLACK //黑色节点
};//任务3:定义红黑树节点的“结构体模板”
template <class K,class V>
struct RBTreeNode
{/*--------------------------成员变量--------------------------*///1.节点存储的键值对//2.左孩子指针//3.右孩子指针//4.父节点指针//4.节点的颜色pair<K, V> _kv;RBTreeNode<K, V>* _left; RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent; /* 注意事项:怎么理解上面的三个指针的类型为什么要这么写成BBTNode<K,V>*的形式?* 1. 首先:它们都是指针,所以要添加指针的符号“*”* 2. 其次:它们都指向的都是红黑树的节点,所以要添加节点的类型“RBTNode”* 3. 最后:红黑树的节点是“结构体”并且是“结构体模板”,所以要添加模板参数“<K,V>”*/Colour _col;/*--------------------------成员函数--------------------------*///1.定义:“红黑树节点的构造函数”RBTreeNode(const pair<K,V>& kv) //注意:参数是“红黑树节点存储的键值对”:_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr){}
};//任务4:定义红黑树的“类模板”
template<class K, class V>
class RBTree
{
private:/*--------------------------成员变量--------------------------*/RBTreeNode<K, V>* _root = nullptr;/*--------------------------类型别名--------------------------*///1.重新定义红黑树节点的类型:RBTreeNode<K,V> ---> Nodetypedef RBTreeNode<K, V> Node;/*--------------------------成员函数(私有)--------------------------*///1.实现:“中序遍历”void _InOrder(Node* root){/*------------处理“特殊情况 + 递归出口”------------*/if (root == nullptr){return;}/*------------处理“正常情况”------------*///1.递归遍历左子树_InOrder(root->_left);//2.输出当前节点的键值对cout << root->_kv.first << ":" << root->_kv.second << endl;//3.递归遍历右子树_InOrder(root->_right);}//2.实现:“获取树的高度”int _Height(Node* root){/*------------处理“特殊情况 + 递归出口”------------*/if (root == nullptr){return 0;}/*------------处理“正常情况”------------*///1.递归计算左子树的高度int leftHeight = _Height(root->_left);//2.递归计算右子树的高度int rightHeight = _Height(root->_right);//3.返回较高子树的高度+1return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;}//3.实现:“获取树中节点的数量”int _Size(Node* root){/*------------处理“特殊情况 + 递归出口”------------*/if (root == nullptr){return 0;}/*------------处理“正常情况”------------*///1.递归计算树中节点的总数量 ---> 节点数 = 左子树节点数 + 右子树节点数 + 1(当前节点)return _Size(root->_left) + _Size(root->_right) + 1;}//4.实现:“判断一棵树是不是红黑树”bool Check(Node* root, int blackNum, const int refNum){/*------------处理“特殊情况 + 递归出口”------------*/if (root == nullptr){//情况1:新遍历的这条路径上的黑色节点的数量“不等于”最左侧路径上的黑色节点的数量if (refNum != blackNum){cout << "存在黑色结点的数量不相等的路径" << endl;return false;}//情况2:新路径“等于”旧路径return true;}/*------------处理“正常情况”------------*///1.检查“连续红节点的情况”if (root->_col == RED && root->_parent->_col == RED) //注意这里的细节,如果我们正向思维的话{cout << root->_kv.first << " 存在连续的红色结点" << endl;return false;}/* 注意实现:如果解决上面的这个问题我们使用正向思维的话:“对于当前红色节点要判断它的孩子是也是红色节点”* 1. 我们不禁要判断root节点的左孩子还要判断右孩子是不是“红色”* 2. 并且有可能正在遍历的这个节点还有可能根本不存在*///2.检查“记录黑色节点的数量”if (root->_col == BLACK){blackNum++;}//3.递归检查左右子树return Check(root->_left, blackNum, refNum)&& Check(root->_right, blackNum, refNum);}public:/*--------------------------成员函数(公有)--------------------------*///1.实现:“红黑树的查找”的操作Node* Find(const K& key) //注意:根据键key进行查找,类似二叉搜索树中查找操作{//1.定义进行遍历节点的指针Node* curr = _root;//2.使用while循环查找对应节点while (curr){//情况1:当前遍历到的节点的键 < 要查找的键 ---> “继续向当前节点的右子树中查找”if (curr->_kv.first < key){curr = curr->_right;}//情况2:当前遍历到的节点的键 > 要查找的键 ---> “继续向当前节点的左子树中查找”else if (curr->_kv.first > key){curr = curr->_left;}//情况3:当前遍历到的节点的键 = 要查找的键 ---> “找到了要查找的键”else{return curr;}}//3.跳出了循环,说明没有找到的键为key的节点return nullptr;}//2.实现:“红黑树的插入”的操作bool Insert(const pair<K, V>& kv) //注意:对于set而言插入的是键K key,对于map而言插入的是键值对pair<K,V> kv{/*--------处理“特殊情况 + 递归出口”--------*/if (_root == nullptr){//1.创建新节点_root = new Node(kv);//2.将新节点染色为黑色 (*相比AVL树多了这一步骤*)_root->_col = BLACK;//3.返回true即可return true;}/*--------处理“正常情况”--------*//*----------------第一阶段:准备阶段----------------*///1.创建一个指向当前节点的指针Node* current = _root;//2.创建一个指向当前节点的父节点的指针Node* parent = nullptr;/*----------------第二阶段:查找阶段----------------*/while (current) //循环查找插入位置{//情况1:当前遍历到的节点的键 < 要插入的键 ---> “继续寻找”if (current->_kv.first < kv.first) //注意:没学pair之前我们是这样写的:if (current->_key < key){//1.更新当前遍历节点的父节点 parent = current;//不同之处1:这里的需要更新parent指针的位置 //原因:下面我们要在curr指针指向的位置进行插入操作,所以我们需要记录当前遍历到节点的父节点//2.继续去右子树中寻找current = current->_right;}//情况2:当前遍历到的节点的键 > 要插入的键 ---> “继续寻找”else if (current->_kv.first > kv.first) //注意:没学pair之前我们是这样写的:else if (current->_key > key){parent = current;current = current->_left; //继续去左子树中寻找}//情况3:当前遍历到的节点的键 = 要插入的键 ---> “键已存在”---> 查找插入位置失败else{return false;//不同之处2:这里放回的是false//原因:我们实现的二叉搜索树不支持存储“键相等的节点”}}//注意:能执行到此处,说明在第二阶段成功跳出了while循环,并非因return false终止程序,//这意味着已找到插入节点的位置,那下面就让我们插入节点吧/*----------------第三阶段:插入阶段----------------*///1.创建要插入的节点current = new Node(kv); //注意:没学pair之前我们是这样写的:current = new Node(key, value);//2.将新节点染为红色 (*相比AVL树多了这一步骤*)current->_col = RED;//3.将新节点连接到二叉搜索树中 ---> 注意并不能简单的进行插入操作要先判断:// “新节点和父节点的键之间的大小关系,从而判断出新节点是应该插入到父节点的左子节点还是右子节点”//情况1:新节点的键 < 父节点的键if (kv.first < parent->_kv.first) //注意:没学pair之前我们是这样写的:if (key < parent->_key){parent->_left = current;}//情况2:新节点的键 > 父节点的键else //注意:这里使用else表面上看是:满足key >= parent->_key条件的情况都可以执行下面的代码{ //但其实key = parent->_key这种情况在上面的第二阶段中已经被的return false排除掉了parent->_right = current;}/*------------------声明:如果是普通的二叉搜索树,到这里插入操作就已经算作是结束了,但是对于平衡二叉搜索树还有步骤------------------*//*----------------第四阶段:连接阶段----------------*///1.更新新插入节点的父节点current->_parent = parent; //这里之所以要进行更新是因为,红黑树节点的存储了三个指针,也就是其底层是用“三叉链”的结构进行存储的/*----------------第五阶段:调整阶段----------------*/while (parent&& parent->_col == RED) //不断地进行调整,直至:“parent为空节点”或“parent节点颜色为黑色”{//1.获取祖父节点(父节点的父节点)Node* grandfather = parent->_parent;//2.处理需要进行调整的场景//场景一:父节点是祖父节点的左孩子节点if (parent == grandfather->_left){//情况1:叔叔节点“存在”且为“红色” ---> “变色处理”Node* uncle = grandfather->_right;if (uncle && uncle->_col == RED){/*-------第一步:变色处理-------*///1.将“父节点”染为“黑色”//2.将“叔叔节点”染为“黑色”//3.将“祖父节点”染为“红色”parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;//注意:处理完上面这一种仅仅需要变色的情况之后,调整还没结束还需要向上继续进行检查/*-------第二步:向上检查-------*///1.“祖父节点”变为“当前节点”current = grandfather;//2.“父节点”变为“祖父节点的父节点”parent = grandfather->_parent; //或者写成:parent = current->_parent;}//情况2:叔叔节点“不存在”或为“黑色” ---> “旋转处理”else{//情况2.1:当前节点是父节点的左孩子 ---> “右单旋”if (current == parent->_left){/*-------第一步:旋转处理-------*///1.右单旋“祖父节点”RotateR(grandfather);/*-------第一步:变色处理-------*///1.将“父节点”染为“黑色”//2.将“祖父节点”染为“红色”parent->_col = BLACK;grandfather->_col = RED;}//情况2.2:当前节点是父节点的右孩子 ---> “左右双旋”else{/*-------第一步:旋转处理-------*///1.先左单旋“父节点”RotateL(parent);//2.再右单旋“祖父节点”RotateR(grandfather);/*-------第二步:变色处理-------*///1.将“当前节点”染为“黑色”//2.将“祖父节点”染为“红色”current->_col = BLACK;grandfather->_col = RED;}//处理完上面的两种需要进行旋转的情况之后,就可以跳出while循环了break;}}//场景二:父节点是祖父节点的右孩子节点 (注意:场景二其实和场景一的本质是一样的,区别在于两者由于镜像的关系所以两者的旋转操作的是相反的)else{//情况1:叔叔节点“存在”且为“红色” ---> “变色处理”Node* uncle = grandfather->_left;if (uncle && uncle->_col == RED){/*-------第一步:变色处理-------*///1.将“父节点”染为“黑色”//2.将“叔叔节点”染为“黑色”//3.将“祖父节点”染为“红色”parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;//注意:处理完上面这一种仅仅需要变色的情况之后,调整还没结束还需要向上继续进行检查/*-------第二步:向上检查-------*///1.“祖父节点”变为“当前节点”current = grandfather;//2.“父节点”变为“祖父节点的父节点”parent = grandfather->_parent; //或者写成:parent = current->_parent;}//情况2:叔叔节点“不存在”或为“黑色” ---> “旋转处理”else{//情况2.1:当前节点是父节点的右孩子 ---> “左单旋”if (current == parent->_right){/*-------第一步:旋转处理-------*///1.左单旋“祖父节点”RotateL(grandfather);/*-------第一步:变色处理-------*///1.将“父节点”染为“黑色”//2.将“祖父节点”染为“红色”parent->_col = BLACK;grandfather->_col = RED;}//情况2.2:当前节点是父节点的左孩子 ---> “右左双旋”else{/*-------第一步:旋转处理-------*///1.先右单旋“父节点”RotateR(parent);//2.再左单旋“祖父节点”RotateL(grandfather);/*-------第二步:变色处理-------*///1.将“当前节点”染为“黑色”//2.将“祖父节点”染为“红色”current->_col = BLACK;grandfather->_col = RED;}//处理完上面的两种需要进行旋转的情况之后,就可以跳出while循环了break;}} //场景二结束}//调整阶段结束//1.根节点强制染黑_root->_col = BLACK;//2.返回true即可return true;}//Insert函数结束//3.实现:“右单旋”操作(以parent为中心右旋)void RotateR(Node* parent){/*---------------第一阶段:准备阶段---------------*///1.记录parent的左子节点的“指针”//2.记录parent的左子节点的右子节点的“指针”//3.记录parent的父节点的“指针”Node* subL = parent->_left;Node* subLR = parent->_left->_right; //或者写成:Node* subLR = subL->_right;Node* pParent = parent->_parent;/*---------------第二阶段:调整阶段---------------*///1.调整parent和subLR的关系parent->_left = subLR;if (subLR) //注意细节:subLR不一定存在,所以这里为了判断防止对空指针进行解引用,先使用if对subLR指针进行一个判断{subLR->_parent = parent;}//2.调整parent和subL的关系parent->_parent = subL;subL->_right = parent;//3.调整根节点或祖父节点的子树指向//情况1:parent节点是根节点 ---> 调整根节点if (parent == _root){//1.调整根节点_root = subL; //注意:这里的调整根节点要写成:_root = subL,千万不要写成了subL = _root;//2.更新subL的父节点subL->_parent = nullptr; //或者写成:_root->_parent =nullptr;}//情况2:parent节点不是根节点 ---> 调整祖父节点的子树指向else{//1.调整祖父节点的子树指向if (parent == pParent->_left){pParent->_left = subL;}else{pParent->_right = subL;}//2.更新subL的父节点subL->_parent = pParent;}////4.更新平衡因子 ---> 右单旋后subL和parent的平衡因子均为0//subL->_bf = 0;//parent->_bf = 0;//注意:对于红黑树由于没有使用“平衡因子”所以旋转结束后并不需要更新平衡因子}//4.实现:“左单旋”操作(处理右右失衡的情况)void RotateL(Node* parent){/*---------------第一阶段:准备阶段---------------*///1.记录parent的右子节点的“指针//2.记录parent的右子节点的左子节点的“指针”//3.记录parent的父节点的“指针”Node* subR = parent->_right;Node* subRL = parent->_right->_left; //或者写成:Node* subLR = subL->_left;Node* pParent = parent->_parent;/*---------------第二阶段:调整阶段---------------*///1.调整parent和subRL的关系parent->_right = subRL;if (subRL){subRL->_parent = parent;}//2.调整parent和subR的关系parent->_parent = subR;subR->_left = parent;//3.调整根节点或祖父节点的子树指向//情况1:parent节点是根节点 ---> 调整根节点if (pParent == nullptr) //或者写成:parent == _root{//1.调整根节点_root = subR;//2.更新subL的父节点subR->_parent = nullptr; //或者写成:_root->_parent = nullptr;}//情况2:parent节点不是根节点 ---> 调整祖父节点的子树指向else{//1.调整祖父节点的子树指向if (parent == pParent->_left){pParent->_left = subR;}else{pParent->_right = subR;}//2.更新subL的父节点subR->_parent = pParent;}////4.更新平衡因子 ---> 左单旋后subR和parent的平衡因子均为0//subR->_bf = 0;//parent->_bf = 0;}//5.实现:“中序遍历”void InOrder(){_InOrder(_root);cout << endl;}//6.实现:“获取树的高度”int Height(){return _Height(_root);}//7.实现:“获取节点的数量”int Size(){return _Size(_root);}//8.实现:“判断一棵树是不是红黑树”bool IsRBTree(){/*------------处理“特殊情况 + 递归出口”------------*///特殊情况1:“空树”if (_root == nullptr){return true; //空树也是红黑树}//特殊情况2:“只有一个节点并且为红色”if (_root->_col == RED){return false; //不满足红黑树的规则}/*------------处理“正常情况”------------*//*------- 第一步:计算最左路径上节点的数量-------*///1.记录该路径上黑色节点的数量int refNum = 0;//2.定义一个指向当前节点的指针Node* current = _root;//3.使用while循环统计该路径上黑色节点的数量while (current){//3.1:统计if (current->_col == BLACK){++refNum;}//3.2:移动current = current->_left;}/*-------第二步:递归检查其他路径上面的黑色节点的数量-------*/return Check(_root, 0, refNum);}
};
还记得鼠鼠刚开始学数据结构的时候,就早有耳闻红黑树的 “大名”。可无论是学校课堂上的学习,还是《大话数据结构》这本书里的内容,都没有涉及红黑树的具体实现;B站上很多教程也对此避而不谈。以至于那时候的鼠鼠总觉得,能实现红黑树的都不是普通人,所以鼠鼠也就没有苛求过自己一定要学会它。直到后来跟着培训班老师系统学习,才发现红黑树并非完全不可接近 —— 最后鼠鼠也成功实现了红黑树的插入操作。
比如上面鼠鼠粘贴的红黑树源码:这份源码既保留了二叉搜索树 “高效查找 / 插入” 的特性,又通过 “旋转 + 变色” 解决了失衡问题(查找 / 插入时间复杂度稳定为O(log n)
);同时模板化设计和清晰的函数分工,让代码具备通用性和可复用性,可作为set
/map
(C++ STL 中这两个容器的底层就是红黑树)的底层核心逻辑。
这些这些东西对于当年那个鼠鼠来说简直就是不敢想的事情,没想到最终鼠鼠还是做到了,这也许就是最好的成就了吧——把不可能的事情变成可能!挑战自己让自己变得更好!
憧憬
最后鼠鼠想再和大家聊聊今后的打算。
鼠鼠的长远目标一直没变:希望大四秋招时能拿到一份不错的开发岗 offer。而现在鼠鼠已经是大三了,近期的小目标则是争取今年寒假能找到一份开发实习。为了实现这个短期目标,大三上半学期鼠鼠必须更拼命地学习专业知识、打磨项目经验,同时提前准备好简历。所以说,鼠鼠现在真的是时间紧、任务重。同时这也是鼠鼠现在为什么这么拼命的另一个原因了。
这年头找工作本来就不容易,提前准备才能多一分机会。可惜鼠鼠已经错过了不少的时间了,主动占得先机已经变成被动了,所以剩下的每分每秒鼠鼠都不敢浪费了。
至于在 CSDN 这边的小期待,就是希望能在 10 月中旬前能拥有2000粉丝。除此之外,暂时鼠鼠就没有其他特别的规划啦。
纪念日就先纪念到这里吧,我们下次纪念日再见,这里就先拜拜了~(。•́‿•̀。)♡