1. 了解红黑树
1.1. 概念
红黑树,是一种二叉搜索树,但在每个节点增加一个存储位表示节点的颜色,可以是红色,或是黑色,通过对任何一条从根到叶子的路径上各个节点的着色方式进行限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡。
都是搜索二叉树,红黑树和AVL树有什么区别?
AVL 树:左右高度差不超过1(严格平衡)
红黑树:最长路径不超过最短路径的两倍(近似平衡)
这样看起来,似乎 AVL树更优秀,严格平衡
如果相同的数据,AVL 树可能是 18层,但是 红黑树有可能是 18 - 36 层
在查找方面 AVL 树可能会比红黑树优秀一点,但是只是一点。
因为18 层的数据,2^17 ==131072,18层数据就有 这么多节点,查找 18 次和 查找 35次,对计算机来说区别很小。
同时还要注意,AVL树的严格平衡,因为严格平衡,所以 AVL树为了保证平衡,需要不停的旋转,因此在插入删除上,效率会慢很多
但是红黑树并不是严格平衡,所以在调整上,不会进行那么多次的调整,因此插入和删除的时间就会少很多。
1.2. 性质
先看下面一棵树
红黑树性质:
- 每个节点不是红色就是黑色。
- 根节点是黑色的。
- 如果一个节点是红色的,则它的两个节点都是黑色的(不能出现连续的红色节点,可以出现的组合:黑+黑, 黑+红, 红+黑)。
- 对于每个节点,从该节点到其所有后代中,均包含数目相同的黑色节点(每条路径都包含相同数量的黑色节点)。
- 每个叶子节点(NIL节点)都是黑色的。
为什么满足上面的性质,红黑树就能保证:最长路径中节点个数不会超过最短路径节点个数的两倍?
最短路径:全黑
最长路径:一黑一红间隔
假设最短上有 N 个节点
其他路径上的节点数量都在 [N, 2N] 之间
每条路径上黑色节点数量相同,最长的情况下是一红一黑间隔,同一条路径下,红节点数量和黑节点数量相同,也就是2N
NIL 节点(叶子节点的空节点)
比如这棵树,这里看起来有 4 条理解,其实有 8 条
NIL 节点也要被算入路径内
路径是从跟节点走到空,才算路径。
这棵树也算红黑树,这棵树有 7 条路径。
这棵树,我们看起来好像是一棵红黑树,但是当我们把所有的 NIL 节点全部标出来
这样就能清楚的看到,标记位置的右子树只含有一个黑色节点,但是左子树的每条路径含有 两个黑色节点,所以 这棵树并不是红黑树
2. 模拟实现
2.1. 节点
enum Colour
{
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode
{
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
pair<K, V> _kv;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
{}
};
这里的节点和 AVL 树类似,不过 红黑树 是用颜色来控制的,所以这里我们添加了 _col 来控制。
不够在这里的 构造函数,我们还不能确定 _col 初始化成什么样子,我们后面再看。
2.2. insert
首先 红黑树 还是搜索二叉树,所以插入的基本逻辑还是那套,我们先实现这部分,后面主要分析调整的地方。
template<class k, class V>
class RBTree
{
public:
typedef RBTreeNode<K, V> Node;
RBTree()
:_root(nullptr)
{}
bool insert(const pair<K, V>* kv)
{
if(_root == nullptr)
{
_root = new Node;
return true;
}
Node* cur = _root;
Node* parent = nullptr;
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(cur);
if(parent->_kv.first < kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
return true;
}
private:
Node* _root;
};
二叉树的插入,这段没什么好说的,接下来看调整。
如果是插入节点,最开始应该插入什么颜色的节点最好?
我们都分析一下
如果我们在上面这棵树上插入节点,插入黑色节点后,我们发现,这条路径上的黑色节点数量发生变化,所以这里必须调整。
如果插入的是红色节点,每条路径上的黑色节点数量并未发生变化,这里可以不需要调整。
所以我们最好把 插入的节点初始化为 红色节点
但这不代表插入红色节点完全不需要修改,插入红色节点后,主要影响的是父节点。
这里插入分为 3 类型的情况:
- 如果新增的节点是 黑色节点,会影响所有父亲节点下面的路径。
- 如果新增的节点是 红色节点,父亲节点是 黑色节点,不用影响。
- 如果新增的节点是 红色节点, 父亲节点是 红色节点, 需要进行调整。
我们需要处理的情况是第 3 种。
如果出现这种情况,我们首先考虑把父亲变色。
如果仅靠变色不能解决问题,就需要通过 旋转 + 变色。
第一步,7 变 黑,因为 7 的父节点一定是 黑色,所以还要处理 父节点(7原本是红色,红色节点的父亲只能是黑色,7 所在的路径上多了一个黑色节点,所以还是要从 7 的父亲节点入手处理)。
第二步,7 的 叔叔节点 5 变 黑,但是又会导致 6 的两条路径上分别多出来一个 黑色节点,所以我们还需要把 6 变 红,从而使每条路径黑色节点数量想同。
这里调整自己路径后,还要通过自己的根去调整另一条路径,我们这里默认的情况是 叔叔节点 存在 且 和 7 都是红色。
那么叔叔不存在的情况,或者叔叔颜色原本为黑呢?
叔叔 不存在时:
这两棵树,如果是前面学过 AVL 树的人,应该能一眼看出来,这里是 右左双旋 和 右右左单旋。
旋转 + 变色 处理。
当然,这是基于 叔叔节点不存在的情况,叔叔节点存在且为黑的情况还要讨论,现在我们慢慢分析怎么用代码实现这些操作
2.2.1. 调整
我们先把需要调整的情况主要分为三种
先确定节点
插入的节点: cur
插入节点的父节点: parent - p
插入节点的父节点的父节点: grandfather - g
叔叔节点: uncle - u
a,b,c,d,e 子树(可能为空,也可能是一整颗树)
- cur 为红,p 为红,g 为黑, u存在且为红
- cur 为红,p为红, g 为黑, u不存在
- cur 为红,p为红, g 为黑, u存在且为黑
首先是 第一种 情况:=
cur 为红,p 为红,g 为黑, u存在且为红
解决方法:将 p ,u 改为黑色, g 改为红色, 然后把 g 当做 cur,继续向上处理
但是简简单单变个色就行了吗?
这里还是有特殊情况需要处理的
- 如果 g 是 根节点
- 如果 g 的父节点是 红色
第一种情况很简单,直接把 _root 变黑色就行了。
毕竟不能出现连续的红色节点,但是可以出现连续的黑色节点,所以这样变完全没有问题
那第二种呢,修改后,g 变成了 红色,但是如果 g 是一棵树的子树,同时 g 的父亲节点是 红色呢?
我们的子树是没有问题了,所以我们把 原树的 g 当做新的 cur 向上调整。
如果 修改后的树 的父亲节点是黑色,那么此时直接退出即可(原本g为根节点的树每条路径是 一个 黑色节点,修改后每条路径仍是一个黑色节点,对 g 以上的根节点来说,每天路径上的黑色节点数量没有发生改变,所以不需要向上调整)
这里我们像 AVL 树那样分析分析,是不是 只需要改变这 4 个节点才能完成操作?
还是这棵树,此时 a,b,c,d,e 的情况有点复杂
如果 a/b/c/d/e 都是 空,cur 就是新增节点。
如果 a/b 不是空节点,但是想要触发这个调整,必须在 a,b任意位置插入节点就会破坏规则
至于 c,d,e 可能是下面的任意一种情况
c,d,e 是每条路径上含有一个黑色节点的红黑树。
a,b 作为之前新插入的节点。
在 a,b 下任意位置插入节点都会破坏规则。
所以这里可能存在的树 (4x4x4)x4 = 256种
不管再怎么复杂,对应的子树都是经过调整后的 红黑树,或者说下面调整玩完成后向上调整到这个位置,所以我们只需要关注这里的 cur 就行。
第二种情况:
cur 为红,p为红, g 为黑, u不存在
如果遇到单纯变色不能解决问题的情况,这里就需要先旋转后变色。
解决方法:左左右单旋,旋转后,g变红, p变黑
这里一般没有什么特殊情况,最后根节点也处理成黑色,不会和父节点冲突
第三种情况:
cur 为红,p为红, g 为黑, u为黑
这种情况会出现吗,这里 u 可能是上一次调整后的结构,g 可能是根节点,所以会出现这种情况。
这里的处理方法,还是先进行旋转。
但是还是要看 cur 的位置
p 为 g 的左孩子,cur 为 p 的左孩子,进行右单旋
p 为 g 的右孩子,cur 为 p 的右孩子,进行左单旋
p 为 g 的左孩子,cur 为 p 的右孩子,进行左右双旋
p 为 g 的右孩子,cur 为 p 的左孩子,进行右左双旋
这里默认 a,b,c 都是含有一个黑色节点的红黑树
图不好画,大概按这样的过程理解
旋转完成后,还要变色,主要的变色逻辑就是上面展示的。
单旋:p 变为 黑,g 变为 红色
双旋:g 变为红, cur 变为黑
旋转的大概逻辑了解以后,下面开始实现
while(parent && parent->_parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
if(parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
if(uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if(cur == parent->_left)
{
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else
{
Node* uncle = grandfather->_left;
if(uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandfather-_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if(cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col =RED;
}
}
}
_root->_col = BLACK;
}
左旋和右旋还是 AVL 树那套,但是要注意,这里我们在 insert 里修改节点颜色,所以左旋右旋里不需要修改颜色,同时 也不需要我们去单独写函数实现 左右双旋和右左双旋来修改颜色。
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
subR->_left = parent;
Node* parentParent = parent->_parent;
parent->_parent = subR;
if (subRL)
{
subRL->_parent = parent;
}
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
}
2.2.2. 插入全代码
bool insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
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);
cur->_col = RED;
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
while (parent && parent->_parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
subR->_left = parent;
Node* parentParent = parent->_parent;
parent->_parent = subR;
if (subRL)
{
subRL->_parent = parent;
}
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
}
2.3. check
现在插入的大概逻辑是没问题了,但是和 AVL 树的问题一样,怎么样证明我们的树是 红黑树?
树是不是红黑树,我们需要根据红黑树的性质来判断
第 1 点,第 2 点,第 5 点都很好检查。
第 3 点相对来说就麻烦了
如果是红色节点,检查子节点是不是都是黑的,但是也不好检查,红色节点的孩子节点可能是 两个,可能是 一个,也可能不存在,所以我们可以考虑不向下查找,向上查找
bool Check(Node* root)
{
if(root == nullptr)
{
return true;
}
if(root->_col == RED)
{
}
return Check(root->_left) && Check(root->_right);
}
bool IsBalance(Node* root)
{
if(root->_col ==BLACK)
{
return true;
}
if(root->_col == RED)
{
return false;
}
return Check(root);
}
IsBalance 检查根节点是不是黑色,Check 检查每个红色节点是不是有两个黑色子节点,如果直接检查红色节点的子节点情况,有点麻烦,所以我们反向检查
if(root->_col == RED && root->_parent->_col == RED)
{
cout << "有连续的红色节点" << endl;
return false;
}
这里的判断条件可以这样写,当前节点为红色,且这个节点的父节点是红色,就返回 false;
红色节点一定有父亲,且红色节点的父亲一定是黑色。
接下来处理,每条路径上的黑色节点数量相同,该怎么操作?
初步想法,拿栈可以检查,黑色节点入栈,当需要出栈时检测一下数量即可。
除了这个方法,我们还有其他方法
isBalance 传入 blacknum ,当遇见黑色节点++,直到遇见空节点时输出 blacknum 。
bool Check(Node* root, int blacknum)
{
if(root == nullptr)
{
cout << blacknum << " ";
}
if(root->_col == RED & root->_parent->_col == RED)
{
cout << "有连续的红色节点" << endl;
}
if(root->_col == BLACK)
{
++blacknum;
}
return Check(root->_left, blacknum) && Check(root->_right, blacknum);
}
bool IsBalance()
{
return _IsBalance(_root);
}
private:
bool _IsBalance(Node* root)
{
if(root->_col == RED)
{
return false;
}
int blacknum = 0;
return Check(root, blacknum);
}
这里我们看见,所有路径的黑色节点数量都是 2
但是数据量太少了,我们需要和 AVL 树一样,传入大量数据来测试我们的 红黑树
void test_RBT ree2()
{
const size_t N = 10000;
vector<int> v1;
RBTree<int, int> t1;
srand(time(0));
for(int i = 0; i < N; i++)
{
v1.push_back(rand());
}
for(auto : v1)
{
t1.insert(make_pair(e, e));
}
cout << t1.IsBalance() << endl;
}
当我们输出所有路径的黑色节点数量时,发现,这数据有点多,我们看不过来,不方便检查。
这里我们的思路是,除了传入 blacknum, 我们还需要传入一个判断的值
bool Check(Node* root, int blacknum, const int refVal)
{
if (root == nullptr)
{
if (blacknum != refVal)
{
cout << "存在黑色节点数量不相同的路径" << endl;
return false;
}
return true;
}
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "有连续的红色节点" << endl;
return false;
}
if (root->_col == BLACK)
{
++blacknum;
}
return Check(root->_left, blacknum) && Check(root->_right, blacknum);
}
bool IsBalance()
{
return _IsBalance(_root);
}
private:
bool _IsBalance(Node* root)
{
if (root->_col == RED)
{
return false;
}
int blacknum = 0;
int refVal = 0;
Node* cur = root;
while (cur)
{
if (cur->_col = BLACK)
{
refVal++;
}
cur = cur->_left;
}
return Check(root, blacknum, refVal);
}
这里我们看见,在debug 版本下,插入和检查的时间一共用了 309ms,非常快
release 版本下,用了 119ms
100w 的数据一共是 18 层
注:这里看起来和 AVL 树高度差不多,其实是因为 rand 是伪随机,给的数值在量很大的情况下,会出现重复,所以表面上我们插入了 100w 个数据,实际上数据早就饱和了
2.4. erase
删除节点
删除的情况比插入的情况多很多,和AVL树一样,我们还是简单说一下,不实现
如果我们要删除 红色节点
这没啥影响,那5条规则没有受到破坏
如果删除的黑色节点
删除 黑色节点,我们发现,这个节点所在的路径黑色节点减少了,这里仅靠变色无法解决
25变红,17变黑?但是这样会导致出现连续的红色节点。
最右边的两条路径不管怎么样都会含有一个黑色节点,但是左边这条路径不存在黑色节点,所以需要旋转解决,这里还好看一点,右边整体高,右右左单旋。
删除时,如果出现问题,和插入一样,优先考虑 变色,后考虑旋转,但是旋转一定要放在 变色前。
删除的情况比较多,这里就不细说了