红黑树
一、概念
- 红黑树是一种自平衡、高效的二叉搜索树,由 Rudolf Bayer 于1978年发明,在当时被称为平衡二叉 B 树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的红黑树。
- 红黑树具有良好的效率,它可在O( l o g 2 N log_2 N log2N)时间内完成查找、插入、删除等操作。
- 红黑树通过对任何一条从根节点到叶子节点的路径上各个结点着色方式的限制,即通过5条性质的限制,使红黑树最长路径的长度不超过最短路径的长度的两倍。
- 红黑树最短路径为对应路径上所有节点都为黑色节点,最长路径为对应路径上节点是一个红色节点与一个黑色节点相间的。这两种情况在红黑树中可能不会存在,只是两个极值,多数路径的长度在这两者之间。
- NIL节点的作用是方便数路径和不容易数错。
二、性质
- 每个结点的颜色不是红色就是黑色。
- 根节点的颜色是黑色的。
- 如果一个节点的颜色是红色的,则它的两个孩子结点的颜色是黑色的。
- 对于每个结点,从该结点到其所有后代叶子结点的每条简单路径上,均包含相同数目的黑色结点。
- 每个叶子结点(空结点)的颜色都是黑色的。
三、节点结构
1、代码
enum color
{
RED,
BLACK
};
template<class T>
struct RBTreeNode
{
RBTreeNode(T data = T())
:_data(data),
_left(nullptr),
_right(nullptr),
_parent(nullptr),
_col(RED)
{}
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
T _data;
color _col;
};
template<class T>
class RBTree
{
typedef RBTreeNode<T> Node;
public:
RBTree()
{
_pHead = new Node;
_pHead->_left = _pHead;
_pHead->_right = _pHead;
}
private:
Node* _pHead;
};
2、实现原理
- 红黑树节点的颜色只有红色和黑色,所以,使用一个枚举变量进行定义,当在编写或者查看后续代码时,能比较清晰地知晓对应的变量值代表什么颜色。
- 红黑树的数据进行插入、删除等操作时,需要进行变色与旋转。此时,节点就需要知晓其父节点与其他节点,使用三叉链可以使寻找节点的操作变得简单。
- RBTreeNode类的构造函数中,data可以给一个匿名对象作为形参的缺省值。这样在下方实例化时,不用传实参。
- 在RBTreeNode类中,节点的颜色变量_col默认初始化为红色。因为当新增插入的节点颜色是黑色时,会影响所有路径;而当新增插入的节点颜色是红色时,只会影响父亲节点。
- 在RBTree类中,_pHead成员变量为一个标志的头节点,方便对后续代码中的判根操作和寻找最左和最后节点的操作。
四、变色
1、情况
- 当新增节点或更新后的节点(cur)的颜色为红色、其父亲节点的颜色为红色,祖父节点的颜色为黑色,叔叔节点存在且颜色为红色时,可以通过改变节点的颜色来解决。
- 因为祖父节点的颜色为黑色,而祖父节点是否有父亲节点是未知的。所以,该红黑树可能是一棵完整的树,即祖父节点就是根节点,也可能只是一棵子树,即祖父节点有父亲节点。
2、处理操作
- 将父亲节点和叔叔节点的颜色改为黑色,祖父节点的颜色改为红色,然后把祖父节点赋值给指针cur,继续向上调整。
- 如果祖父节点是根节点,调整完成后需要将祖父节点的颜色改为黑色,处理结束;如果祖父节点只是一棵子树中的一个节点,且祖父节点的父亲节点的颜色是红色,则需要继续向上调整,反之处理结束。
3、示意图
五、单旋
1、情况
- 新增节点或更新后的节点(cur)的颜色为红色,其父亲节点的颜色为红色,祖父节点的颜色为黑色,叔叔节点不存在或者存在且颜色为黑色时,无法通过变色解决,需要采用旋转加变色操作。
- 如果叔叔节点不存在,则cur节点一定是新插入的节点,因为如果cur节点不是新插入的节点,则cur节点和其父亲节一定有一个节点的颜色是黑色的,这时就不满足每条路径黑色节点个数相同的性质。
- 如果叔叔节点存在,则其颜色一定是黑色的,那么cur节点原来的颜色一定是黑色的,此时它是红色的原因是cur节点的子树在调整的过程中将cur节点的颜色由黑色改成了红色。
2、处理操作
- 当父亲节点为祖父节点的左孩子,cur节点为父亲节点的左孩子时,进行右单旋。
- 当父亲节点为祖父节点的右孩子,cur节点为父亲节点的右孩子时,进行左单旋。
- 父亲节点的颜色变为黑色,祖父节点的颜色变为红色。
3、示意图
- 此示意图为右单旋操作,左单旋操作参见AVL树详解
六、双旋
1、情况
- 当新增节点或更新后的节点(cur)的颜色为红色,其父亲节点的颜色为红色,祖父节点的颜色为黑色,叔叔节点不存在或者存在且颜色为黑色时,无法通过变色和单旋解决,需要采用双旋加变色操作。
2、处理操作
- 当父亲节点为祖父节点的左孩子,cur节点为父亲节点的右孩子时,进行左右双旋。
- 当父亲节点为祖父节点的右孩子,cur节点为父亲节点的左孩子时,进行右左双旋。
- cur节点的颜色变为黑色,祖父节点的颜色变为红色。
3、示意图
- 此示意图为左右双旋操作,右左双旋操作参见AVL树详解
七、插入节点
1、操作
- _pHead的父节点指向红黑树的根节点,当其为空时,说明整棵红黑树是空的。
- LeftMost与RightMost函数的作用为能较快地找到整棵红黑树的最小与最大节点。
- 插入节点前,需要先寻找插入位置再进行插入节点。因为插入的节点的颜色默认是红色的,可能会引发一些问题。所以,需要通过特定的情况进行解决。
- 旋转的函数实现参见AVL树详解树,在红黑树与AVL树的旋转代码中有一些差别。因为红黑树不存在平衡因子,所以,在红黑树的旋转代码中,需要删除对平衡因子的处理操作。
2、代码
bool Insert(const T& data)
{
if (_pHead->_parent == nullptr)
{
Node* root = new Node(data);
root->_col = BLACK;
_pHead->_parent = root;
_pHead->_left = LeftMost();
_pHead->_right = RightMost();
return true;
}
Node* cur = _pHead->_parent;
Node* parent = nullptr;
while (cur)
{
parent = cur;
if (cur->_data > data)
cur = cur->_left;
else if (cur->_data < data)
cur = cur->_right;
else
return false;
}
cur = new Node(data);
if (parent->_data > data)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
while (parent != nullptr && parent->_col == RED)
{
Node* grandFather = parent->_parent;
if (parent == grandFather->_left)
{
Node* uncle = grandFather->_right;
if (uncle && RED == uncle->_col)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandFather->_col = RED;
cur = grandFather;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)
{
RotateL(parent);
swap(cur, parent);
}
parent->_col = BLACK;
grandFather->_col = RED;
RotateR(grandFather);
}
}
else
{
Node* uncle = grandFather->_left;
if (uncle && RED == uncle->_col)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandFather->_col = RED;
cur = grandFather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
RotateR(parent);
swap(cur, parent);
}
parent->_col = BLACK;
grandFather->_col = RED;
RotateL(grandFather);
}
}
}
_pHead->_parent->_col = BLACK;
_pHead->_left = LeftMost();
_pHead->_right = RightMost();
}
Node* LeftMost()
{
Node* cur = _pHead->_parent;
if (cur == nullptr)
return _pHead;
while (cur->_left)
cur = cur->_left;
return cur;
}
Node* RightMost()
{
Node* cur = _pHead->_parent;
if (cur == nullptr)
return _pHead;
while (cur->_right)
cur = cur->_right;
return cur;
}
八、检测
1、代码
bool IsValidRBTRee()
{
if (_pHead->_parent == nullptr)
return true;
if (_pHead->_parent->_col == RED)
{
cout << "根节点颜色为红" << endl;
return false;
}
size_t Benchmark = 0;
Node* cur = _pHead->_parent;
while (cur)
{
if (cur->_col == BLACK)
++Benchmark;
cur = cur->_left;
}
return _IsValidRBTRee(_pHead->_parent, 0, Benchmark);
}
bool _IsValidRBTRee(Node* root, size_t blackCount, size_t Benchmark)
{
if (root == nullptr)
{
if (blackCount != Benchmark)
{
cout << "某条路径的黑色节点数量不相等" << endl;
return false;
}
return true;
}
if (root->_col == RED)
{
if (root->_parent->_col == RED)
{
cout << "连续的红色节点" << endl;
return false;
}
}
if (root->_col == BLACK)
++blackCount;
return _IsValidRBTRee(root->_left, blackCount, Benchmark)
&& _IsValidRBTRee(root->_right, blackCount, Benchmark);
}
2、实现原理
- IsValidRBTRee函数封装了_IsValidRBTRee函数,在调用检测函数时,只需调用IsValidRBTRee函数且无需传递参数。
- 当根节点(_pHead->_parent)为空时直接返回真而不进行其他操作,因为空树也属于红黑树。但如果根节点不为空,则其一定是黑色,反之这棵红黑树是错误的。
- 由于红黑树每条路径的黑色节点数是一样的,所以,使用一个变量Benchmark作为基准值。如果有一条路径的黑色节点数量不等于它时,说明这棵红黑树是错误的。
- 由于红黑树节点的父亲节点只有一个,而子节点有两个。所以,判断一个节点父亲节点的颜色比判断它的子节点的颜色容易。当节点的颜色为红色时,它的父亲节点的颜色一定是黑色,否则,这棵红黑树是错误的。
- 红黑树的每个节点都有可能出现错误。所以,需要对每个节点都进行判断,采用递归的方式可以达到这一目的。
九、红黑树与AVL树的比较
- 红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N)。
- 红黑树不追求绝对平衡,其只需保证最长路径的长度不超过最短路径的长度的2倍,相对而言,降低了插入和旋转的次数。
- 在经常进行增删的结构中,红黑树的性能比AVL树更优,而且红黑树的实现比较简单。所以,在实际运用中,使用红黑树比使用AVL树更多。
本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请点赞、收藏加关注支持一下💕💕💕