对红黑树的一些理解及实现
红黑树(英语:Red–black tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。
以下是我对红黑树的一些理解,可供参考,不保证准确性。
参考资料:
https://www.cnblogs.com/alantu2018/p/8462017.html
https://zhuanlan.zhihu.com/p/22800206
最后实现的代码放在Github上:
https://github.com/Radium1209/Red-Black-Tree
特性
- 每个节点是黑色或红色(所以叫红黑树)
- 根节点是黑色的
- 叶节点是黑色的(空的叶节点)
- 如果一个节点为红,子节点必须为黑
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点
- 为什么根节点是黑色的?
我的理解是使前两层一定满足红黑树条件,不然在前两层就需要进行旋转操作(见后文),这样会使旋转操作更加复杂(因为要考虑祖父节点不存在),且旋转后的根节点也会变黑,因此不如一开始就令根节点为黑。 - 为什么叶子节点是黑色的?
这个和后面旋转时操作的叔叔节点有关,因为叔叔节点为空的话也为黑色,这样满足其中一个旋转条件。 - 特性4和特性5
这两个特性是用来保持平衡的,红黑树是一种近似平衡的树,特性5保证了没有一条路径会比其他路径长出2倍,所以可以保持红黑树的近似平衡。红黑树的近似平衡是因为它的旋转操作会比较少(与AVL树相比),所以红黑树在插入删除的性能要好于AVL树,在查询上可能会略逊于AVL树。
主要操作
- 变颜色
- 左旋
- 右旋
变颜色
就是改变一下颜色,总共就两种。
左旋
关于旋转的部分网上的资料很多,可以自行搜索,这边主要讲一下关于代码的部分。
左旋示意图(对节点x进行左旋):
px px
/ /
x y
/ \ --(左旋)--> / \
lx y x ry
/ \ / \
ly ry lx ly
以上图为例,左旋所改变的部分是:
px->Child = y // 可能是左孩子也有可能是右孩子
x->father = y // x的父亲和右儿子都变了
x->rightChild = ly
y->father = px // y的父亲和左儿子都变了
y->leftChild = x
ly->father = x // ly的父亲变了
所以我们的代码就直接改变这些变化的值就可以了,不过需要注意一些值可能为空:
template<class T>
void RBTree<T>::leftRotate( RBTNode<T> *node )
{
RBTNode<T> *x = node;
RBTNode<T> *y = x->rightChild;
// 修改x->rightChild = ly
x->rightChild = y->leftChild;
// 修改ly->father = x, 注意ly可能为空
if ( y->leftChild != NULL)
{
y->leftChild->father = x;
}
// 修改y->father = px
y->father = x->father;
// 修改px-Child = y,此时需要考虑px为NULL以及左右孩子的区别
if ( x->father == NULL )
{
root = y; // 此时要更新root
}
else
{
if ( x->father->leftChild == x )
{
x->father->leftChild = y;
}
else
{
x->father->rightChild = y;
}
}
// 修改y->leftChild = x
y->leftChild = x;
// 修改x->father = y
x->father = y;
}
右旋
和左旋基本上一样:
template<class T>
void RBTree<T>::rightRotate( RBTNode<T>* node )
{
RBTNode<T> *y = node;
RBTNode<T> *x = y->leftChild;
y->leftChild = x->rightChild;
if ( x->rightChild != NULL )
{
x->rightChild->father = y;
}
x->father = y->father;
if ( y->father == NULL )
{
root = x;
}
else
{
if ( y->father->leftChild == y )
{
y->father->leftChild = x;
}
else
{
y->father->rightChild = x;
}
}
x->rightChild = y;
y->father = x;
}
插入
正常插入
以上操作都是为了插入和删除做准备的,因为在插入和删除的时候会使红黑树不满足红黑树的特性,所以需要用以上操作对红黑树进行修正。下面讲一下在插入时如何对红黑树进行修正。
首先按照正常的二叉搜索树进行插入,且插入的节点为红色。
- 插入的节点为什么是红色?
如果插入的是黑色永远不会破坏特性4,而且所有节点都变成黑色了,根本没有意义。
/* 插入 */
template<class T>
void RBTree<T>::insert( RBTNode<T>* node )
{
RBTNode<T> *y = NULL;
RBTNode<T> *x = root;
// 当做二叉排序树正常插入
while ( x != NULL )
{
y = x;
if ( node->value < x->value )
{
x = x->leftChild;
}
else
{
x = x->rightChild;
}
}
node->father = y;
if ( y != NULL )
{
if ( node->value < y->value)
{
y->leftChild = node;
}
else
{
y->rightChild = node;
}
}
else
{
root = node;
return ;
}
node->color = RED;
insertFix( node );
}
插入修正
插入完后可能会导致红黑树的特性遭到破坏。
- 我们思考一下可能会破坏的特性是什么?
特性1和2不可能被破坏,特性3也不会。特性5因为插入的是红节点所以也不会被破坏,那么被破坏的只可能是特性4。
于是,我们要对红黑树进行修复,其实修复和AVL树也类似,主要是通过旋转来保持平衡,通过变色来保持红黑特性。总结如下:
- 插入的是根节点,让根节点为黑色,不用修复。
- 插入的节点的父节点是黑色的,不会破坏任何特性,不用修复
- 父节点和叔叔节点为红色,只要变颜色,父亲和叔叔变黑,祖父变红,此时要注意祖父变红了,也可能会破坏红黑树的特性,所以要递归修复祖父节点。
- 父节点为红,叔叔为黑,且当前节点、父亲节点和祖父节点的关系为RL和LR,此时先以父节点旋转(根据当前节点和父节点关系旋转,L右旋,R左旋)成LL和RR,然后就变成下一种情况。
- 父节点为红,叔叔为黑,且当前节点、父亲节点和祖父节点的关系为LL和RR,以祖父节点旋转(根据父节点和祖父节点关系旋转,L右旋,R左旋),同时因为此时旋转后节点颜色将不满足红黑树特性,因此要先将父亲变黑,祖父变红,再进行旋转。
总结 :
- 父节点为红色才需要修复。
- 叔叔节点为黑色才需要旋转。
- 不需要旋转时变颜色,要递归修复祖父节点。
- 旋转分两种,一种为LR(RL),另一种为LL(RR),第一种需要先旋转为第二种再进行旋转。
- 两种旋转的对象不同,第一种是父亲,第二种是祖父。
- 第二种旋转需要变颜色,第一种不用。
/* 插入修正*/
template<class T>
void RBTree<T>::insertFix( RBTNode<T>* node )
{
if ( node == NULL )
{
return ;
}
// fa--->父亲, gfa--->祖父, un--->叔叔
RBTNode<T> *fa, *gfa, *un;
// 如果父节点存在,且为红色
while ( ( ( fa = node->father ) != NULL ) && ( fa->color == RED) )
{
gfa = fa->father;
// 父亲节点是祖父节点的左孩子,所以只能出现LL和LR情况
if ( fa == gfa->leftChild )
{
un = gfa->rightChild;
// 叔节点是红色的
if ( un != NULL && un->color == RED )
{
fa->color = BLACK;
un->color = BLACK;
gfa->color = RED;
node = gfa;
continue;
}
else
{
// LR型,左旋父节点,旋转完后会变成LL型
if ( fa->rightChild == node )
{
leftRotate( fa );
std::swap( fa, node );
}
// LL型,右旋祖父节点
fa->color = BLACK;
gfa->color = RED;
rightRotate( gfa );
}
}
// 父亲节点是祖父节点的右孩子,所以只能出现RL和RR情况
else
{
un = gfa->leftChild;
// 叔节点是红色的
if ( un != NULL && un->color == RED )
{
fa->color = BLACK;
un->color = BLACK;
gfa->color = RED;
node = gfa;
continue;
}
else
{
// RL型,右旋父节点,旋转完后会变成RR型
if ( fa->leftChild == node )
{
rightRotate( fa );
std::swap( fa, node );
}
// RR型,左旋祖父节点
fa->color = BLACK;
gtemplate<class T>
void RBTree<T>::erase( RBTNode<T>* node )
{
if ( node == NULL )
{
return ;
}
RBTNode<T> *child, *father;
RBTColor color;
// 情况1
if ( node->leftChild == NULL && node->rightChild == NULL )
{
// 情况1.2
if ( node->color == BLACK )
{
if ( node == root )
{
eraseNode( node );
return ;
}
eraseFix( node );
}
}
// 情况2
else if ( node->leftChild == NULL || node->rightChild == NULL )
{
child = node->leftChild ? node->leftChild : node->rightChild;
std::swap( node->value, child->value );
erase( child );
return ;
}
// 情况3,有两个孩子
else
{
// 待删节点的后继节点,用它来取代待删节点
RBTNode<T> *replace = node;
// 求后继节点,后继节点是右孩子的最左下角的孩子
replace = replace->rightChild;
while ( replace->leftChild != NULL )
{
replace = replace->leftChild;
}
std::swap( node->value, replace->value );
erase( replace );
return ;
}
eraseNode( node );
}fa->color = RED;
leftRotate( gfa );
}
}
}
root->color = BLACK;
}
删除
正常删除
还是和插入一样,先按照正常的二叉搜索树进行删除,这边先调用一下查询函数根据数值查到对应的节点。
二叉搜索树的删除:
- 为叶节点,直接删除。
- 只有一个儿子,儿子代替自己。
- 有两个儿子,先将后继节点(中序遍历的下一个节点)复制到当前节点,然后删除后继节点。后继节点要不有一个儿子要不就没有,按照情况1和2进行处理。
template<class T>
void RBTree<T>::erase( RBTNode<T>* node )
{
if ( node == NULL )
{
return ;
}
RBTNode<T> *child, *father;
RBTColor color;
// 情况1
if ( node->leftChild == NULL && node->rightChild == NULL )
{
// 情况1.2
if ( node->color == BLACK )
{
if ( node == root )
{
eraseNode( node );
return ;
}
eraseFix( node );
}
}
// 情况2
else if ( node->leftChild == NULL || node->rightChild == NULL )
{
child = node->leftChild ? node->leftChild : node->rightChild;
std::swap( node->value, child->value );
erase( child );
return ;
}
// 情况3,有两个孩子
else
{
// 待删节点的后继节点,用它来取代待删节点
RBTNode<T> *replace = node;
// 求后继节点,后继节点是右孩子的最左下角的孩子
replace = replace->rightChild;
while ( replace->leftChild != NULL )
{
replace = replace->leftChild;
}
std::swap( node->value, replace->value );
erase( replace );
return ;
}
eraseNode( node );
}
删除修正
template<class T>
void RBTree<T>::eraseFix( RBTNode<T>* node )
{
if ( node == NULL || node == root )
{
return ;
}
RBTNode<T> *brother, *father;
father = node->father;
while ( ( node == NULL || node->color == BLACK ) && node != root )
{
// 当前节点是左孩子
if ( node = father->leftChild )
{
brother = father->rightChild;
// 1. 兄弟是红色的
if ( brother != NULL && brother->color == RED )
{
brother->color = BLACK;
father->color = RED;
leftRotate( father );
brother = father->rightChild;
}
if ( ( brother->leftChild == NULL || brother->leftChild->color == BLACK ) &&
( brother->rightChild == NULL || brother->rightChild->color == BLACK) )
{
brother->color = RED;
node = father;
father = node->father;
}
else
{
if ( brother->rightChild == NULL || brother->rightChild->color == BLACK )
{
if ( brother->leftChild != NULL )
{
brother->leftChild->color = BLACK;
}
brother->color = RED;
rightRotate( brother );
brother = father->rightChild;
}
brother->color = father->color;
father->color = BLACK;
if ( brother->rightChild )
{
brother->rightChild->color = BLACK;
}
leftRotate( father );
node = root;
break;
}
}
else
{
brother = father->leftChild;
// 1. 兄弟是红色的
if ( brother != NULL && brother->color == RED )
{
brother->color = BLACK;
father->color = RED;
rightRotate( father );
brother = father->leftChild;
}
if ( ( brother->leftChild == NULL || brother->leftChild->color == BLACK ) &&
( brother->rightChild == NULL || brother->rightChild->color == BLACK) )
{
brother->color = RED;
node = father;
father = node->father;
}
else
{
if ( brother->leftChild == NULL || brother->leftChild->color == BLACK )
{
if ( brother->rightChild != NULL )
{
brother->rightChild->color = BLACK;
}
brother->color = RED;
leftRotate( brother );
brother = father->leftChild;
}
brother->color = father->color;
father->color = BLACK;
if ( brother->leftChild )
{
brother->leftChild->color = BLACK;
}
rightRotate( father );
node = root;
break;
}
}
}
if ( node )
node->color = BLACK;
}
查询
查询比较简单,就和二叉排序树一样,二分的向下查询即可。
template<class T>
RBTNode<T>* RBTree<T>::find( RBTNode<T> *node, T &key )
{
if ( node == NULL || node->value == key)
{
return node;
}
if ( key < node->value )
{
return find( node->leftChild, key );
}
else
{
return find( node->rightChild, key );
}
}
测试
我们写完了红黑树不知道有没有写错,这边我写了一个测试函数来判断当前的树是不是红黑树,如果我们进行操作完后依然满足红黑树特性,则说明红黑树的编写没有问题。
- 那么如何判断是不是红黑树呢?
首先看一下5个特性:
- 每个节点是黑色或红色(所以叫红黑树)
- 根节点是黑色的
- 叶节点是黑色的(空的叶节点)
- 如果一个节点为红,子节点必须为黑
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点
首先特性1是不会有问题的,然后特性2也很好判断,然后3的话因为我们根本没有创建空的叶节点,所以也不会有问题。
关键是判断4和5。
特性4,跑一个深搜就行。
特性5,我的做法是在节点中加入一个新的参数,表示当前节点的路径包含的黑节点个数(初始为-1,如果发现存在不同数目的路径则为-2)。先跑一个深搜找到所有叶节点,然后用一个辅助函数从叶节点往回找,标记当前路径的黑节点数目。如果发现某个节点的值为-1就给他赋当前值,若为-2不操作,否则和他进行比较若不同则置为-2。然后在搜完子节点后判断当前点是否为-2,若为-2则说明特性5不满足。
注意还要判断一下是不是满足二叉搜索树。
/* 判断是不是二叉搜索树,判断红黑树的时候用 */
template<class T>
bool RBTree<T>::isBST( RBTNode<T> *node )
{
if ( node == NULL )
{
return true;
}
bool flagL = true, flagR = true;
if ( node->leftChild != NULL )
{
if ( node->leftChild->value >= node->value )
{
flagL = false;
}
}
if ( node->rightChild != NULL )
{
if ( node->value >= node->rightChild->value )
{
flagR = false;
}
}
if ( flagL == false || flagR == false )
{
return false;
}
if ( isBST( node->leftChild ) && isBST( node->rightChild ) )
{
return true;
}
return false;
}
/* 判断是不是红黑树,测试用 */
template<class T>
bool RBTree<T>::isRBT()
{
if ( root == NULL )
{
return true;
}
if ( !isBST( root ) )
{
std::cout << "Not a BST!" << std::endl;
return false;
}
if ( root->color == RED )
{
std::cout << "Root is red!" << std::endl;
return false;
}
if ( hasTwoRed( root ) )
{
return false;
}
if ( !hasSameBlack( root ) )
{
return false;
}
return true;
}
/* 判断是否有两个连续的红色节点 */
template<class T>
bool RBTree<T>::hasTwoRed( RBTNode<T>* node )
{
if ( node == NULL )
{
return false;
}
if ( node != root )
{
if ( node->father->color == RED && node->color == RED )
{
std::cout << "Has tow red node!" << std::endl;
std::cout << "node value = " << node->value << std::endl;
return true;
}
}
if ( hasTwoRed( node->leftChild ) || hasTwoRed( node->rightChild ) )
{
return true;
}
return false;
}
/* 判断每个节点的所有路径上的黑色节点个数是否相等 */
template<class T>
bool RBTree<T>::hasSameBlack( RBTNode<T>* node )
{
if ( node == NULL )
{
return true;
}
node->blackNum = -1;
if ( node->leftChild == NULL && node->rightChild == NULL)
{
calBlackNum( node, 0 );
return true;
}
if ( node->leftChild )
{
if ( !hasSameBlack( node->leftChild ) )
{
return false;
}
}
if ( node->rightChild )
{
if ( !hasSameBlack( node->rightChild ) )
{
return false;
}
}
if ( node->blackNum == -2 )
{
std::cout << "Don't has same black node!" << std::endl;
std::cout << "node value = " << node->value << std::endl;
return false;
}
return true;
}
/* 辅助函数,用于计数路径上黑色节点个数 */
template<class T>
void RBTree<T>::calBlackNum( RBTNode<T>* node, int blackNum )
{
if ( node == NULL )
{
return ;
}
if ( node->color == BLACK )
{
if ( node->blackNum == -1 )
{
node->blackNum = ++blackNum;
}
else if ( node->blackNum != -2 )
{
if ( node->blackNum != ++blackNum )
{
node->blackNum = -2;
}
}
}
if ( node->father != NULL )
{
calBlackNum( node->father, blackNum );
}
}
实现set和map
放在下一篇文章了。
https://blog.csdn.net/Radium_1209/article/details/104952296