这是小星学DSA系列的第一篇,我会记录我学习的过程与理解,希望能够帮到你。
本篇文章的思维导图如下,在文章的末尾,我会给出更加详细的思维导图。
红黑树的定义
红黑树的概念与性质
- 红黑树是一棵节点为黑色或红色的二叉搜索树;
- 性质1:根节点与外部节点(叶子节点的空子节点)为黑色
- 性质2:从根节点到外部节点的路径上,不能有两个连续的红色节点
- 性质3:从根节点到外部节点的路径上,黑色节点的数目相同
💫 小星说丨一句话概括红黑树性质:头尾黑,红红不相连,黑节点数目相等
红黑树的复杂度及证明
- 红黑树的空间复杂度为O(n)
- 红黑树的时间复杂度为: O(lgn)
证明红黑树的时间复杂度
- 等价命题:一棵含有n个节点的红黑树的高度至多为2log(n+1)
- 逆否命题:高度为h的红黑树,其节点至少为 2 h / 2 − 1 2^{h/2}-1 2h/2−1个
- 设节点x路径中黑节点的数量为bh(x),则上述命题为:高度为h的红黑树,其黑节点至少为 2 b h ( x ) − 1 2^{bh(x)}-1 2bh(x)−1个
- 数学归纳法证明:
- h = 0, 易证
- 假设h=H-1时等式成立,则h=H时,根节点的两个子节点高度均为H-1,则根节点的黑节点数量至少为 2 ∗ 2 b h ( r o o t . c h i l d ) − 2 + 1 = 2 b h ( r o o t ) − 1 2*2^{bh(root.child)}-2+1=2^{bh{(root)}}-1 2∗2bh(root.child)−2+1=2bh(root)−1,等式成立
💫 小星说丨用高度推节点数地计算和理解更容易
红黑树的操作
红黑树的作为树的基本操作:查找、插入、删除
为了维护红黑树性质需要的操作:左旋、右旋
红黑树的左旋与右旋
左旋
左旋即被旋转的节点(根节点)变为了右节点的左子节点,右节点代替了它的位置,而右节点原先的左子节点则变为了被旋转节点的右节点
右旋
右旋即被旋转的节点(根节点)变为了左节点的右子节点,左节点代替了它的位置,而左节点原先的右子节点则变为了被旋转节点的左节点
💫 小星说丨左旋:被左旋的节点变为左子节点;右旋:被右旋的节点变为右子节点。想象被旋转的节点是一个跷跷板的支点,左旋即把右节点翘上去,右旋即把左节点翘上去,多出来的节点由支点接住。
红黑树的查找
在查找上,红黑树与普通的二叉搜索树完全一样,不一样的点在于红黑树的查找复杂度为O(lgn)。
二叉搜索树的查找非常简单,只要将查找值与当前节点的值比较,大则向右找,小则向左找,这里不再赘述。
红黑树的插入
红黑树的插入总共三步:
- 将红黑树当作二叉查找树,插入节点;
- 将插入的节点着色为红色;
- 通过旋转着色,使之重新成为一颗红黑树。
接下来我们详细说明一下这三步:
插入二叉查找树
首先我们不考虑颜色,而是根据二叉查找树的性质,找到红黑树的插入点。
我们用一个节点指针遍历二叉树,反复与节点比较,大则向右,小则向左,直到到达null;
插入节点着色为红色
为了在插入时不破坏红黑树的性质3(从根节点到外部节点的路径上,黑色节点的数目相同),我们将该节点着色为红色,接下来,只要解决红-红冲突,便能完成插入。
插入修正
💫 小星说丨一句话理解红黑树的插入修正:红色矛盾向上转移,直到移到根节点变为黑色。因为矛盾要向上转移,因此我们需要考虑上一层长辈节点的状态,即父节点与叔叔节点。
这里,我给插入修正的情况做了一个总结表
当前节点 | 父节点 | 叔叔节点 | 当前节点与父亲节点的偏向 | 操作 |
---|---|---|---|---|
根节点 | 染黑 | |||
红 | 黑 | 无需修正 | ||
红 | 红 | 红 | 父节点和叔叔节点变黑,祖父节点变红,开始解决祖父节点可能存在的矛盾 | |
红 | 红 | 黑 | 不一致 | 当前节点通过左旋或右旋成为原父节点的父节点,使二者朝向一致,此时将原父节点当作当前节点,再次判断当前节点状态。 |
红 | 红 | 黑 | 一致 | 父节点通过左旋或右旋成为原祖父节点的父节点,父节点变为黑色,原祖父节点变为红色 |
可以这样理解红黑树插入修正的逻辑:
- 父亲红,叔叔红,那么就交换父亲层和祖父层的颜色,将红色矛盾向上转移至祖父,而父亲和叔叔这一层可以变为黑色;
- 父亲红,叔叔黑,由于性质3的限制,不能直接交换(下一层需要两个变黑才能换上一层一个变红),为了只变父亲这边不变叔叔那边,于是使用左旋或右旋来交换父亲和祖父的颜色;
- 旋转时要保持当前节点和父亲节点的父子关系,所以要求偏向一致。
💫 小星说丨红黑树插入修正达到以下三种情况,即为最终情况,可以彻底解决矛盾,其他的操作是为了达到这三种状态。
- 当前节点为根节点
- 当前节点的父节点为黑色
- 双红偏向一致,叔叔黑色
红黑树的删除
红黑树的删除同样是三步:
- 将红黑树当作二叉搜索树,找到需要删除的节点
- 使用恰当的节点值代替该删除节点,并将矛盾转移到代替节点
- 删除代替节点,并根据代替节点的情况,旋转着色使之重新成为一颗红黑树
看起来比插入要复杂一些,这是因为插入的地方肯定为叶子节点,而删除的地方则不一定,因此我们需要将删除的矛盾转移至叶子节点,然后再来解决红黑树的矛盾。
从二叉搜索树找到需要删除的节点
利用二叉搜索树的查找方法,找到该节点
找到代替节点
找到删除节点后,我们需要明确,删除节点的位置是否可以空置,如果不空置,是否需要找一个替代节点,而替代节点又如何解决?
这里有三种情况
- 被删除节点为叶子节点,由于它已经是叶子节点,因此这个地方可以为空,也即节点可以直接删除。
- 被删除的节点有一个子节点,那么我们就用这个子节点代替这个节点的位置,而将子节点删除,由于红黑树的的性质3限制,这个子节点肯定是一个叶子节点。
- 被删除的节点有两个子节点,那么我们就找到该节点的后继节点(右子树的最左节点),用后继节点代替这个节点的位置,而将后继节点删除,后继节点也必定为一个叶子节点
删除修正
经过第二步,我们将删除指定节点的任务,都转化为了删除一个叶子节点的任务,接下来,我们需要根据这个叶子节点的状态,通过旋转着色维护红黑树的性质。
💫 小星说丨一句话总结红黑树的删除修正:父节点下放弥补双黑,兄弟相应调整。这里主要影响到的是兄弟节点和侄子节点
以下两种情况,可以直接删除该节点,用一个外部节点代替其位置
当前节点 | 兄弟节点 | 侄子节点 | 操作 |
---|---|---|---|
根节点 | 直接删除 | ||
红色 | 直接删除 |
以下几种情况,为了维护性质3,我们在用外部节点代替该节点时,将该外部节点标记为双黑(DB,double black)
当前节点 | 兄弟节点 | 侄子节点 | 操作 |
---|---|---|---|
DB | 红色 | 父亲节点与兄弟节点颜色互换,且父亲节点向DB方向旋转,此时再重新判断DB状态。 | |
DB | 黑色 | 黑色 | 当前节点变为单黒,兄弟节点变为红色,父亲节点加一个黑色(黑或双黑),再次判断情况 |
DB | 黑色 | 远黑近红 | 兄弟和红侄子颜色互换,朝着DB的反方向旋转,(即变到下一种状态)此时再重新判断DB状态 |
DB | 黑色 | 近黑远红 | 兄弟和父节点颜色互换,父节点向DB方向旋转,删除DB记号,并将远红侄子标为黑色 |
这样理解红黑树的删除修正逻辑:为了弥补双黑节点将要失去的黑色,我们将父节点加一个黑色弥补到这条线路上,但这样兄弟节点那边会多一个黑色。因此,如果侄子是黑的,那么兄弟就可以变红来保持黑色平衡;否则为了不影响兄弟路线上的黑色数目,父节点需要通过旋转来到DB的路径,而兄弟路线上少的一个黑色要由远侄子(不会被带到DB路线上的侄子)弥补。因此,这种情况下兄弟必须是黑色(这个黑色将贡献给父亲),而远侄子也必须是红色(才能弥补一个黑色)。
💫 小星说丨红黑树删除修正达到以下三种情况,即为最终情况,可以彻底解决矛盾,其他的操作是为了达到这三种状态。
- 当前节点为根节点
- 当前节点为红色节点
- 兄黑远侄子红
手撕红黑树
初始化
首先,我们需要准备构建红黑树所需要的基础数据结构,以及基本的类成员与初始化方法
节点数据结构
红黑树由节点构成,因此我们首先需要定义节点的数据结构
struct Node {
int data;
Node *parent;
Node *left;
Node *right;
int color;
};
// 定义节点指针类型,方便引用
typedef Node *NodePtr;
基本类成员
这里我们需要定义两个类成员:根节点与NULL节点(用于截止判定)
class RedBlackTree {
private:
NodePtr root;
NodePtr TNULL;
}
初始化二叉树
二叉树的初始化:1. 初始化TNULL节点 2. 初始化根节点
RedBlackTree() {
TNULL = new Node;
TNULL->color = 0;
TNULL->left = nullptr;
TNULL->right = nullptr;
root = TNULL;
}
红黑树的左旋与右旋
左旋实现:
- 定义right为节点x的右子节点
- 将right的左孩子接在父节点的右边
- right的父节点变为祖父节点;祖父节点的(左/右)子节点变为right
- right的左节点变为x,x的父节点变为right
右旋实现同理:
void leftRotate(NodePtr x) {
NodePtr y = x->right;
x->right = y->left;
if (y->left != TNULL) {
y->left->parent = x;
}
y->parent = x->parent;
if (x->parent == nullptr) {
this->root = y;
} else if (x == x->parent->left) {
x->parent->left = y;
} else {
x->parent->right = y;
}
y->left = x;
x->parent = y;
}
void rightRotate(NodePtr x) {
NodePtr y = x->left;
x->left = y->right;
if (y->right != TNULL) {
y->right->parent = x;
}
y->parent = x->parent;
if (x->parent == nullptr) {
this->root = y;
} else if (x == x->parent->right) {
x->parent->right = y;
} else {
x->parent->left = y;
}
y->right = x;
x->parent = y;
}
红黑树的查找
接下来实现红黑树的查找,我们定义一个search公共函数作为外部调用接口,内部的递归使用私有函数searchHelper。
private:
NodePtr searchTreeHelper(NodePtr node, int key) {
if (node == TNULL || key == node->data) {
return node;
}
if (key < node->data) {
return searchTreeHelper(node->left, key);
}
return searchTreeHelper(node->right, key);
}
public:
NodePtr searchTree(int k) {
return searchTreeHelper(this->root, k);
}
红黑树的插入
这里我们需要定义两个函数,一个是insert函数,一个是插入修正insertFix函数
插入
插入函数逻辑的实现包括:
- 为插入的值new一个新节点
- 找到插入位置
- 插入该节点,建立父子连接(注意根节点的判定)
void insert(int key) {
// 1. new一个新节点
NodePtr node = new Node;
node->parent = nullptr;
node->data = key;
node->left = TNULL;
node->right = TNULL;
node->color = 1;
NodePtr y = nullptr;
NodePtr x = this->root;
// 2. 找到插入位置
while (x != TNULL) {
y = x;
if (node->data < x->data) {
x = x->left;
} else {
x = x->right;
}
}
// 3. 建立父子连接
node->parent = y;
if (y == nullptr) {
root = node;
} else if (node->data < y->data) {
y->left = node;
} else {
y->right = node;
}
insertFix(node);
}
插入修正
插入修正的5种情况中,情况1和情况2可以排除在循环外,情况3,4,5由循环解决
void insertFix(NodePtr k) {
NodePtr u;
// 情况1:根结点&情况2:黑父节点
while (k->parent != nullptr && k->parent->color == 1) {
// 父节点为右孩子
if (k->parent == k->parent->parent->right) {
// 获取叔叔节点
u = k->parent->parent->left;
// 情况3:叔叔节点为红
if (u->color == 1) {
u->color = 0;
k->parent->color = 0;
k->parent->parent->color = 1;
// 矛盾转移至祖父节点
k = k->parent->parent;
}
// 叔叔节点为黑
else {
// 情况4:父子偏向不一致
if (k == k->parent->left) {
k = k->parent;
rightRotate(k);
}
// 情况5:父子偏向一致
k->parent->color = 0;
k->parent->parent->color = 1;
leftRotate(k->parent->parent);
}
}
// 父节点为左孩子,类似
else {
u = k->parent->parent->right;
if (u->color == 1) {
u->color = 0;
k->parent->color = 0;
k->parent->parent->color = 1;
k = k->parent->parent;
}
else {
if (k == k->parent->right) {
k = k->parent;
leftRotate(k);
}
k->parent->color = 0;
k->parent->parent->color = 1;
rightRotate(k->parent->parent);
}
}
}
// 根结点染黑
root->color = 0;
}
红黑树的删除
这里同样需要定义两个函数,一个是删除函数deleteNode,一个是删除修正函数deleteFix
删除
删除函数的逻辑实现包括:
- 找到key对应的节点
- 找到对应的代替节点(叶子节点;有一个子节点;有两个子节点)
- 删除修正
void deleteNode(int key)
{
// 1. 找到key对应的节点
NodePtr z = TNULL;
z = searchTreeHelper(this->root, key);
if (z == TNULL)
{
cout << "Key not found in the tree" << endl;
return;
}
// 2. 找到对应的代替节点
NodePtr y = TNULL;
if (z->left == TNULL && z->right == TNULL)
{
y = z;
}
else if (z->left == TNULL)
{
y = z->right;
}
else if (z->right == TNULL)
{
y = z->left;
}
else
{
y = minimum(z->right);
}
z->data = y->data;
// 3. 删除修正
deleteFix(y);
// 修正完后删除这个节点
if (y->data < y->parent->data){
y->parent->left = TNULL;
}
else{
y->parent->right = TNULL;
}
// 释放指针
z = y;
delete y;
y = NULL;
z = NULL;
}
删除修正
void deleteFix(NodePtr x)
{
NodePtr s;
// 情况1:根结点&情况2:红节点
while (x != root && x->color == 0)
{
// x为左节点
if (x == x->parent->left)
{
s = x->parent->right;
// 情况3:兄弟节点为红色
if (s->color == 1)
{
s->color = 0;
x->parent->color = 1;
leftRotate(x->parent);
// 获得新的兄弟节点
s = x->parent->right;
}
// 情况4:兄弟节点为黑色,侄子节点为黑色
if (s->left->color == 0 && s->right->color == 0)
{
s->color = 1;
// 父亲节点变为要判断的节点
x = x->parent;
}
else
{
// 情况5: 侄子远黑近红
if (s->right->color == 0)
{
s->left->color = 0;
s->color = 1;
rightRotate(s);
s = x->parent->right;
}
// 情况6: 侄子远红近黑
s->color = x->parent->color;
x->parent->color = 0;
s->right->color = 0;
leftRotate(x->parent);
// 终止循环
x = root;
}
}
// x为右节点,类似
else
{
s = x->parent->left;
if (s->color == 1)
{
s->color = 0;
x->parent->color = 1;
rightRotate(x->parent);
s = x->parent->left;
}
if (s->right->color == 0 && s->right->color == 0)
{
s->color = 1;
x = x->parent;
}
else
{
if (s->left->color == 0)
{
s->right->color = 0;
s->color = 1;
leftRotate(s);
s = x->parent->left;
}
s->color = x->parent->color;
x->parent->color = 0;
s->left->color = 0;
rightRotate(x->parent);
x = root;
}
}
}
x->color = 0;
}
总结
再最后,我们再用一张思维导图总结本篇博客的内容
源代码
本文代码已在github上开源,包含c++,python(待补充), golang(待补充)的红黑树代码
https://github.com/Yuxin1999/star-code