目录
1.红黑树的概念
红黑树其实还是一个二叉搜索树,不一样的是,红黑树多了一个颜色的概念
在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的二叉搜索树, 而AVLTree是严格平衡的二叉搜索树
红黑树的参考图:
2.红黑树的性质
-
每个结点不是红色就是黑色
-
根节点是黑色的
-
如果一个节点是红色的,则它的两个孩子结点是黑色的
-
对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点
-
每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍
为什么?
因为超过了就一定会破坏性质4:对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点
3.红黑树节点的定义
template<class K, class V>
struct RedBlackTreeNode
{
RedBlackTreeNode<K, V>* _left;
RedBlackTreeNode<K, V>* _right;
RedBlackTreeNode<K, V>* _parent; // 三叉链
pair<K, V> _kv; // 键值对
Colour _col; // 节点的颜色
RedBlackTreeNode(const pair<K, V> kv)
:_left(nullptr)
, _right(nullptr)
,_parent(nullptr)
,_kv(kv)
_col(RED)
{}
};
4.红黑树的插入
首先红黑树还是一个二叉搜索树,因此还是得先按照二叉搜索树的规则去插入,在插入的时候判断红黑树的规则是否被破坏了,破坏了就进行调整。从而得到红黑树
下面是先按照二叉搜索树插入的代码:
template<class K, class V>
class RedBlackTree
{
typedef RedBlackTreeNode<K, V> Node;
public:
// 库中的返回值是pair类型,后面模拟实现set和map再用pair,现在先用bool
bool Insert(const pair<K, V> kv)
{
// 红黑树首先是二叉搜索树,因此还得按照二叉搜索树的规则去插入,在插入的时候按照红黑树的规则再去修改
// 先判断树是不是空的
if (_root == nullptr)
{
_root = new Node(kv);
_root->_clo = BLACK; // 根节点一定是黑的
return true;
}
// 不是空的就要找到插入的位置
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
// 判断kv的K是比cur的K是小还是大,小往左走,大就往右走
if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
// 二叉搜素树不允许数据重复
return false;
}
}
// 找到了要插入的位置的父亲节点
// 判断要插入的是parent的左边还是右边
Node* newnode = new Node(kv);
if (kv.first < parent->_kv.first)
{
parent->_left = newnode;
newnode->_parent = parent;
}
else
{
parent->_right = newnode;
newnode->_parent = parent;
}
//判断新增节点该是红的还是黑的
newnode->_clo = RED; // 红的好处理,黑的就不好处理了,黑的会影响每个路径的黑节点都是一样的这个规则
// 插入红的节点,需要判断是否合法。这里会有多种情况,需要一一分析
return ture;
}
private:
Node* _root = nullptr;
};
检测新节点插入后,红黑树的性质是否造到破坏
因为新节点的默认颜色是红色,因此:如果其父亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的父亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:
插入的时候红黑树会有多种情况,因此需要对多种情况进行分析,才能写出代码
第一种情况:
插入的节点cur默认为红,p为红、g为黑、u存在且为红
这个情况的处理思路如下图所示:
总结:
- 默认插入节点是红色节点,因此需要判断其父亲节点是否是红节点。黑的不处理,红的就处理
- 处理的时候判断u是否存在且是否为红,若u为红,则将p、u一起变黑,将g变红;
- 变完之后还得判断g是否为根节点。
- 如果是g是根节点,则将其改为黑即可
- 如果g不是根节点。说明这只是一个子树,那么就要判断g的父亲节点是黑的还是红的。如果是黑的,就说明合法,停止更新。如果是红的,就将g的位置当成cur再次更新、因为此时的g就相当于插入了一个红节点
第5点的:如果是红的,就将g的位置当成cur再次更新、因为此时的g就相当于插入了一个红节点
这个是怎么理解呢?看下面这图:
需要注意的是:
只有第一种情况是这样的,第二种情况就不是这样了。这里不涉及旋转,自然不在意cur的位置,但是情况2、3涉及旋转就不能不管cur插入的位置了
上面是第一种情况的一种具体的情况,下面这个图是第一种情况的具象图:
三角形代表子树
第二种情况:
插入的节点cur默认为红,p为红、g为黑、u为黑\u不存在
这里u为黑和u不存在是两种情况
注意:
这里u为黑情况的cur是怎么变红的,其实就是第一种情况的其中一种处理方式
但是这两种情况的处理方式都是一样的,因此我们归类为第二种情况
处理方法都是单旋+变色
p为g的左孩子,cur为p的左孩子,则进行右单旋;
相反,p为g的右孩子,cur为p的右孩子,则进行左单旋
p、g变色–p变黑,g变红
过程如下图所示:
注意:
上图的情况是用右单旋,但是旋转是具体情况具体分析的,可能会是左单旋也可能是右单旋
要注意图片里的只是一种具体的情况,树可能是很复杂的,但是将问题拆分出来都是这样处理的。
下面是第二种情况的具象图:
总结:
- 插入节点cur(也可能是下面子树更新导致cur变红)默认是红节点,此时判断父亲p是黑还是红,如果是红就要调整
- 判断u是否存在,如果存在且为红那就是第一种情况处理。如果不存在就第二种情况处理。此时不可能存在u为黑的情况。因为cur是插入节点
- 如果第一种情况处理完,还要向上处理,即g的父亲是红,此时可能会碰到u是黑的情况,也是第二种情况处理。单旋+变色
注意:
这里判断是第二种情况还是第三种情况是通过判断cur是p的左孩子还是右孩子判断的。
左孩子就是第二种情况,单旋即可
右孩子就是第三种情况,需要双旋
第三种情况:
第三种情况是第二种情况的变种。
仍然是插入的节点cur默认为红,p为红、g为黑、u为黑\u不存在
但是cur的位置不一样了。
这里u为黑和u不存在是两种情况,但是处理方法是一样的,因此归类为第三种情况
处理方法都是双旋+变色
p为g的左孩子,cur为p的右孩子,则进行左右双旋;
相反,p为g的右孩子,cur为p的左孩子,则进行右左双旋
g、cur变色–cur变黑,g变红
第三张情况和第二种情况的区别就是cur插入的位置不一样
采取双旋+变色的解决方法
解决过程的图:
注意:
这里可能不只是左右双旋,也可能是右左双旋,具体情况具体分析
上面只是一个具体情况,实际调整的红黑树可能会非常复杂。
下面是具象图:
总结:
- 插入节点cur(也可能是下面子树更新导致cur变红)默认是红节点,这个时候判断父亲是否为红
- 如果为红,就判断u是什么情况,第三种情况的u是不存在或者u为黑。
- 第三种情况,cur是p的右孩子,这个时候就采取双旋+变色
注意:
这里判断是第二种情况还是第三种情况是通过判断cur是p的左孩子还是右孩子判断的。
左孩子就是第二种情况,单旋即可
右孩子就是第三种情况,需要双旋
代码实现:
插入的过程可能会用到旋转。旋转的代码和AVLTree的是一样的。
旋转代码:
// 左单旋
void RotateL(Node* parent)
{
// 这里要结合博客和笔记来理解旋转的过程才好理解代码
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 左单旋 【将subR的左孩子变成parent的右孩子,parent变成subR的左孩子】
parent->_right = subRL;
subR->_left = parent;
Node* ppNode = parent->_parent; // 防止后面subR链接不到parent->_parent,这里要做个保存
// 处理父亲指针的关系
parent->_parent = subR;
if (subRL) // 防止subR的左孩子是空的情况
subRL->_parent = parent;
// subR的父亲指针也需要变化
// 这里会有两种情况
// 1.左单旋之后,subR直接是根节点
if (parent == _root)
{
// 原本parent是根节点,左单旋之后subR变成跟节点
_root = subR;
subR->_parent = nullptr;
return;
}
else //2.parent不是根节点,左单旋完毕之后subR仍然是子树
{
// 这个情况说明原本parent是一个子树,左单旋后subR也是一个子树
// 这个时候就要链接上原本parent的_parent,上面存储起来是ppNode
// 但是这个时候还得判断左单旋之后的subR应该是ppNode的左子树还是右子树
if (parent == ppNode->_left)
{
subR->_parent = ppNode;
ppNode->_left = subR;
}
else
{
subR->_parent = ppNode;
ppNode->_right = subR;
}
}
}
// 右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//旋转的时候要注意处理节点之间的关系
//右单旋【subL的右孩子变成parent的左孩子,parent变成subL的右孩子】
parent->_left = subLR;
subL->_right = parent;
Node* ppNode = parent->_parent; // 防止后面subR链接不到parent->_parent,这里要做个保存
// 处理父亲节点之间的关系
parent->_parent = subL;
if (subLR) // 防止subL的右孩子是空的情况
subLR->_parent = parent;
// subL的父亲指针也需要变化,其父亲节点与subL的关系也要连接
// 这里会有两种情况
// 1.右单旋之后,subL直接是根节点
if (_root == parent) // 旋转之前的parent是根节点
{
subL->_parent = nullptr;
_root = subL;
return;
}
else //2.parent是原先的一个子树,左单旋完毕之后subL仍然是子树
{
// 判断原先的parent是其父节点ppNode的左子树还是右子树
if (parent == ppNode->_left)
{
// subL替代之后,是ppNode的左子树
ppNode->_left = subL;
subL->_parent = ppNode; //处理父亲指针
}
else
{
// subL替代之后,是ppNode的右子树
ppNode->_right = subL;
subL ->_parent = ppNode; //处理父亲指针
}
}
}
红黑树的插入接口的实现代码:
// 库中的返回值是pair类型,后面模拟实现set和map再用pair,现在先用bool
bool Insert(const pair<K, V> kv)
{
// 红黑树首先是二叉搜索树,因此还得按照二叉搜索树的规则去插入,在插入的时候按照红黑树的规则再去修改
// 先判断树是不是空的
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK; // 根节点一定是黑的
return true;
}
// 不是空的就要找到插入的位置
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
// 判断kv的K是比cur的K是小还是大,小往左走,大就往右走
if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
// 二叉搜素树不允许数据重复
return false;
}
}
// 出循环,cur为nullptr。
// parent节点就是cur要插入的位置的父节点
// 判断要插入的是parent的左边还是右边
cur = new Node(kv);
if (kv.first < parent->_kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//判断新增节点该是红的还是黑的
cur->_col = RED; // 红的好处理,黑的就不好处理了,黑的会影响每个路径的黑节点都是一样的这个规则
// 插入红的节点,需要判断是否合法。
// 先判断插入节点的父亲是否为红。红才需要调整,黑色就不需要调整了
while (parent && parent->_col == RED) // 前面的parent是为了防止父亲不存在了,不然对nullptr解引用直接崩溃
{
Node* grandfather = parent->_parent; //进这个循环,g就一定存在
// 先讨论左侧情况
if (grandfather->_left == parent)
{
Node* uncle = grandfather->_right;
// 这里会有3种情况,需要一一分析 【结合博客和笔记理解(比如下面出现的p、q、u)】
// 1.cur为红,p为红,g为黑,u存在且为红
if (uncle && uncle->_col == RED)
{
// 只需要变色,不需要旋转
grandfather->_col = RED; // 祖先变红
// 父亲和叔叔变黑
parent->_col = BLACK;
uncle->_col = BLACK;
// 判断祖先节点g是否为根
if (_root == grandfather)
{
// 直接让g变黑就完成了。停止更新,退出循环
grandfather->_col = BLACK;
return true;
}
// 判断 g的父亲是否为黑的
if(grandfather->_parent && grandfather->_parent->_col == BLACK)
{
// 直接让g变黑就完成了。停止更新,退出循环
grandfather->_col = BLACK;
return true;
}
// 走到这里说明g的父亲节点是红的,要继续向上调整
cur = grandfather;
parent = cur->_parent;
}
else //u不存在或者u为黑 都进入该分支
{
// 走到这里根据cur的位置,可以分成第二种和第三种情况
// 但是第三种情况可以转化为第二种情况,因此我们先判断是不是第三种情况,是的话就转换
// 然后后面统一按第二种情况来处理
// 3.cur是p的右孩子,p是g的左孩子 ,p为红,g为黑、u不存在/u为黑
// 第三种情况就是双旋+变色
if (cur == parent->_right)
{
// p是g的左孩子
// 因此是左右双旋 + 变色处理
// 这里先左单旋,然后交换cur和p(不是交换节点,只是将cur和p这两个指针变量交换)
// 这样转化为第二种情况处理,即右单旋+变色即可
RotateL(parent); // 左单旋
swap(parent, cur); // 转化为第二种情况
}
// 要注意这里第二种情况可能是第三种情况转化来的
// 2.cur是p的左孩子,p是g的左孩子,p为红,g为黑、u不存在/u为黑
// 第二种情况就是单旋+变色
RotateR(grandfather); // 右单旋
grandfather->_col = RED;
parent->_col = BLACK; // 原先是cur变黑。但是上面指针变量交换了
// 处理完这里可以直接退出循环
// 因为旋转完之后,一定符合红黑树的规则,
// 即便这里处理的是一个子树,子树符合规则了,整棵树也就符合了红黑树的规则
break;
}
}
else // parent位于g的右孩子
{
Node* uncle = grandfather->_left;
// 这里会有3种情况,需要一一分析 【结合博客和笔记理解(比如下面出现的p、q、u)】
// 1.cur为红,p为红,g为黑,u存在且为红
if (uncle && uncle->_col == RED)
{
// 只变色不旋转【g变红,p和u变黑】
grandfather->_col = RED;
uncle->_col = parent->_col = BLACK;
// 判断祖先节点g是否为根
if (_root == grandfather)
{
// 直接让g变黑就完成了。停止更新,退出循环
grandfather->_col = BLACK;
return true;
}
// 判断 g的父亲是否为黑的
if (grandfather->_parent && grandfather->_parent->_col == BLACK)
{
// g父亲为黑,就不用调整了,g就为红即可
return true;
}
// 走到这里说明g的父节点是红的,继续向上调整
cur = grandfather;
parent = grandfather->_parent;
}
else // u不存在或者u为黑
{
// 2.cur是p的右孩子,p是g的右孩子 ,p为红,g为黑、u不存在/u为黑
// 第二种情况的解决方法就是左单旋+变色
// 3.cur是p的左孩子,p是g的右孩子 ,p为红,g为黑、u不存在/u为黑
// 第二种情况的解决方法就是右左双旋+变色
// 走到这里根据cur的位置,可以分成第二种和第三种情况
// 但是第三种情况可以转化为第二种情况,因此我们先判断是不是第三种情况,是的话就转换
// 然后后面统一按第二种情况来处理
if (parent->_left == cur)
{
// 右单旋 然后交换cur和parent的位置,转化为第二种情况
RotateR(parent);
swap(parent, cur); //(不是交换节点,只是将cur和p这两个指针变量交换)
}
// 统一按第二种情况处理 【这里的第二种情况可能是第三种转化来的】
// 左单旋 + 变色
RotateL(grandfather);
parent->_col = BLACK; // 原先是cur变黑。但是上面指针变量交换了
grandfather->_col = RED;
// 旋转之后就可以直接退出而不考虑上面的情况了。
// 因为旋转完之后,一定符合红黑树的规则,
// 即便这里处理的是一个子树,子树符合规则了,整棵树也就符合了红黑树的规则
break;
}
}
}
// 插入完毕,返回true
return true;
}
这个插入代码的实现中,需要双旋的第三种情况,我将其拆分为两个单旋,即一个单旋后,交换指针变量达到第二种情况,按照第二种情况处理、这样不需要写双旋代码了。
如下图所示:
这样后面进行变色处理的时候要记得指针变量交换过的问题。
5.红黑树的删除
删除看这个博客的分析
红黑树 - Never - 博客园 (cnblogs.com)
6.红黑树的查找
这里查找就跟之前的二叉搜索树一样查找就行了
代码如下:
// 红黑树的查找
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
// 判断key比cur的key大还是小。大往右边走,小往左边走
if (cur->_kv.first < key)
{
cur = cur->_right;
}
else if(cur->_kv.first > key)
{
cur = cur->_left;
}
else
{
// 走到这里key相同,找到了,那就返回
return cur;
}
}
// 走到这里就是没找到
return nullptr;
}
7.红黑树的验证
要验证红黑树分两步:
- 验证是二叉搜索树【中序遍历是有序的】
通过中序遍历来判断是否为二叉搜索树
//中序遍历
void _InOrder(Node* root)
{
if (root == nullptr)
return;
// 中序遍历——左子树、根、右子树
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
//cpp中一般实现递归都要通过子函数
// 因为外边调用这个中序遍历接口的时候没办法直接传一个_root进来,_root是私有的
void InOrder()
{
_InOrder(_root);
//_InOrder(this->_root); // 等价于上面的
cout << endl;
}
- 验证是否符合红黑树的规则
要检查三个规则:
- 根节点必须是黑的
- 不能出现连续两个的红节点
- 每个路径的黑节点的个数都必须相同
代码如下:
bool _IsValidRBTree(Node* root, size_t k, size_t blackCount)
{
// 判断当前根节点走到空后,k和blackCount是否相等
if (root == nullptr)
{
if (k == blackCount)
{
return true;
}
else
{
cout << "违反红黑树规则:每个路径的黑色节点个数都要相同\n";
return false;
}
}
// 根节点没有走到空就继续统计当前路径黑色节点出现的次数
if (root->_col == BLACK)
k++;
// 判断当前节点与父节点是否都是红的
Node* parent = root->_parent;
if (parent && parent->_col == RED && root->_col == RED) //前面的parent防止第一次进来是根节点,parent是不存在的
{
cout << "违反红黑树规则:不允许有两个连续红节点\n";
return false;
}
// 递归判断每个路径的黑节点个数是否相同 以及路径中是否存在两个连续红节点
return _IsValidRBTree(root->_left, k, blackCount) && _IsValidRBTree(root->_right, k, blackCount);
}
// 判断是否为真的红黑树 【先得是二叉搜索树,然后再判断是不是红黑树】
bool IsValidRBTree()
{
// 空树也是红黑树
if (_root == nullptr)
return true;
// 不是空树判断该树是否符合红黑树的性质
if (_root->_col == RED)
{
cout << "违反红黑树规则:根节点必须为黑\n";
return false;
}
// 接下来验证每个路径的黑色节点个数都是一样的
// 先随机获取一条路径的黑节点个数
Node* cur = _root;
size_t blackCount = 0;
while (cur)
{
if (cur->_col == BLACK)
blackCount++;
cur = cur->_left; // 获取最左路径的黑节点个数
}
// 拿到了一条路径的黑色节点,验证每条路径的黑节点是否都是一样的、
size_t k = 0; // 用k记录每个路径的黑节点个数
return _IsValidRBTree(_root, k, blackCount); // 这里面还会判断是否存在连续2个红节点存在
}
验证代码:
void TestRBTree() { int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 }; int b[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 }; RedBlackTree<int, int> Rbtree; for (auto& e : a) { Rbtree.Insert(make_pair(e, e)); //Rbtree.Insert(pair<int, int>(e, e)); // 等价于上面 } Rbtree.InOrder(); bool ret = Rbtree.IsValidRBTree(); if (ret == 1) { cout << "该树是红黑树\n"; } RedBlackTree<int, int> Rbtree2; for (auto& e : b) { Rbtree2.Insert(make_pair(e, e)); } Rbtree2.InOrder(); bool ret2 = Rbtree.IsValidRBTree(); if (ret2 == 1) { cout << "该树是红黑树\n"; } }
验证后的结果:
8.红黑树的应用
红黑树的应用非常的广泛
-
C++ STL库-- map/set、mutil_map/mutil_set
-
Java 库
-
linux内核
-
其他一些库
9.红黑树和AVL树的效率对比
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追
求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,
所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红
黑树更多
我这里的思路是生成N个随机数,然后放到一个vector中,然后通过范围for循环,取出vector的随机数,插入红黑树和AVLtree里面,看看插入的时间耗费了多少。
代码如下:
// 测试一下AVLTree和RedBlackTree的效率
void Compare()
{
// 分别向两种树中插入10000个数字,看看效率
int N = 1000000;
vector<int> v;
v.reserve(N); // 先开辟N个空间
srand((unsigned int)time(nullptr)); // 给个种子
// 先把随机数塞到这个vector中,插入树的时候从vector取
for (int i = 0; i < N; i++)
{
v.push_back(rand());
}
RedBlackTree<int, int> Rbtree;
AVLTree<int, int> Avltree;
// 统计插入N个数 AVLTree需要多少时间
int begin1 = clock();
for (auto e : v)
{
Avltree.Insert(make_pair(e, e));
}
int end1 = clock();
cout << "Avltree是否为正确的AVLTree:" << Avltree.IsBanlance() << endl;
// 统计插入N个数 RedBlackTree需要多少时间
int begin2 = clock();
for (auto e : v)
{
Rbtree.Insert(make_pair(e, e));
}
int end2 = clock();
cout << "Rbtree是否为正确的RedBlackTree:" << Rbtree.IsValidRBTree() << endl;
cout << "AVLTree耗费的时间:" << end1 - begin1 << "ms" << endl;
cout << "RedBlackTree耗费的时间:" << end2 - begin2 << "ms" << endl;
}
当N = 100000时
当N = 1000000时