前言
前面一节我们介绍了平衡搜索二叉树AVL树,我们知道,AVL树虽然查找效率很高,但是不能过多的修改,因为它为了保持平衡要不断的进行旋转。我们今天介绍的红黑树也是一种平衡搜索树,不过它所要求的平衡没有AVL树那么严格,因此对它进行修改操作时所要进行的旋转比AVL树要进行的旋转少。
一、概念
红黑树,一种二叉搜索树,它额外在每一个结点上增加一个表示结点颜色的存储位,有Red和Black两种可能。
通过对任意一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长处二倍,因此它是接近平衡的。
二、性质
- 每个结点不是黑色就是红色;
- 根节点是黑色;
- 如果一个结点是红色,那么它的两个孩子结点是黑色;
- 对于每一个结点,从该节点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点;
- 每个叶子节点都是黑色(此处值空结点,即NIL结点)。
三、结点的定义
//结点的颜色 enum color { RED, BLACK };
//结点的定义 template<typename T> struct RBnode//红黑树的结点(三叉链) { RBnode(T data) :_data(data) , _parent(nullptr) , _left(nullptr) , _right(nullptr) , col(RED) {} T _data;//数据 RBnode* _parent;//父节点(为了方便实现旋转) RBnode* _left;//左孩子 RBnode* _right;//右孩子 color _col;//颜色 };
看到这里,相信大家会有一个疑问:为什么默认新插入的结点颜色是红色?
答:如果我们将新结点的颜色设置为黑色,那么它一定会违背性质4(即,对于每一个结点,从该节点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点),这样我们就需要大幅度的在这棵树上进行调整(几乎需要所有路径进行调整),才能使它再次符合性质4;
如果我们将新结点颜色设置为红色,它有可能违背性质3(即,如果一个结点是红色,那么它的两个孩子结点是黑色),也有一定的可能不违背,即使违背性质3它所带来的后果比违背性质4严重性小很多,我们可以通过几次旋转(只对它所在的路径进行调整)使它重新符合性质3。
因为我们新插入一个结点,它不是黑色就是红色,因此一定会违反一条性质,我们选择违反后果较小的性质3。
四、红黑树的结构
五、插入操作
1.插入代码
bool insert(const pair<K, V>& kv) { Node* newnode = new RBnode<K,V>(kv); if (!_root)//如果根节点为空,则新插入的结直接就是根节点 { _root = newnode; } else { Node* cur = _root; Node* parent = cur; while (cur) { parent = cur; if (cur->_kv.first > kv.first) { cur = cur->_left; } else if (cur->_kv.first < kv.first) { cur = cur->_right; } else//树中已经有这个结点,插入失败 { return false; } } cur = newnode; if (parent->_kv.first > cur->_kv.first) { parent->_left = cur; } else { parent->_right = cur; } cur->_parent = parent; Node* Grandpa = parent->_parent; Node* uncle = nullptr; while(Grandpa && parent->_col == RED)//如果父节点不是黑色,且父节点不是根结点 { if (parent == Grandpa->_left)//定义叔叔结点 { uncle = Grandpa->_right; } else { uncle = Grandpa->_left; } if (uncle->_col == RED)//如果叔叔存在且为红 { Grandpa->_col = RED; parent->_col = uncle->_col = BLACK; } else//如果叔叔不存在,或者存在且为黑 { //p是g的右孩子,c是p的右孩子(左单旋) if (parent == Grandpa->_right && cur == parent->_right) { Rotetal(Grandpa); Grandpa->_col = RED; parent->_col = BLACK; } //p是g的左孩子,c是p的左孩子(右单旋) else if (parent == Grandpa->_left && cur == parent->_left) { Rotetal(Grandpa); Grandpa->_col = RED; parent->_col = BLACK; } //p是g的左孩子,c是p的右孩子(左右双旋) else if (parent == Grandpa->_left && cur == parent->_right) { //先以parent为轴进行左单旋 Rotetal(parent); //再以Grandpa为轴进行右单旋 Rotetar(Grandpa); //更新颜色 cur->_col = BLACK; Grandpa->_col = parent->_col = RED; } //p是g的右孩子,c是p的左孩子(右左双旋) else if (parent == Grandpa->_right && cur == parent->_left) { //先以parent为轴进行右单旋 Rotetar(parent); //再以Grandpa为轴进行左单旋 Rotetal(Grandpa); //更新颜色 cur->_col = BLACK; Grandpa->_col = parent->_col = RED; } //旋转之后就符合性质4了,因此不用再继续更新 break; } cur = parent; parent = Grandpa; Grandpa = Grandpa->_parent; } if (_root->_col == RED)//如果到最后更新到根节点,导致根节点为红色,为了满足性质2(根节点是黑色),就要将根结点置为黑色 { _root->_col = BLACK; } } return true; }
2.左单旋
//左单旋 void Rotetal(Node* parent) { Node* SubR = parent->_right; Node* SubRL = SubR->_left; Node* Grandpa = parent->_parent; parent->_parent = SubR; parent->_right = SubRL; if (SubRL) { SubRL->_parent = parent; } SubR->_parent = Grandpa; if (!Grandpa) { _root = SubR; _root->_parent = nullptr; } else { if (parent == Grandpa->_left) { Grandpa->_left = SubR; } else { Grandpa->_right = SubR; } } SubR->_left = parent; }
3.右单旋
//右单旋 void Rotetar(Node* parent) { Node* SubL = parent->_left; Node* SubLR = SubL->_right; Node* Grandpa = parent->_parent; parent->_parent = SubL; parent->_left = SubLR; if (SubLR) { SubLR->_parent = parent; } SubL->_parent = Grandpa; if (!Grandpa) { _root = SubL; _root->_parent = nullptr; } else { if (parent == Grandpa->_left) { Grandpa->_left = SubL; } else { Grandpa->_right = SubL; } } SubL->_right = parent; }
4.插入新结点的情况分析与总结
第一步、按照搜索二叉树的规则插入新结点
如果该树是空树,就让新结点作为它的根节点。
先找到要插入的位置,比当前结点小就向左子树寻找,比当前结点大就向右子树寻找。
第二步、分析插入结点后红黑树的性质是否被破坏
新结点默认为红色,
1.如果双亲节点的颜色是黑色,则没有违反红黑树性质,不需要调整;
2.如果双亲节点的颜色是红色,则违反性质4需要进行调整。
为了方便分析,我们约定当前结点为cur©,当前节点的父节点为parent§,当前节点的祖父结点为Grandpa(g),当前结点的叔叔结点为uncle(c).。
- 情况一:c为红色,p为红,g为黑,u存在且为红
只需要将p和u的颜色置为黑色,g的颜色置为红色。
如果g是根节点,调整之后只需要将g改为黑色即可;
如果g是子树,那么g一定有双亲结点,如果g的双亲结点为红色,就需要继续向上调整。 - 情况二:cur为红,p为红,g为黑,u不存在/存在且为黑
- 如果u不存在,则说明cur是新增节点。因为如果cur不是新增节点,那么cur和p一定有一个是黑色,那么就不满足性质4(每条路径上的黑色结点的个数相同);
- 如果u存在且为黑,则说明cur原本就存在且为黑。现在是红色是因为cur所在子树新增节点导致向上调整颜色的过程中将cur置为红色。
这种情况有四种可能:
- p是g的左孩子,c是p的左孩子;(要进行右单旋)
以g为轴进行右单旋:
更新结点p为黑色,cur和g为红色。 - p是g的右孩子,c是p的右孩子;(要进行左单旋)
以g为轴进行左单旋:
更新结点p为黑色,cur和g为红色。 - p是g的左孩子,c是p的右孩子;(要进行左右双旋)
先以p为轴进行左单旋,再以g为轴进行右单旋:
更新结点cur为黑色,p和g为红色。 - p是g的右孩子,c是p的左孩子。(要进行右左双旋)
先以p为轴进行左单旋,再以g为轴进行右单旋:
更新结点cur为黑色,p和g为红色。
动态演示:
升序:
降序:
随机插入构建红黑树:
右旋转:
左旋转:
六、验证红黑树
验证红黑树分为两步:
1.检测是否满足二叉搜索树
中序遍历是否为有序序列
2.检测是否满足红黑树性质
代码如下:
bool IsValidRBTree()//验证是否为红黑树 { Node* pRoot = GetRoot(); // 空树也是红黑树 if (nullptr == pRoot) return true; // 检测根节点是否满足情况 if (BLACK != pRoot->_col) { cout << "违反红黑树性质二:根节点必须为黑色" << endl; return false; } // 获取任意一条路径中黑色节点的个数 size_t blackCount = 0; Node* pCur = pRoot; while (pCur) { if (BLACK == pCur->_col) blackCount++; pCur = pCur->_left; } // 检测是否满足红黑树的性质,k用来记录路径中黑色节点的个数 size_t k = 0; return _IsValidRBTree(pRoot, k, blackCount); } bool _IsValidRBTree(Node* pRoot, size_t k, const size_t blackCount) { //走到null之后,判断k和black是否相等 if (nullptr == pRoot) { if (k != blackCount) { cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl; return false; } return true; } // 统计黑色节点的个数 if (BLACK == pRoot->_col) k++; // 检测当前节点与其双亲是否都为红色 Node* pParent = pRoot->_parent; if (pParent && RED == pParent->_col && RED == pRoot->_col) { cout << "违反性质三:没有连在一起的红色节点" << endl; return false; } return _IsValidRBTree(pRoot->_left, k, blackCount) && _IsValidRBTree(pRoot->_right, k, blackCount); }
七、红黑树与AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删查改的时间复杂度都是O(l o g 2 N log_2 Nlog2N),红黑树不追求绝对平衡,只要保证最长路径不超过最短路径的两倍。相对而言,插入和旋转的次数更少,在经常进行增删的结构中性能比AVL树更优,而且红黑树的实现比AVL树简单,因此更加常用。
总结
以上就是今天要讲的内容,本文介绍了C++中红黑树的相关概念。本文作者目前也是正在学习C++相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!