目录
一、基本概念
红黑树(Red-Black Tree)是一种自平衡的二叉查找树。在红黑树中,每个节点都有一个颜色,红色或黑色。通过对任何一条从根到叶子的路径上各个节点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出两倍,因此它大致是平衡的。
红黑树的主要特点包括:
- 节点颜色:每个节点被涂成红色或黑色。
- 根节点性质:根节点总是黑色的。
- 红色节点性质:如果一个节点是红色的,则它的两个子节点都是黑色的(也就是说,红色节点不能相邻)。
- 黑色深度性质:从任一节点到其任何叶子的所有路径,都包含相同数目的黑色节点。
- 叶子节点性质:所有叶子节点(NIL节点,树尾端的虚节点)都是黑色的。
红黑树通过旋转和重新着色等调整操作,在插入和删除节点时维护这些性质,以保持树的平衡,从而保证查找、插入、删除的最坏情况时间复杂度为O(log n)。这种特性使得红黑树在计算集合和映射等数据结构中非常有用。
让我们通过插入操作的例子来讲解红黑树的调整过程。假设我们有一个初始的红黑树,我们要在这个树中插入一个新节点。我们从树的根节点开始,寻找插入新节点的适当位置,并将新节点着色为红色。插入新节点后,我们需要调整树以保持红黑树的性质。
二、示例
假设我们要向以下红黑树中插入键值为 11 的节点:
7(黑)
/ \
3(红) 18(红)
/ \ / \
2(黑) 5(黑) 14(黑)
步骤 1: 正常的二叉搜索树插入
- 我们首先按照二叉搜索树的规则插入节点 11。节点 11 将被插入在节点 14 的左侧,并且将节点 11 着色为红色。
7(黑)
/ \
3(红) 18(红)
/ \ / \
2(黑) 5(黑) 14(黑)
/
11(红)
步骤 2: 检查红黑性质
- 插入后,红色节点 11 和其父节点 14 均为红色,违反了红黑树的性质(红色节点的子节点必须是黑色)。
步骤 3: 重新着色与旋转
- 为了修复这个问题,我们可以进行重新着色和旋转。首先,我们更改节点 18 和其子节点 14 的颜色。节点 18 变为黑色,节点 14 变为红色。然后,检查根节点 7,因为根节点总是黑色,无需变动。
7(黑)
/ \
3(红) 18(黑)
/ \ / \
2(黑) 5(黑) 14(红)
/
11(红)
现在,树恢复了平衡和红黑性质,所有从根到叶子的路径包含相同数目的黑色节点,且没有连续的红色节点。
通过这个例子,你可以看到插入新节点后,如何通过着色和旋转来调整红黑树,确保树的平衡和满足红黑树的性质。这样的调整确保了红黑树操作的高效性,使得查找、插入和删除操作的时间复杂度均为O(log n)。
使用C++实现来插入节点到红黑树中,并通过一个例子演示插入操作。这个实现将包括节点结构、红黑树类的基础框架,以及一个简单的插入操作。为了清晰起见,我们会简化一些功能。
三、代码实现
定义节点结构和红黑树类
首先,定义节点结构以及基本的红黑树类:
#include <iostream>
enum Color { RED, BLACK };
struct Node {
int data;
Color color;
Node *left, *right, *parent;
// 节点构造函数
Node(int data) : data(data), color(RED), left(nullptr), right(nullptr), parent(nullptr) {}
};
class RedBlackTree {
private:
Node *root;
// 左旋转函数
void rotateLeft(Node *&ptr) {
Node *right_child = ptr->right;
ptr->right = right_child->left;
if (ptr->right != nullptr)
ptr->right->parent = ptr;
right_child->parent = ptr->parent;
if (ptr->parent == nullptr)
root = right_child;
else if (ptr == ptr->parent->left)
ptr->parent->left = right_child;
else
ptr->parent->right = right_child;
right_child->left = ptr;
ptr->parent = right_child;
}
// 右旋转函数
void rotateRight(Node *&ptr) {
Node *left_child = ptr->left;
ptr->left = left_child->right;
if (ptr->left != nullptr)
ptr->left->parent = ptr;
left_child->parent = ptr->parent;
if (ptr->parent == nullptr)
root = left_child;
else if (ptr == ptr->parent->left)
ptr->parent->left = left_child;
else
ptr->parent->right = left_child;
left_child->right = ptr;
ptr->parent = left_child;
}
// 修复红黑树性质的违反
void fixViolation(Node *&ptr) {
Node *parent = nullptr;
Node *grandparent = nullptr;
// 如果ptr不是根并且存在双红色违规,继续修复
while ((ptr != root) && (ptr->color != BLACK) && (ptr->parent->color == RED)) {
parent = ptr->parent;
grandparent = parent->parent;
// 父节点是祖父节点的左孩子
if (parent == grandparent->left) {
Node *uncle = grandparent->right;
// 情况1: 叔叔节点是红色
if (uncle != nullptr && uncle->color == RED) {
grandparent->color = RED;
parent->color = BLACK;
uncle->color = BLACK;
ptr = grandparent;
} else {
// 情况2: 节点是其父节点的右孩子
if (ptr == parent->right) {
rotateLeft(parent);
ptr = parent;
parent = ptr->parent;
}
// 情况3: 节点是其父节点的左孩子
rotateRight(grandparent);
std::swap(parent->color, grandparent->color);
ptr = parent;
}
} else {
// 父节点是祖父节点的右孩子,与上面的情况对称
Node *uncle = grandparent->left;
if (uncle != nullptr && uncle->color == RED) {
grandparent->color = RED;
parent->color = BLACK;
uncle->color = BLACK;
ptr = grandparent;
} else {
if (ptr == parent->left) {
rotateRight(parent);
ptr = parent;
parent = ptr->parent;
}
rotateLeft(grandparent);
std::swap(parent->color, grandparent->color);
ptr = parent;
}
}
}
// 确保根节点总是黑色
root->color = BLACK;
}
public:
RedBlackTree() : root(nullptr) {}
// 插入新节点
void insert(const int &data) {
Node *node = new Node(data);
root = BSTInsert(root, node);
fixViolation(node);
}
// 二叉搜索树插入并维持父子关系
Node* BSTInsert(Node* root, Node* ptr) {
if (root == nullptr)
return ptr;
if (ptr->data < root->data) {
root->left = BSTInsert(root->left, ptr);
root->left->parent = root;
} else if (ptr->data > root->data) {
root->right = BSTInsert(root->right, ptr);
root->right->parent = root;
}
return root;
}
};
插入操作的例子
下面我们插入几个节点来看看红黑树是如何工作的:
int main() {
RedBlackTree tree;
tree.insert(7);
tree.insert(3);
tree.insert(18);
tree.insert(10);
tree.insert(22);
tree.insert(8);
tree.insert(11);
// 插入数值后,树自动调整保持红黑树的性质
return 0;
}
这段代码中的红黑树将在每次插入操作后自动调整,以保持红黑树的性质。具体的调整包括了左旋、右旋和重新着色等操作。
四、补充说明:
左旋和右旋是两种基本的操作,用来保持树的平衡。这些操作可以帮助我们调整树的结构,确保搜索、插入和删除操作的效率。
右旋(Right Rotation)
右旋是将节点向右下方旋转。这通常在插入一个左孩子后,或者为了保持树的平衡而需要进行的操作。
考虑下面的树,其中需要对节点 Y
进行右旋:
Y X
/ \ / \
X C ---> A Y
/ \ / \
A B B C
在右旋操作中,Y
成为 X
的右子节点,X
的右子节点 B
成为 Y
的左子节点。这个操作后,X
占据了之前 Y
的位置。
左旋(Left Rotation)
左旋是将节点向左下方旋转。这通常在插入一个右孩子后,或者为了保持树的平衡而需要进行的操作。
考虑下面的树,其中需要对节点 X
进行左旋:
X Y
/ \ / \
A Y ---> X C
/ \ / \
B C A B
在左旋操作中,X
成为 Y
的左子节点,Y
的左子节点 B
成为 X
的右子节点。这个操作后,Y
占据了之前 X
的位置。
在代码中的表现
这些旋转操作在红黑树的代码中体现在如何改变指针的指向来调整节点的父子关系。旋转操作是平衡树的关键部分,它们帮助保持树的深度最小化,从而保证树操作的效率。
通过旋转,我们可以确保树的任何一条从根到叶子的路径都大致相等,无一条路径会比其他路径长出很多,这是红黑树平衡性的体现。这样的特性使得红黑树在性能上是非常高效的,特别是在执行查找、插入和删除操作时能保持对数时间复杂度。
右旋举例
在进行右旋操作时,关于节点 X
的右子节点 B
和节点 Y
的排布是关键的一步。这里我会详细解释节点 X
的右子节点 B
如何成为节点 Y
的左子节点,以及这种操作的原因。
右旋示例
假设我们有以下结构,其中需要对节点 Y
进行右旋:
Y X
/ \ / \
X C ---> A Y
/ \ / \
A B B C
右旋的步骤
在这个结构中,我们对节点 Y
进行右旋,具体的步骤如下:
-
设置
X
的父节点:节点X
升级为这部分子树的新根节点,所以它的父节点将变成原节点Y
的父节点。 -
移动
B
到Y
的左子节点:- 在旋转前,
B
是X
的右子节点。 - 旋转时,
X
被推向上占据Y
的位置,而Y
下降成为X
的右子节点。 - 因为
Y
下降为X
的子节点,B
需要调整位置以保持树的二叉搜索属性。因此,B
移动成为Y
的左子节点。
- 在旋转前,
-
调整
Y
的父节点:由于X
升级为根节点,Y
的新父节点是X
。 -
维护树的结构:
- 将
X
的原右子节点(即B
)设置为Y
的左子节点。 - 连接
Y
的左指针到B
,如果B
存在的话,设置B
的父指针指向Y
。
- 将
通过这种方式,旋转操作保持了二叉搜索树的性质,即左子树中所有节点的值都小于根节点的值,右子树中所有节点的值都大于根节点的值。同时,这种结构调整也有助于恢复或保持红黑树的平衡性质,特别是在插入和删除节点时。
右旋操作后,节点 X
和 Y
的关系及其子树的布局被调整,但二叉搜索树的基本要求和红黑树的特性仍然得到保持。这样的操作是红黑树自平衡机制的一部分,确保了操作的效率和性能。