红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。而AVL树是左右高度差的绝对值不超过1,它是一种绝对平衡,而红黑树是相对平衡。
红黑树的性质
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
有了这些规则也就意味着,红黑树的最短路径是全部都是黑色节点,最长路径就是一黑一红交替出现,因为每条路径都拥有相同给的黑色节点数量,假设每条路径的黑色节点数量为N,那所有路径的节点数量就在N ~ 2*N之间,但是最短路径和最长路径不一定存在。
红黑树实现
红黑树节点的定义
因为每个节点都存在颜色,并且不为黑色就为红色,所以我们可以定义一个表示颜色的枚举常量。红黑树依旧是三叉链。
enum Colour
{
RED,
BLACK
};
template <class K, class V>
struct RBTreeNode
{
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Colour _col;
pair<K, V> _kv;
RBTreeNode(const pair<K,V>& kv)
: _kv(kv)
, _col(RED)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
};
template <class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
bool Insert(const pair<K, V>& kv);
private:
Node* _root = nullptr;
};
红黑树的实现
插入
插入刚开始依旧是搜索二叉树的规则,如果插入的值比当前节点的大就往右边走,比当前节点的值小,就往左边走,一直走到空。
思考
如果走到空了以后,我们插入了这个节点,那么这个节点的颜色给什么好呢?
如果我们给黑色的话,我们会违反规则4,因为在插入前,我们这棵树就是红黑树,所以每条路径的黑色节点数量是相同的,如果我们插入了黑色节点,会导致所有路径的黑色节点的数量不一样,我们需要修改所有的路径代价有点大,如果我们插入红色节点的话,如果插入后父亲是黑色的,就可以插入结束了,如果是红色,那么违反的也是规则三,不会影响所有路径,只需要调整当前路径就可以了,所以我们对于刚插入的节点应该给红色,然后在根据父亲的颜色去判断是否需要调整。
当我们插入了一个节点后,我们初始值给红色,如果父亲的颜色为黑色的话,那么就不违反红黑树的规则,可以直接插入结束,但是如果父亲的节点为红色,那么我们需要调整,调整分为好几种情况,我们一起来看一下。
下图 g表示爷爷(grandfather) ,p表示父亲(parent), u表示叔叔(uncle), c表示当前节点。
- 情况一
u存在并且为红色
a,b,c,d,e均为红黑树。cur可能是新插入,也有可能是下面更新上来的。
此时c和p和g一定是确定的,因为p为红色,g只能为黑色,不然插入前就不是红黑树,应该去查以前的问题。
此时是我们需要将p和u变为黑色,g变为红色。
此时有个小细节,g有可能是一颗红黑树的子树,也有可能是根节点,所以如果是根节点的话我们需要把它变黑。但是会比较麻烦,所以我们在插入的最后,不管怎么样,都把根节点的颜色变黑,就可以解决问题。
如果他不是根节点,那么爷爷的父亲也有可能是红色的,所以我们需要接着往上调整,我们需要把g给c,然后接着更新父亲,接着调整即可。
- 情况二
u不存在/u存在且为黑
此时cur就是新插入节点。
此时如果d和e是空或者为红色节点,那么a,b,c就是每条路径一个黑色节点的红黑树,不管怎么样,a,b,c都是比d,c多一个黑色节点的红黑树。并且cur原来的颜色一定是黑色的,它一定是下面的节点变色变上来的。因为如果他本来就是红色的话,这棵树就出现了连续的红色节点,违反了规则三。
此时光靠变色时无法解决问题问题的,如果我们和上面一样,把p变黑,把g变红,那么右边的路径就会少一个黑色节点,所以光凭变色时无法解决这里的问题的。我看看上图,就会想到右单旋,没错,这里就是需要对grandfather进行一个右单旋,然后把p变黑,把g变红就可以解决这里的问题。
但是如果cur原本在p的右边呢?
此时就需要进行一个左右双旋。
先进行一个左单旋
在进行一个右单旋
在旋转完成以后,需要将cur变黑,把grandfather变红,即可。
这是叔叔在爷爷的右边的两种情况,还有叔叔在爷爷的左边,和在右边的处理情况一样,类似就好了。
通过上面两个我们可以看到红黑树在插入以后,一直在变色,或者旋转+变色,本质都是在保持在插入一个节点后保持原来每条路径黑色节点的数量不发生变化,所以有些情况单凭变色是无法完成的,所以有些情况需要旋转+变色。
代码:
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
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);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
//判断父亲是否为黑色,为红色就需要处理
while (parent&&parent->_col==RED)
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
// g
// c u
//p
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// g
// p u
// c
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else
{
//parent == grandfather->_right
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)
{
RotateL(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
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;
Node* parentParent = parent->_parent;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
parent->_parent = subR;
if (_root != parent)
{
if (parentParent->_left == parent)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
else
{
_root = subR;
_root->_parent = nullptr;
}
}
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parentParent = parent->_parent;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
parent->_parent = subL;
if (_root != parent)
{
if (parentParent->_left == parent)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
else
{
_root = subL;
_root->_parent = nullptr;
}
}
验证红黑树
我们写了一颗红黑树,那怎么来验证我们写的树是否出现问题呢?
本质还是围绕红黑树的几点规则来进行判断。
如何判断是否有连续的红色节点呢?
如我们拿到一个节点后,判断它的孩子的话,它的孩子有可能为空,也有可能为红色or黑色,处理起来会比较麻烦,而且它还有可能有一个孩子或者两个甚至没有,比较难处理,但是每一个节点的一定只有一个父亲,除了根节点,此时,我们判断如果当前节点是否为红色节点,如果是,就判断它的父亲节点,如果它的父亲节点也为红色,那么就可以认为出现了连续的红色节点,即红黑树出现异常。
如何验证每条路径是否有相同的黑色节点?
因为是递归的过程,记录起来会比较麻烦,但是我们可以用二叉树祖先那个题的第二个思路,使用一个栈把路径给记录下来,但是这里不需要用栈,我们只需要一个变量就可以完成,每条路径的最后一定是空节点,所以我们可以在遍历的同时如果遇到黑色节点接++i,这里要用传值,不能传引用,不然记录的就是整棵树的黑色节点的数量,然后当遇到空节点后把当前路径的黑色节点数量打印一下就可以了。
bool isbanlance()
{
if (_root == nullptr)
{
return true;
}
if (_root->_col == RED)
{
return false;
}
int i = 0;
return check(_root,i);
}
bool check(Node* root, int i)
{
if (root == nullptr)
{
//cout << "->" << i << endl;
return true;
}
if (root->_col == BLACK)
{
i++;
}
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "有连续的红色节点" << endl;
return false;
}
return check(root->_left,i)
&& check(root->_right,i);
}
红黑树与AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。map和set的底层都是红黑树实现的。