目录
声明:本文在参照维基百科上的红黑树介绍和下面这篇文章的基础上做了修改和并且进行了详细的解释。在此感谢这两篇文章的作者。
https://blog.csdn.net/QIANGWEIYUAN/article/details/89321144
1.红黑树简介
红黑树严格来说是一种更高效的二叉查找树,它比BST(二叉查找树)和AVL(平衡二叉查找树)更加高效,能够将插入和删除操作的时间复杂度控制在O(log n),因为高效,被应用在库容器的底层,如STL::map,正是由于高效,因此也多了很多的规则。红黑树的规则一共有五条。对红黑树有过一点点了解的人应该都能想出一两条,我在这里一一列出。
- 节点必须是红色或者是黑色;
- 根节点是黑色;
- 所有叶子节点都是黑色(叶子节点为NULL);
- 任何一个节点和它的父节点不能同时为红色;
- 从根节点到任何一个叶子节点所经过的黑色的节点的数目必须是相同的。
我次奥,看完这些是不是一脸懵13,不要紧,我第一次看也是这样,我们先不要拘泥于这些规则。
首先,红黑树是一颗二叉查找树,所以它的插入的遍历顺序和删除的遍历顺序应该是和二叉查找树没有任何区别的。同时红黑树还应该是一颗AVL树,因此在操作的过程中为了保持树的平衡,肯定要进行旋转操作,其实,上述规则的第四条和第五条已经确保了红黑树是一颗AVL树。
有了这些信息,想一下,红黑树和AVL树相比无非就是多了一个颜色的要求,并且这个颜色的要求还是为了去保证的那么红黑树的操作无非就是下面两种:
- 变换颜色
- 旋转
总而言之,红黑树会围绕上面这两种操作去适应它的五条规则。
题外话:我觉得人家AVL树和红黑树的效率差不多,前提是得经过一点小得优化,不能每次都回溯到根节点,这个内容可以自行去进行了解。
接下来,进入正题了。
2.红黑树的定义
首先我们定义一下红黑树,有颜色,嗯,先来了枚举定义一下颜色。
enum COLOR
{RED,BLACK};
接下来我们进行节点的定义,节点无非就是什么左孩子啊,右孩子啊什么的。想一想还要什么?
我们还是围绕颜色和旋转这两个主体,想一哈,颜色变化不得有颜色啊,旋转不得有娃他爸啊。接下来我们将节点定义好并进行相关信息的封装。
template<typename T>
class RBNode
{
public:
RBNode(T value, COLOR col)
{
data = value;
color = col;
lefChild = NULL;
rightChild = NULL;
father = NULL;
}
void SetData(T value)
{
data = value;
}
T GetDAta()
{
return data;
}
void SetLeftChild(RBNode* left)
{
lefChild = left;
}
RBNode* GetLeftChild( )
{
return lefChild;
}
void SetRightChild(RBNode* right)
{
rightChild = right;
}
RBNode* GetRightChild()
{
return rightChild;
}
void SetFather(RBNode* par)
{
father = par;
}
RBNode* GetFather()
{
return father;
}
void SetColor(Color col)
{
color = col;
}
Color GetCorlor()
{
return color;
}
private:
T data;
RBNode* lefChild;
RBNode* rightChild;
RBNode * father;
COLOR color;
};
接下来我们来介绍红黑树的重点,首先我们来介绍一下左旋和右旋操作。旋转操作和AVL树的旋转操作没有任何区别。
3.红黑树的左旋和右旋
左旋操作:
就是让他爸把他大儿子叫爸,再让他小孙子管他叫爸。哈哈,有点乱是不是,看图(以红色节点为旋转节点)。
右旋操作:
现在,把上面两幅图的操作翻译成代码。
//类得成员函数:左旋操作
void LeftRotate()
{
RBNode* child = this->GetRightChild();
if (child == NULL)
return;
child->SetFather(this->father);//头节点的父节点为空
if (this->father != NULL)
{
if (this->father->GetLeftChild() == this)
{
this->father->SetLeftChild(child);
}
else
{
this->father->SetRightChild(child);
}
}
this->father = child;
this->rightChild = child->GetLeftChild();
child->SetLeftChild(this);
}
//右旋操作
void RightRotate()
{
RBNode* child = this->GetLeftChild();
if (child == NULL)
return;
child->SetFather(this->father);//头节点的父节点为空
if (this->father != NULL)
{
if (this->father->GetLeftChild() == this)
{
this->father->SetLeftChild(child);
}
else
{
this->father->SetRightChild(child);
}
}
this->father = child;
this->LeftChild = child->GetRightChild();
child->SetRightChild(this);
}
完成了左旋和右旋的操作以后我们来看看红黑树的插入操作。
4.红黑树的插入操作
红黑树插入节点时候的位置搜索过程和二叉查找树一样,这里我们不再赘述。只是在插入以后要用变换颜色和旋转去保持红黑树的五条规则。话不多说直接开始。
根据二叉查找树的插入规则我们知道新插入的元素一定是树的叶子节点。
首先,我们将新插入的节点的颜色设置为红色,这样根到每个叶子节点路径上的黑色节点的数目是不会变的,满足了红黑树的规则。然后我们根据父节点和叔叔节点的状态将情况分为5种:
真让人头大,但还是要看,because you are poor。
第一种情况:
如果插入的节点为根节点,直接将颜色设置为黑色,完成操作。
第二种情况:
如果新插入的节点的父节点为黑色,(我们设置新插入的节点为红色),来,你翻到上面的五条规则,都满足吧。完成操作。
第三种情况:
我们假设新插入的节点为D,他的父节点为B,叔叔节点假设为C,如果父节点和叔叔节点都为红色我们怎么办呢?
可以发现D和他的父节点B都为红色,这违反了规则4。这时们把D的父节点和叔叔节点的颜色改为黑色,这时规则4满足了,但是违反了规则5,因为以A为根的子树种多出了一个黑色节点。所以我们将将D的他爷的节点改为红色,然后从D的爷爷节点A开始,继续向上判断A是处于哪种情况。因为他爷爷的父亲可能也是红色。注意,我们这里一共有五种情况哦。
如上图,我们继续把A当作新插入的节点向上回溯,A的父节点为黑色,满足情况2,就可以结束操作了,当然也可能是其他情况,我们继续来看另外两种情况。
第四种情况:
如果新插入节点的父节点为红色,而他的叔叔节点为黑色或者不存在(不存在也为黑色),且新插入的节点和父节点以及祖父节点在一条直线上。
如果满足这种情况,那么插入的节点一定有祖父节点,不要问我为什么,我就是知道。
如上图:
假设A为插入的节点,如果A没有爷爷节点,那么不满足规则2:根必须为黑色。
接下来我们分析这种情况。
如上图左边为这种情况,发现不满足规则4,因为新插入的节点C以及他的父节点B都是红色,这时我们可以将父节点B的颜色改为黑色,将爷爷节点改为红色,然后将爷爷节点进行右旋操作,右旋后如右边所示,这里我们没有显示叔叔节点,这是因为叔叔节点为黑色,且旋转以后会成为A的儿子节点,对于叔叔节点而言,各条规则都是满足的。
旋转之后我们可以发现,各条规则都满足了,你再往上考虑一层,由于插入节点之前的红黑树一定是符合规则的,因此旋转之后各条规则也都满足,首先各条路径上的黑色节点没有增加,其次,没有出现颜色相同的节点,由于B变为黑色,旋转之。后不可能冲突。这时可以return了。
这时可能还有人说,你举的例子是三个节点都是靠左一条线,如果靠右呢?
靠左向右旋,靠右一条线肯定向左旋了。
第五种情况:
父节点为红色,叔叔节点为黑色,且新插入的节点,以及他的父节点和爷爷节点不在一条直线上。
终于最后一种情况了。
如上图,我们可以对C的父节点B进行左旋,旋转之后是不是符合了第四种情况了,按照第四种情况操作即可。
总结上述情况3,4,5,破坏的都是规则4,都是出现了连续的红色节点。如情况3,我们向上回溯的途中,由于当前节点下面的节点经过调整都满足条件,如果这时当前节点的父节点的颜色为黑色就可以停止遍历了。这段信息可以作为我们编程的一个终止条件。
到了这一步,我相信各位小老板都差不多理解了。接下来我们把上述的五种情况翻译成代码。
//单独的声明的函数,不再是类的成员函数
void Insert(RBNode<int> *node)
{
//这里我们假设节点已经插入到了合适的位置,这里只按按照规则进行调整
//将新插入节点的颜色进行调整
node->SetColor(RED);
//情况1
if (node->GetFather() == nullptr)
{
node->SetColor(BLACK);
return;
}
//情况2
if (node->GetFather()->GetCorlor() == BLACK)
return;
//情况3,4,5都要对左右孩子区别开
while (node->GetFather()->GetCorlor() == RED)
{
//插入的节点在爷爷节点的左子树上
if (node->GetFather()->GetFather()->GetLeftChild() == node->GetFather())
{
RBNode<int>* uncle = node->GetFather()->GetFather()->GetRightChild();//得到叔叔节点
//情况3
if (uncle != nullptr&&uncle->GetCorlor == RED)
{
//父节点变黑,叔叔节点变黑,爷爷节点变红,node节点指向爷爷节点
node->GetFather()->SetColor(BLACK);
uncle->SetColor(BLACK);
node->GetFather()->GetFather()->SetColor(RED);
node=node->GetFather()->GetFather();
}
//叔叔节点是黑色
else
{
//情况4:不再一条直线上
if (node->GetFather()->GetRightChild() == node)
{
//父节点左旋
node->GetFather()->LeftRotate();
}
//情况5:在一条直线上了
//父节点变黑,爷爷节点变红,爷爷点右旋
node->GetFather()->SetColor(BLACK);
node->GetFather()->GetFather()->SetColor(BLACK);
node->GetFather()->GetFather()->RightRotate();
//完成跳出
break;
}
}
//插入的节点在爷爷节点的右子树上
else
{
RBNode<int>* uncle = node->GetFather()->GetFather()->GetLeftChild();//得到叔叔节点
//情况3
if (uncle != nullptr && uncle->GetCorlor == RED)
{
//父节点变黑,叔叔节点变黑,爷爷节点变红,node节点指向爷爷节点
node->GetFather()->SetColor(BLACK);
uncle->SetColor(BLACK);
node->GetFather()->GetFather()->SetColor(RED);
node = node->GetFather()->GetFather();
}
//叔叔节点是黑色
else
{
//情况4:不再一条直线上
if (node->GetFather()->GetLeftChild() == node)
{
//父节点右旋
node->GetFather()->RightRotate();
}
//情况5:在一条直线上了
//父节点变黑,爷爷节点变红,爷爷点左旋
node->GetFather()->SetColor(BLACK);
node->GetFather()->GetFather()->SetColor(BLACK);
node->GetFather()->GetFather()->LeftRotate();
//完成跳出
break;
}
}
}
}
翻译成代码还是挺简单的。
5.红黑树的删除操作
下述,我们同样假设已经找到了需要删除的节点。这里只根据情况进行讨论。
我们继续分情况讨论,能看到删除操作,说明优秀的你已经将插入操作理解得差不多了,你就是IT界最靓得zai。
红黑树的删除操作,稍微复杂一点点,just一点点。
我们先看一个例子:
我们先不管什么颜色不颜色的,假如上面这颗是一个BST树(二叉查找),假如我们要删除节点A(A实际情况可能不是根节点,这要里说明的内容你马上就懂),那我们可以用将左边最大的E或者右边最大的C去替换A的值,这样问题就变成删除一个叶子节点,或者只有一个左孩子的叶子节点。
这时因为能替换A的节点必须为右子树最大或者左子树中最小的元素,右子树最小的元素一般是最左边的元素,上图中的F就是右子树中最小的元素。左子树中最大的元素肯定是左子树中最有右边的元素,如上如E。因为只有用这两个节点去替换,才能保证树的左小右大的性质。而这两个元素要么是叶子节点,要么是只有一个叶子节点的节点。并且这个节点不可能有孙子节点,因为还要保证规则5。
由于红黑树和BST树的渊源,二叉树删除节点的问题就转换为删除一个叶子节点 或者 删除一个只有一个儿子的节点的问题。那么以下讨论的要删除的节点都是在讨论用于替换的这个节点(叶子节点或者是只有一个儿子的节点)。(绿色是不是很好看)
情况1:要删除的节点是根节点,它不是你的亲戚,请不要犹豫,直接删除。
情况2:要删除的节点是红shai的。
如果这个红色的节点没有儿子,那么直接删除。
如果这个红色节点有一个儿子,那么这个儿子肯定是黑色的,且要删除节点的父亲是黑色。我们可以将红色节点删除,用儿子节点去填补红色节点的位置。因为路径上只是少了一个红色的节点,各条规则都满足。
接下来几种情况我们讨论的都是黑色的节点,且这个黑色的节点为叶子节点。那么可能有人说,那如果这个节点有一个孩子节点呢?我们继续用孩子节点去替代这个节点,是不是又转换成了删除叶子节点的问题。
只不过是要经过两次替代操作。上面那条绿色的是在一次替代操作的前提下。
情况3:兄弟节点是红色的
这种情况我们来分析以下,如果兄弟节点为红色,那么如果兄弟节点有孩子节点,那么一定是黑色的。并且父节点一定是黑色。
另外,如果我们删除了当前的黑色节点,那么兄弟节点路径上的黑色节点就会多出来一个,我们看图分析。
上图的左边就是这种情况,如果兄弟节点是红色的,我们将兄弟节点变为黑色,将父节点变为红色,然后将兄弟节点进行左旋操作,操作后结果如上图的右边,我们注意观察,如果我们在没有删除B节点的情况下,它还是满足红黑树的性质,并且B的路径上多出了一个红色的节点。这有什么用呢?
现在情况1转换成了这种状态,然后该怎么操作呢?我们先假设一种方案:我么这时可不可以把右图D的颜色改为红色,然后将A的颜色改为黑色。然后将B删除,之后的状态是不是满足红黑树的五条规则。
所以情况1转换后的问题可以再次转化为去B的路径上去寻找一个红色的节点呢。带着这个问题,来看情况4.
情况四:兄弟节点是黑色,并且兄弟节点的孩子节点也为黑色
这就是情况3转换后到达的状态,接下来我们用情况3后面分析的思路进行操作。
如情况3所说,我们将兄弟节点C的颜色改为红色,这时C的路径上少了一个黑色节点,我们删除B以后这时A这棵树上的黑色节点统一少了一个。那么如果A还有兄弟树,那么兄弟树的黑色节点就比A多一个,所以我们要向上回溯,假设父节点A是要删除的节点,再根据A的状态去调整A(调整情况还是分为情况3,4,5,6四种),调整完成以后再将B进行移除。
如果A(回溯过程中调整的新A)为红色或者A为根节点,我们就可以停止回溯了。如果为根节点,那么删除B以后,整棵树上的黑色节点都减少1。如果A为红色,改为黑色,可以使左右平衡。
你会发现,情况3和情况四其实是相关联的,因为情况3最终会转换为情况4.
同样情况5和情况6也有颇深的渊源。
情况5:兄弟节点是黑色,而且兄弟节点的左孩子是红色的,右孩子是黑色的(兄弟节点在右边,左边相反)
我们先看这种状态的图示:
我们可以将兄弟节点的颜色变为红色,将红色孩子变为黑色,然后对兄弟节点进行右旋操作,转入情况6。
情况6:兄弟节点的左孩子为黑色(或不存在),右孩子为红色
如上图,我们可以将兄弟节点的右孩子变为黑色,然后将兄弟节点进行左旋,删除B两边平衡。
情况6中如果D的左孩子为黑色(为红是情况4),那么旋转以后D的左孩子会挂在A的左孩子位置处,由于初始状态是满足红黑树的五条规则,那么这种状态也是满足的。
后两种情况比较简单。
上述已经考虑到了所有的细节,接下来我们将思路翻译成代码:
再讲一点:红黑树的插入操作最多两次旋转,删除操作最多三次。下面这篇博文举了详细的例子。
http://blog.sina.com.cn/s/blog_74dffc060102wus6.html
红黑树在面试中经常会被问到,注意是面试,不是笔试,我说的意思自己慢慢体会。拜拜了,各位。下次再见。
对了,删除操作代码在下面。
//我感觉我这部分代码写的不是很好,刚开始没有定义树的根节点,删除操作的这部分代码可以参考我开始给出那两篇博文。
//但是理解思路还是很好的
void Delete(RBNode<int>* node)
{
//node是已经寻找到的要删除的节点
//第一次替换操作:转换成叶子节点或者只有一个孩子的节点
if (node->GetLeftChild() != nullptr && node->GetRightChild() != nullptr)
{
RBNode<int>* oldNode = node;
node = node->GetRightChild();
while (node->GetLeftChild() != NULL)
{
node = node->GetLeftChild();
}
oldNode->SetData(node->GetDAta);
}
RBNode<int>* child = (node->GetLeftChild() != nullptr ? node->GetLeftChild() : node->GetRightChild());
//情况1
if (!node->GetFather())
{
free(node);
child->SetColor(BLACK);
}
//有一个孩子
if (child)
{
//第二次替换
int temp = node->GetDAta();
node->SetData(child->GetDAta);
child->SetData(temp);
node = child;
}
//node为叶子节点
//情况2
if (node->GetCorlor() == RED)
{
free(node);
return;
}
//情况3、4、5、6
else
{
//情况3、4的跳出条件
while (node->GetFather() != nullptr && node->GetCorlor() == BLACK)
{
//分左右讨论
if (node == node->GetFather()->GetLeftChild())
{
//得到兄弟节点
RBNode<int>* b = node->GetFather()->GetRightChild();
//情况3
if (b != nullptr && b->GetCorlor() == RED)
{
b->SetColor(BLACK);
node->GetFather()->SetColor(RED);
node->GetFather()->LeftRotate();
b = node->GetFather()->GetRightChild();
}
//清况4
if ((b->GetLeftChild() == nullptr || b->GetLeftChild()->GetCorlor() == BLACK)
&& (b->GetRightChild() == nullptr || b->GetRightChild()->GetCorlor() == BLACK))
{
b->SetColor(RED);
node = node->GetFather();
}
else
{
//情况5、6
if (b != nullptr && b->GetCorlor() == BLACK)
{
if (b->GetRightChild()->GetCorlor() = BLACK)
{
b->GetLeftChild()->SetColor(BLACK);
b->SetColor(RED);
b->RightRotate();
b = node->GetFather()->GetRightChild();
}
b->GetRightChild()->SetColor(BLACK);
node->GetFather()->LeftRotate();
break;
}
}
}
else
{
//对左右取反即可,不再赘述了,好鸡儿累
}
}
}
free(node);
}