红黑树 详解
黑红树和上一篇AVL树一样 也是一棵二叉搜索树 同样可以解决二叉搜索树最坏情况的问题 不过没有AVL树那么严格 AVL树只要有一个节点的左右子树高度差大于1了就需要处理 而红黑树只需要确保没有⼀条路径会比其他路径长出2倍
红黑树的介绍及规则
红黑树相较于二叉搜索树有了一个变量来存储每一个节点的颜色 而存的颜色顾名思义只有红和黑两种颜色 通过对任何⼀条从根到叶子的路径上各个结点的颜色进行约束 来实现它的功能
黑红树的规则
- 每个结点不是红色就是黑色
- 根结点是黑色的
- 如果一个结点是红色 如果它的孩子节点存在 则它的孩子结点一定是黑色的,也就是说任意一条路径不会有连续的红色结点。
- 对于任意⼀个结点,从该结点通过任意一条路径到空节点,均包含相同数量的黑色结点
如下图 就是符合要求的红黑树图 可以数一下从根到空节点的路径一共有多少条
结果如下 一共有十条路径 每一条路径上黑色节点的数量都是一样的 路径长度是从根节点到空节点的长度单位 比如①路径算上空节点为4个节点长度为4
另外 《算法导论》等书籍上补充了⼀条每个叶⼦结点(NIL)都是⿊色的规则。他这里所指的叶子结点 不是传统的意义上的叶子结点,而是我们说的空结点 (了解一下就好)
红黑树是如何确保最长路径不超过最短路径的2倍的
由规则4可知 从根到空节点的最短路径为这一路径上全为黑节点的情况
由3知道两个红色节点不能连续 那么从根到空节点的最长路径为一黑一红的情况 也就是二倍全黑节点 也就是说红黑树中最长路径是最短路径的2倍
所以如果这颗树是红黑树 那么最长路径不超过最短路径的2倍是一定成立的 而且最短路径和最长路径在一颗红黑树中也不一定存在的 所以从根节点到空节点的任意路径的节点数n满足
最短路径<=n<=最长路径
红黑树的效率
假设节点数量为N 红黑树的最短路径长度为h 那么最长路径就为2h
那么N满足2^h-1<=N<2^2*h−1所以 h≈logN 所以即使在最坏情况下 查找效率也为 O(logN)。
之前的AVL树是通过高度直观地控制平衡 而红黑树是通过遵守四条规则来近似平衡了 但是他们的效率是同一级别的
红黑树实现
首先先搭建好基本的结构 和AVL树那里一样 只是平衡因子变成了颜色 另外用到了枚举体来存两个颜色 这样保证了里面不会存其他的颜色
enum Colour
{RED,BLACK
};template <class K, class V>
struct RBTreeNode
{pair<K, V> _kv;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;Colour _col;RBTreeNode(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr){}
};
template<class K, class V>
class RBTree
{
public:typedef RBTreeNode<K, V> Node;private:Node* _root = nullptr;
};
插入
插入的情况比较多 总结之后就是下图
刚开始先按照二叉搜索树的规则把这个新的节点给插入
那么插入的这个节点的颜色是什么呢
有两种情况
① 如果这个树是空树 新插入的节点必须是黑色 (规则第二点的要求 根节点必须是黑色)
② 只要不是空树 那么新插入的节点一定是红色节点
为什么非空树时候插入新节点不能是黑色呢
如果是黑色的话一定会违反规则的第四点处理起来很麻烦 而如果插入的是红色节点可能违反了 也可能没违反
接下来对非空树插入节点是红色的情况进行分析
①如果新节点的父节点是黑色的话 没有违反任何的规则 不需要做处理
②如果新节点的父节点是红色的话 违反了规则的第三条 需要对这种情况处理
接下来就分析一下对于②的情况怎么处理
新插入的节点的父节点是红色 那么它的父节点一定不是根节点(规则2 根节点一定为黑) 那么这个父节点一定还存在它的父节点且一定为黑色(规则3 不能连续两个红色节点)
如下图 新插入的节点x它的父节点6是红色的 那么6一定存在父节点且一定为黑色
这里处理方法是根据叔节点(新插入节点的爷节点的另一个孩子 对上图也就是15)分为两种情况来处理
①叔节点存在且为红
②叔节点不存在
此时只有这两种情况 没有叔节点存在且为黑的情况 如果存在这种情况就违反了规则第四点
对于①的情况 处理的方法为 变色
如下图 为了方便 c代表新插入的节点 p代表c节点的父节点 g代表c的爷节点 u代表c的叔节点
新插入x后此时 c和p都是红色违反了规则的第三点 所以我们要把p变为黑色 但是这样对于10的左路径就多了一个黑色节点 违反了规则第四点 所以我们还需要把g变红 u变黑 这样对于18的左路径在处理前后除了18之外都是只有一个黑色节点
所以对于①的情况处理方法为 把p和u节点变黑 把g变红
上面这种情况是没问题的 但是如果g的父节点也是红色呢 此时就又违反了规则第三点
所以要让此时的g节点作为c来重新分析 此时有两种情况 第一种和之前一样
①叔节点存在且为红
(2)叔节点存在且为黑
这里是没有叔节点存在且为红的情况 如果存在就违反了规则第四点
如下图 10之前是x的g节点为黑色 在处理之后变为右边的情况后 此时10作为c来看 此时它的u节点25就是黑色的
所以对于新节点为c的时候 只有①②的情况 对于非新节点为c来处理的话有①(2)两种情况
如果一直发生①的情况 一直到了根节点作为g变为了红色 如下图 3就是新插入的节点作为c 发生的是①的情况 处理方法为p和u变为黑 然后g变为红 但是此时10是根节点变红后违反了规则2
此时只需要要根节点重新变为黑色就可以了 (之前让g变色是因为g可能只是一个节点的左节点或者是右节点 那么我们需要保持这条路径在改变前后黑色节点的数量不变 如果g是根节点就没必要了 把它重新变为黑对于根的左右都增加了一个黑色节点不违反红黑树的规则)
接下来分析一下②的情况(u节点不存在)该如何处理
p同样必须变为黑色 但是这里无论怎么变色都不能解决
对于②的处理的方法是先对旋转点单旋后 p节点变黑 然后u节点变红
对于非新节点为c的(2)的情况(u存在且为黑)处理 处理方法和u不存在的处理一模一样
u不存在,则c⼀定是新增结点,u存在且为⿊,则 c⼀定不是新增
需要双旋+变色的情况
和AVL树那里一样
当p为g的左 c为p的右 或者是p为g的右 c为g的左时候需要双旋 先对p节点进行单旋后就变成需要单旋+变色的情况了 此时对g单旋 然后进行变色处理
对于插入所有情况总结之后如下图
代码实现
首先同样是和之前的AVL树一样 先插入这个节点 如果这个树是空树 新插入的就是根节点 为黑色 如果插入的是非根节点 那么这个节点是红色
然后根据上面的图进行编写代码就很顺利写出来了 虽然红黑树有很多的情况 但是如果真的弄清楚了总结一下 代码实现还是很简单的
bool insert(const pair<K, V>& kv){if (_root == nullptr) //对空树的处理{_root = new Node(kv);_root->_col = BLACK; //根节点一定是黑色return true;} Node* cur = _root;Node* curparent = cur;while (cur != nullptr) //直到为空了 就是要插入的位置{if (kv.first > cur->_kv.first) //要插入的数大于根的值就往右{curparent = cur; //cur里面存的是cur的上一个位置 cur改变之前先把它的位置存到curparent中cur = cur->_right;}else if (kv.first < cur->_kv.first) //小于根的值就往左{curparent = cur;cur = cur->_left;}elsereturn false; //不支持插入重复的元素 插入失败 返回false}//如果正常出了循环 那么此时cur的位置就是新插入节点的位置 那么此时为新节点开空间 然后让它的父节点指向它cur = new Node(kv);if (curparent->_kv.first < kv.first)curparent->_right = cur; //此时还需要判断 cur位置的节点是父节点的右孩子还是左孩子elsecurparent->_left = cur;cur->_parent = curparent; //处理节点的parent指针cur->_col = RED; //新插入的非根节点一定是红色的while (curparent&&curparent->_col == RED) //这样用while是针对u存在且为红的情况{ //通过处理这种情况 当grandparent为根节点再更新cur后此时的curparent为空所以需要判空Node* grandparent = curparent->_parent; //gNode* uncle = nullptr;if (curparent->_kv.first<grandparent->_kv.first) //uncle为g的另一个孩子节点{uncle = grandparent->_right;}else{uncle = grandparent->_left;}//开始根据不同的情况处理if(uncle && uncle->_col == RED) //叔叔节点存在且为红的处理{uncle->_col = curparent->_col = BLACK; //u和p变黑 g变红grandparent->_col = RED;_root->_col = BLACK; //可能grandparent就是根节点 在变色之后根节点变红了 此时要变回去 cur = grandparent; //让此时的grandparent为cur 并确立它的父节点 为了进行下一次的判断curparent = cur->_parent;}else //如果是第一次进循环 这里就是uncle节点不存在的情况 如果是第二次或更多次进的循环 这里就是uncle节点存在且为黑的情况{ //这两种情况处理的方法一样if (curparent == grandparent->_left) //p为g左的情况{if (cur==curparent->_right) // 先对curparent进行左单旋 // g g{ // p p uRotareL(curparent); //c c} // if里面的情况在处理后变为下面RotateR(grandparent); // g g} // p p uelse // p为g右的情况 c c{if (cur == curparent->_left) //先对curparent进行左单旋{RotateR(curparent);}RotateL(grandparent);}curparent->_col = BLACK; //最后统一变色grandparent->_col = RED;return true;}} //如果父节点不是红色 不需要处理 直接返回return true; //插入成功 返回true}
旋转的代码和AVL树那里一样 去掉平衡因子的处理就可以了
void RotateR(Node* parent){Node* subl = parent->_left;Node* sublR = subl->_right;parent->_left = sublR; //先将subl的右给了parent的左 subl->_right = parent; //然后parent变为subl的右if (sublR){sublR->_parent = parent;}Node* pparent = parent->_parent; //在改变parent的parent指针之前先存一下parent->_parent = subl; //还需要改变parent的parent指针//处理pparent的指向问题if (pparent) //pparent不为空就让pparent指向subl{if (pparent->_left == parent){pparent->_left = subl;}else{pparent->_right = subl;}}else //如果pparent为空 说明parent就是根节点 那么直接更新根节点为parent{_root = subl;}subl->_parent = pparent;}void RotateL(Node* parent){Node* subl = parent->_right;Node* sublL = subl->_left;parent->_right = sublL;subl->_left = parent;if (sublL){sublL->_parent = parent;}Node* pparent = parent->_parent;parent->_parent = subl;if (pparent){if (pparent->_left == parent){pparent->_left = subl;}elsepparent->_right = subl;}else{_root = subl;}subl->_parent = pparent;}
查找
查找和二叉搜索树和AVL一样 时间复杂度稳定在O(logN)
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;
}
红黑树的验证
在我们实现在后 我们怎么知道我们实现的是否正确呢
我们要检测我们实现的这棵树是否满足红黑树的四点规则
- 每个结点不是红色就是黑色
- 根结点是黑色的
- 如果⼀个结点是红色的,如果它的孩子存在则它的孩子结点必须是黑色的,也就是说任意⼀条路径不会有连续的红色结点。
- 对于任意⼀个结点,从该结点通过任意一条路径到空节点,均包含相同数量的黑色结点
第一点因为我们用到的是枚举体只会有红和黑两种颜色 不需要考虑这种情况
第二点的检查直接检查根的颜色就可以了
第三点我们可以用前序遍历的方式 如果用查孩子的方式比较麻烦---如果这个节点是红色需要先分别判断一下它两个孩子是否存在存在的话再看是否为红来判断
所以我们可以用和它父亲比较的方式来判断----每一个节点的父亲节点只有一个 判断这个节点是红色的后 只需要判断它的父节点是否为红色就可以了
第四点用前序遍历方式 遍历过程中用形参记录从根节点到当前结点的黑色结点数量 遇到黑色结点就++ 直到到了空节点此时就计算出了⼀条路径的黑色结点数量。再以任意⼀条路径的黑色结点数量作为参考值 将每条路径黑色节点数量于之比较
下面代码就检查了红黑树后三点的规则 有一条不满足就会返回0 如果返回1就代表我们实现的树满足红黑树的规则 那么就一定满足当前树中没有⼀条路径会比其他路径⻓出2倍
bool Check(Node* root, int blackNum, const int refNum)
{if (root == nullptr){// 前序遍历⾛到空时,意味着⼀条路径⾛完了if (refNum != blackNum) //判断这条路径的黑色节点和某一天路径的节点是否相同{ //检查第四点规则cout << " 存在黑色节点的数量不相等的路径 " << endl;return false;}return true;}// 检查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲就⽅便多了if (root->_col == RED && root->_parent->_col == RED) //检查第三点规则{cout << root->_kv.first << " 存在连续的红⾊结点 " << endl;return false;}if (root->_col == BLACK){blackNum++;}return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
}
bool IsBalance()
{if (_root == nullptr)return true;if (_root->_col == RED) //检查第二点规则return false;int refNum = 0; // 参考值Node* cur = _root;while (cur){if (cur->_col == BLACK) //遍历最左边路径 统计黑色节点个数{++refNum;}cur = cur->_left;}return Check(_root, 0, refNum);
}
测试用例可以用以下两种
RBTree<int, int> t;
// 常规的测试用例
/*int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };*/
// 特殊的带有双旋场景的测试用例
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };for (auto e : a)
{t.insert({ e, e });
}
第一次测试时候发现出错了 打印了存在黑色数量不相等路径的问题 调试之后发现下面的=都写成==了
改正之后这两组测试的结果都是正确的
RBTree.h完整代码
enum Colour
{RED,BLACK
};template <class K, class V>
struct RBTreeNode
{pair<K, V> _kv;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;Colour _col;RBTreeNode(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr){}
};
template<class K, class V>
class RBTree
{
public:typedef RBTreeNode<K, V> Node;bool insert(const pair<K, V>& kv){if (_root == nullptr) //对空树的处理{_root = new Node(kv);_root->_col = BLACK; //根节点一定是黑色return true;} Node* cur = _root;Node* curparent = cur;while (cur != nullptr) //直到为空了 就是要插入的位置{if (kv.first > cur->_kv.first) //要插入的数大于根的值就往右{curparent = cur; //cur里面存的是cur的上一个位置 cur改变之前先把它的位置存到curparent中cur = cur->_right;}else if (kv.first < cur->_kv.first) //小于根的值就往左{curparent = cur;cur = cur->_left;}elsereturn false; //不支持插入重复的元素 插入失败 返回false}//如果正常出了循环 那么此时cur的位置就是新插入节点的位置 那么此时为新节点开空间 然后让它的父节点指向它cur = new Node(kv);if (curparent->_kv.first < kv.first)curparent->_right = cur; //此时还需要判断 cur位置的节点是父节点的右孩子还是左孩子elsecurparent->_left = cur;cur->_parent = curparent; //处理节点的parent指针cur->_col = RED; //新插入的非根节点一定是红色的while (curparent&&curparent->_col == RED) //这样用while是针对u存在且为红的情况{ //通过处理这种情况 当grandparent为根节点再更新cur后此时的curparent为空所以需要判空Node* grandparent = curparent->_parent; //gNode* uncle = nullptr;if (curparent->_kv.first<grandparent->_kv.first) //uncle为g的另一个孩子节点{uncle = grandparent->_right;}else{uncle = grandparent->_left;}//开始根据不同的情况处理if(uncle && uncle->_col == RED) //叔叔节点存在且为红的处理{uncle->_col = curparent->_col = BLACK; //u和p变黑 g变红grandparent->_col = RED;_root->_col = BLACK; //可能grandparent就是根节点 在变色之后根节点变红了 此时要变回去 cur = grandparent; //让此时的grandparent为cur 并确立它的父节点 为了进行下一次的判断curparent = cur->_parent;}else //如果是第一次进循环 这里就是uncle节点不存在的情况 如果是第二次或更多次进的循环 这里就是uncle节点存在且为黑的情况{ //这两种情况处理的方法一样if (curparent == grandparent->_left) //p为g左的情况{if (cur==curparent->_right) // 先对curparent进行左单旋 // g g{ // p p uRotateL(curparent); //c cNode* m = cur; //curparent单旋之后 cur和curparent的位置交换了cur = curparent;curparent = m;} // if里面的情况在处理后变为下面RotateR(grandparent); // g g} // p p uelse // p为g右的情况 c c{if (cur == curparent->_left) //先对curparent进行左单旋{RotateR(curparent);Node* m = cur;cur = curparent;curparent = m;}RotateL(grandparent);}curparent->_col = BLACK; //最后统一变色grandparent->_col = RED;return true;}} //如果父节点不是红色 不需要处理 直接返回return true; //插入成功 返回true}void RotateR(Node* parent){Node* subl = parent->_left;Node* sublR = subl->_right;parent->_left = sublR; //先将subl的右给了parent的左 subl->_right = parent; //然后parent变为subl的右if (sublR){sublR->_parent = parent;}Node* pparent = parent->_parent; //在改变parent的parent指针之前先存一下parent->_parent = subl; //还需要改变parent的parent指针//处理pparent的指向问题if (pparent) //pparent不为空就让pparent指向subl{if (pparent->_left == parent){pparent->_left = subl;}else{pparent->_right = subl;}}else //如果pparent为空 说明parent就是根节点 那么直接更新根节点为parent{_root = subl;}subl->_parent = pparent;}void RotateL(Node* parent){Node* subl = parent->_right;Node* sublL = subl->_left;parent->_right = sublL;subl->_left = parent;if (sublL){sublL->_parent = parent;}Node* pparent = parent->_parent;parent->_parent = subl;if (pparent){if (pparent->_left == parent){pparent->_left = subl;}elsepparent->_right = subl;}else{_root = subl;}subl->_parent = pparent;}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;}bool Check(Node* root, int blackNum, const int refNum){if (root == nullptr){// 前序遍历⾛到空时,意味着⼀条路径⾛完了if (refNum != blackNum) //判断这条路径的黑色节点和某一天路径的节点是否相同{ //检查第四点规则cout << " 存在黑色节点的数量不相等的路径 " << endl;return false;}return true;}// 检查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲就⽅便多了if (root->_col == RED && root->_parent->_col == RED) //检查第三点规则{cout << root->_kv.first << " 存在连续的红⾊结点 " << endl;return false;}if (root->_col == BLACK){blackNum++;}return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);}bool IsBalance(){if (_root == nullptr)return true;if (_root->_col == RED) //检查第二点规则return false;int refNum = 0; // 参考值Node* cur = _root;while (cur){if (cur->_col == BLACK) //遍历最左边路径 统计黑色节点个数{++refNum;}cur = cur->_left;}return Check(_root, 0, refNum);}void Midbl() //中序遍历的形参类型需要为节点 但是我们创建的对象是BStree类型 且里面的root根节点为私有{ //所以 我们可以提供一个返回根节点的函数 或者像之前实现归并非递归那样做一层封装Midbl1(_root);}void Midbl1(Node* root) //中序遍历 先左再中再右 对搜索二叉树来说也就是从小到大的顺序打印{if (root == nullptr){return;}Midbl1(root->_left);cout << root->_kv.first << " ";Midbl1(root->_right);}
private:Node* _root = nullptr;
};