本篇涉及到的全部代码见以下链接,欢迎参考指正!
红黑树概念
红黑树,本质上也是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
红黑树的性质
1. 每个结点不是红色就是黑色2. 根节点是黑色的3. 如果一个节点是红色的,则它的两个孩子结点是黑色的4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点,至于为什么这么叫,不用纠结,问就是作者规定的【手动狗头】)为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?【见下图分析】
红黑树结点的定义
//红黑树基本结构--红黑树结点的定义
//因为相较于普通的搜索二叉树,红黑树每个节点增加了颜色的属性,且不是红色就是黑色,为此我们定义一个枚举结构来表示颜色
enum Color
{
RED,
BLACK
};
//类比之前的AVL树和搜索二叉树,我们仍然设置两个模板参数分别方便表示Key,和value的类型
template<class K,class V>
//红黑树结点的定义
//class RBTreeNode,直接定义为struct后续访问的时候默认是共有的
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
{
typedef RBTreeNode<K, V> Node;
public:
//成员函数
//...
private:
Node* _root = nullptr;
};
红黑树的插入【重点】
红黑树也是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:1. 按照二叉搜索的树规则插入新节点【简单,我们直接实现代码】bool Insert(const pair<K, V>& kv) { //按照搜索二叉树的规则插入结点 //如果根节点为空,说明第一次插入,那么构造一个新结点让根节点指向它,将其颜色设为BLACK,返回true即插入成功 if (_root == nullptr) { _root = new Node(kv); _root->_col = BLACK; return true; } //根节点不为空,按照搜索二叉树规则插入数据 Node* cur = _root;//cur是为了向下找到插入的位置 Node* parent = nullptr;//parent是为了找到插入位置后与原树链接 while (cur) { if (cur->_kv.first < kv.first) { parent = cur; cur = cur->_right; } else if(cur->_kv.first>kv.first) { parent = cur; cur = cur->_left; } else { return false; } } //找到了插入的位置 cur = new Node(kv); if (cur->_kv.first > parent->_kv.first) { cur = parent->_right; } else { cur = parent->_left; } cur->_parent = parent; //到此结点按照搜索二叉树的性质插入成功了,接下来就是调整为红黑树结构 // ... //调整部分,我们先来分析 //... }
2. 检测新节点插入后,红黑树的性质是否造到破坏,若破坏了则进行相应调整
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连 在一起的红色节点,此时需要对红黑树分情况来讨论:规定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点注意:若父节点p为红色结点,每种情况下,g一定是存在且为黑的,若不存在,则父亲为根节点,根节点为红色不满足条件2,若为红,则g、p都为红,不满足条件3。cur为红,p为红,g为黑,u存在且为红情况1:分析如下://parent一定存在,当parent不为空且是红色结点,说明它不是根节点,就需要向上更新,是parent是黑色则跳过循环跳过直接返回true即可 while (parent&& parent->_col == RED) { Node* grandfather = parent->_parent; //分parent是grandfather的左孩子和右孩子两种情况来讨论 if (grandfather->_left == parent) { //定义grandfather的右孩子为uncle Node* uncle = grandfather->_right; //情况一 if (uncle && uncle->_col == RED) { //变色 uncle->_col = BLACK; parent->_col = BLACK; grandfather->_col = RED; //更新 cur = grandfather; parent = cur->_parent; } //情况二+三 else//uncle不存在或uncle存在且为黑 { //具体操作见下分析 //...... break; } } else//(grandfather->_right == parent) { //定义grandfather的左孩子为uncle Node* uncle = grandfather->_left; //情况一 if (uncle && uncle->_col == RED) { //变色 uncle->_col = BLACK; parent->_col = BLACK; grandfather->_col = RED; //更新 cur = grandfather; parent = cur->_parent; } //情况二+三 else//uncle不存在或uncle存在且为黑 { //具体操作见下分析 //...... break; } } }
cur为红,p为红,g为黑,u不存在/u存在且为黑
情况2:【p为g的左(右)孩子,cur为p的左(右)孩子】分析如下:情况3:【p为g的左(右)孩子,cur为p的右(左)孩子】分析如下:
分析完成后,Insert()完整代码如下:
bool Insert(const pair<K, V>& kv) { //按照搜索二叉树的规则插入结点 //如果根节点为空,说明第一次插入,那么构造一个新结点让根节点指向它,将其颜色设为BLACK,返回true即插入成功 if (_root == nullptr) { _root = new Node(kv); _root->_col = BLACK; return true; } //根节点不为空,按照搜索二叉树规则插入数据 Node* cur = _root;//cur是为了向下找到插入的位置 Node* parent = nullptr;//parent是为了找到插入位置后与原树链接 while (cur) { if (cur->_kv.first < kv.first) { parent = cur; cur = cur->_right; } else if(cur->_kv.first>kv.first) { parent = cur; cur = cur->_left; } else { return false; } } //找到了插入的位置 cur = new Node(kv); if (cur->_kv.first > parent->_kv.first) { cur = parent->_right; } else { cur = parent->_left; } cur->_parent = parent; //到此结点按照搜索二叉树的性质插入成功了,接下来就是调整为红黑树结构 //parent一定存在,当parent不为空且是红色结点,说明它不是根节点,就需要向上更新,是parent是黑色则跳过循环跳过直接返回true即可 while (parent&& parent->_col == RED) { Node* grandfather = parent->_parent; //分parent是grandfather的左孩子和右孩子两种情况来讨论 if (grandfather->_left == parent) { //定义grandfather的右孩子为uncle Node* uncle = grandfather->_right; //情况一 if (uncle && uncle->_col == RED) { //变色 uncle->_col = BLACK; parent->_col = BLACK; grandfather->_col = RED; //更新 cur = grandfather; parent = cur->_parent; } //情况二+三 else//uncle不存在或uncle存在且为黑 { //情况二 if (cur == parent->_left) { //右单旋+变色 RotateR(grandfather); parent->_col = BLACK; grandfather->_col = RED; } //情况3 else { //针对p左单旋,针对g右单旋 RotateL(parent); RotateR(grandfather); //更新结点颜色 cur->_col = BLACK; parent->_col=RED; grandfather->_col = RED; } break; } } else//(grandfather->_right == parent) { //定义grandfather的左孩子为uncle Node* uncle = grandfather->_left; //情况一 if (uncle && uncle->_col == RED) { //变色 uncle->_col = BLACK; parent->_col = BLACK; grandfather->_col = RED; //更新 cur = grandfather; parent = cur->_parent; } //情况二+三 else//uncle不存在或uncle存在且为黑 { //情况二 if (cur == parent->_right) { //左单旋+变色 RotateL(grandfather); parent->_col = BLACK; grandfather->_col = RED; } //情况3 else { //针对p右单旋,针对g左单旋 RotateR(parent); RotateL(grandfather); //更新结点颜色 cur->_col = BLACK; parent->_col = RED; grandfather->_col = RED; } break; } } } //有可能更新到了根节点,如果到了根节点就要把根节点置黑 _root->_col = BLACK; return true; }
补充:右单旋左单旋具体操作方法,在AVL树总结时有详细的分析,链接如下,这里只给出具体的代码做参考,需要注意的是红黑树中没有定义平衡因子,因此在旋转完与原树链接后无需更新平衡因子:
//右单旋 void RotateR(Node* parent) { //定义两个变量分别标记将作为替补根的父节点的左,以及后面要改变父亲的替补结点的右 Node* SubL = parent->_left; Node* SubLR = SubL->_right;//就是分析过程中的b树 //为了和上层的树链接,还要先记录一下当前parent的父亲 Node* pparent = parent->_parent; //按照分析改变链接关系 SubL->_right = parent; parent->_parent = SubL; parent->_left = SubLR; //b树不是空树,则更新SubLR的父指针 if (SubLR) { SubLR->_parent = parent; } //如果来之前parent就是根节点了,那直接将根节点更新为SubL,并让其父亲指向空 if (parent == _root) { _root = SubL; _root->_parent = nullptr; } //否则就让pparent指向parent的指针指向SubL else { if (parent == pparent->_left) { pparent->_left = SubL; } else { pparent->_right = SubL; } SubL->_parent = pparent; } } //左单旋 void RotateL(Node* parent) { Node* SubR = parent->_right; Node* SubRL = SubR->_left; Node* pparent = parent->_parent; //改链接关系 SubR->_left = parent; parent->_parent = SubR; parent->_right = SubRL; if (SubRL) { SubRL->_parent = parent; } if (parent == _root) { _root = SubR; _root->_parent = nullptr; } else { if (pparent->_left == parent) { pparent->_left = SubR; } else { pparent->_right = SubR; } SubR->_parent = pparent; } }
红黑树的验证:
和AVL树一样,我们写出了基本结构以及插入函数,那怎么验证其结构的正确性呢?
红黑树的检测分为两步:1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)中序遍历代码同搜索二叉树,只要中序遍历结果是升序,首先能证明的是我们的红黑树至少满足是搜索二叉树的,代码和验证结果如下:void Inorder() { _Inorder(_root); cout << endl; } private: void _Inorder(Node*& root) { if (root == nullptr) { return; } _Inorder(root->_left); cout << root->_kv.first << " "; _Inorder(root->_right); }
验证结果:
2. 检测其是否满足红黑树的性质
根据前面总结的红黑树的五点性质,我们来设计判断红黑树的IsBalance()函数
bool IsBalance() { //性质1:结点的颜色不是红色就是黑色,这点其实不用验证,因为我们定义的枚举类型就能约束这一点 //性质2:根节点是黑色,又因为空树也可看做红黑树,故若根节点存在且为红色,则不满足,返回false if (_root && _root->_col == RED) { cout << "根节点颜色是红色的"<<endl; return false; } //性质3:不能有连续的红结点; //检查是否有连续红结点只要遍历所有节点同时检查该节点是否和它的父节点同为红色即可 // 【检查当前结点与孩子结点不太好,因为一个结点可能没有孩子,但一定有父亲,走到这里已经不需要考虑整棵树的根节点了,因为前面已经判断过了】 //性质4:每个节点到该后代所有叶节点的所有简单路径上黑色结点数相等, //设置一个参考值,后面每遍历完一条路径就把得出的黑色节点数和这个基准值比较,不相等就返回false int benchmark = 0; Node* cur = _root; while (cur) { if (cur->_col == BLACK) { benchmark++; } cur = cur->_left; } //递归判断是否有连续的红结点以及每条路径黑色结点是否相等 return _check(_root, 0, benchmark); } private: bool _check(Node* root, int blacknum, int benchmark) { //到空节点,说明一条路径走完了,此时判断一次黑色结点数量和基准值是否相等,相等则直接返回true,不等返回false if (root == nullptr) { if (blacknum != benchmark) { cout << "某条路径黑色节点的数量不相等" << endl; return false; } return true; } //不是空节点且颜色是黑色,这条路径的黑色结点就加1 if (root->_col == BLACK) { blacknum++; } //是红色结点,就判断与父节点颜色关系,都为红色直接返回false if (root->_parent&& root->_col == RED && root->_parent->_col == RED) { cout << "出现连续的红色结点" << endl; return false; } //继续向左向右递归判断,当前的黑结点数量以及基准值都要往下传 return _check(root->_left, blacknum, benchmark) && _check(root->_right, blacknum, benchmark); }
随机插入一些值,检测结果如下: 再用生成一些随机数插入,检测结果如下:
到此,我们红黑树的结构基本没有什么大问题了
红黑树的查找、析构函数的补充:
这些都没什么大问题,直接给出代码就看得懂,不再赘述:
//析构 ~RBTree() { _Destory(_root); _root = nullptr; } //查找 Node* Find(const K& key) { Node* cur = _root; while (cur) { if (cur->_kv.first < key) { cur = cur->_right; } else if (cur->_kv.first > key) { cur = cur->_left; } else { return cur; } } return nullptr; } private: void _Destory(Node* _root) { if (root == nullptr) { return; } Destory(_root->_left); Destory(_root->_right); delete root; }
至于删除,同样会有一些复杂的调整,但是和AVL树一样,我们不需要再去详细的研究,红黑树学习的意义在于如何熟练使用它,实际当中我们基本上不用去自己手动实现一颗红黑树
红黑树与AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log_2 N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
补充:本节当中总结的红黑树的实现实际上和库中的实现还略有差别,这里为了更好的理解我们是参考了搜索二叉树和AVL树的实现,有哪些差别以及这些差别的意义在哪里?我们知道像map、set这些容器C++库中对他们的实现底层用的都是红黑树,我会在后面总结map、set相关底层以及封装时再详细说明,关于红黑树的底层原理及实现,掌握本篇笔记中的内容,对于我们初学者来讲已经够用了!
本篇涉及到的全部代码见以下链接,欢迎参考指正!