一、树的基本概念
1.1 什么是树?
树是一种非线性的数据结构,由节点和边组成,具有层次关系。树的根节点没有父节点,而其他节点每个都有一个父节点,并且可以有多个子节点。树形结构广泛应用于表示具有层次关系的数据
1.2 树的相关术语
- 根节点:树的最顶端节点
- 叶子节点:没有子节点的节点
- 父节点:有子节点的节点
- 子节点:父节点直接连接的节点
- 树的度:节点拥有的子树数目。整棵树中度最大的节点的度称为树的度
- 高度:从根节点到叶子节点的最长路径
- 层次:根节点的层次为1,其子节点为第2层,以此类推
- 森林:有n棵互不相交的树组成的集合
二、树的分类
2.1 二叉树(Binary Tree)
二叉树是树的特殊形式,每个节点最多有两个子节点,称为左子节点 和 右子节点。二叉树的子树有顺序,左、右子树不能颠倒
2.2 二叉搜索树(Binary Search Tree,BST)
二叉搜索树(BST)的特点是左子树节点的值小于根节点,右子树节点的值大于根节点,每个子树也是二叉搜索树
2.3 平衡二叉树
在BST中,如果树的高度差过大,可能导致退化成链表,效率下降。平衡二叉树通过保持树的平衡性,提高操作效率。常见的有AVL树、红黑树等(也就是说为了避免二叉搜索树在极端情况下退化成链表)
2.4 红黑树
红黑树是一种自平衡的二叉搜索树,它在插入和删除操作时保持树的平衡,使时间复杂度维持在 O(logn)
红黑树性质:
- 每个节点是红色或黑色。
- 根节点是黑色。
- 每个叶子节点(NIL 节点)是黑色。
- 红色节点的子节点必须是黑色(即,红色节点不能连续)。
- 从任意节点到其每个叶子节点的所有路径都包含相同数量的黑色节点
这五条规则的存在保证了树不会因为连续插入或删除节点而变得太不平衡
简单介绍:
平衡树是指任意节点的两个子树的高度差(也称为平衡因子)不超过某个预定值的树,如果树的高度差过大,意味着树的结构变得不平衡,在极端情况下,当树的高度差达到最大值(即树退化为一条直线时),它就变成了链表
三、树的基本操作
3.1 二叉树节点的结构定义
树通常通过指针来实现,每个节点包含一个数据域和两个指针,分别指向左右子节点
#include <stdio.h>
#include <stdlib.h> // 标准库,用于动态内存分配(如malloc函数)
// 定义二叉树节点的结构体
struct TreeNode {
int data; // 节点存储的数据
struct TreeNode* left; // 指向左子节点的指针
struct TreeNode* right; // 指向右子节点的指针
};
// 创建一个新节点的函数
// 参数:data - 要存储在新节点中的数据
// 返回值:指向新创建的节点的指针
struct TreeNode* createNode(int data) {
// 动态分配内存来存储一个新的TreeNode结构体
struct TreeNode* newNode = (struct TreeNode*)malloc(sizeof(struct TreeNode));
// 检查内存分配是否成功(虽然这里没有显式错误处理,但malloc失败时应处理)
// 为新节点的数据字段赋值
newNode->data = data;
// 初始化新节点的左右子节点指针为NULL,表示这是一个叶子节点或暂时没有其他子节点
newNode->left = NULL;
newNode->right = NULL;
// 返回指向新节点的指针
return newNode;
}
3.2 二叉树的遍历(访问树中每个节点的过程)
- 前序遍历(Pre-order Traversal):先访问根节点,再访问左子树,最后访问右子树。
- 中序遍历(In-order Traversal):先访问左子树,再访问根节点,最后访问右子树。
- 后序遍历(Post-order Traversal):先访问左子树,再访问右子树,最后访问根节点。
前序遍历:
// 前序遍历二叉树的函数
// 参数:root - 指向二叉树根节点的指针
void preOrder(struct TreeNode* root) {
// 如果当前节点为空(即到达了叶子节点的下一个空位置),则直接返回
if (root == NULL) return;
// 访问当前节点:打印当前节点的数据
printf("%d ", root->data);
// 递归地前序遍历左子树
preOrder(root->left);
// 递归地前序遍历右子树
preOrder(root->right);
}
中序:
void inOrder(struct TreeNode* root) {
if (root == NULL) return;
inOrder(root->left);
printf("%d ", root->data);
inOrder(root->right);
}
后序:
void postOrder(struct TreeNode* root) {
if (root == NULL) return;
postOrder(root->left);
postOrder(root->right);
printf("%d ", root->data);
}
3.3 插入节点到二叉搜索树
在二叉搜索树中,插入节点的操作根据其值来决定插入位置。如果新节点的值小于当前节点的值,则递归插入到左子树;反之,插入到右子树
// 在二叉搜索树中插入一个新节点的函数
// 参数:root - 指向二叉搜索树根节点的指针;data - 要插入的新节点的数据
// 返回值:指向更新后的二叉搜索树根节点的指针(可能是新节点,也可能是原节点)
struct TreeNode* insert(struct TreeNode* root, int data) {
// 如果当前节点为空,说明已经到达了应该插入新节点的位置
if (root == NULL) {
// 创建一个新节点,并返回指向它的指针
// 这里假设createNode函数已经正确定义,并且能够返回一个有效的TreeNode指针
return createNode(data);
}
// 如果要插入的数据小于当前节点的数据,则递归地在左子树中插入
if (data < root->data) {
// 插入后,左子树可能会长高(即根节点可能会变化),因此更新root->left
root->left = insert(root->left, data);
}
// 否则,如果要插入的数据大于或等于当前节点的数据,则递归地在右子树中插入
else {
// 注意:在标准的二叉搜索树中,我们通常不允许插入重复的数据
// 但这里为了简化,我们允许data == root->data的情况,并将其插入到右子树
// 如果不允许重复数据,应该在else分支中添加额外的逻辑来处理这种情况
// 插入后,右子树可能会长高(即根节点可能会变化),因此更新root->right
root->right = insert(root->right, data);
}
// 返回当前节点的指针(在递归调用中,这将是更新后的子树的根节点)
// 注意:在递归调用中,每一层都会返回其对应子树的根节点,最终返回到最初的调用者
return root;
}
3.4 查找节点
查找节点的操作也是基于二叉搜索树的特性。如果要查找的值小于当前节点的值,则递归在左子树中查找;反之,查找右子树
// 在二叉搜索树中查找一个节点的函数
// 参数:root - 指向二叉搜索树根节点的指针;key - 要查找的节点的值
// 返回值:指向找到的节点的指针(如果找到),否则返回NULL
struct TreeNode* search(struct TreeNode* root, int key) {
// 如果当前节点为空,说明已经到达了叶子节点的下一个空位置,没有找到目标节点
// 如果当前节点的值等于要查找的值,说明找到了目标节点
if (root == NULL || root->data == key) {
// 返回当前节点的指针(可能是NULL,表示没有找到;也可能是找到的节点的指针)
return root;
}
// 如果要查找的值小于当前节点的值,则递归地在左子树中查找
if (key < root->data) {
// 返回在左子树中查找的结果
// 注意:这里返回的是指向找到的节点的指针,或者如果左子树中没有找到,则返回NULL
return search(root->left, key);
}
// 如果要查找的值大于当前节点的值(由于二叉搜索树的性质,这里不会等于),则递归地在右子树中查找
// 注意:这里没有显式的else语句,因为前面的if已经处理了key < root->data的情况
// 所以,当key >= root->data且前面的if条件不满足时,会自动执行这个分支(尽管在标准BST中key不会等于root->data)
// 但为了代码的清晰性和健壮性,我们仍然保留这个分支来处理key > root->data的情况
return search(root->right, key);
// 注意:这里没有额外的return语句,因为上面的两个分支已经涵盖了所有可能的情况
// 每个分支都会返回一个指向节点的指针(可能是NULL)
}
3.5 删除节点
分为以下三种情况:
- 要删除的节点是叶子节点,直接删除
- 要删除的节点有一个子节点,用该子节点替代它
- 要删除的节点有两个子节点,需找到其右子树的最小值或左子树的最大值替代它
// 在二叉搜索树中删除一个节点的函数 // 参数:root - 指向二叉搜索树根节点的指针;key - 要删除的节点的值 // 返回值:指向更新后的二叉搜索树根节点的指针 struct TreeNode* deleteNode(struct TreeNode* root, int key) { // 如果当前节点为空,说明已经到达了叶子节点的下一个空位置,无需删除,直接返回NULL(或root,这里返回root也是相同的,因为root是NULL) if (root == NULL) return root; // 如果要删除的值小于当前节点的值,则递归地在左子树中删除 if (key < root->data) { root->left = deleteNode(root->left, key); } // 如果要删除的值大于当前节点的值,则递归地在右子树中删除 else if (key > root->data) { root->right = deleteNode(root->right, key); } // 如果找到了要删除的节点(即key == root->data) else { // 如果当前节点没有左子树,则直接用右子树替换当前节点,并释放当前节点的内存 if (root->left == NULL) { struct TreeNode* temp = root->right; free(root); // 释放内存 return temp; // 返回右子树作为新的子树根节点 } // 如果当前节点没有右子树,则直接用左子树替换当前节点,并释放当前节点的内存 else if (root->right == NULL) { struct TreeNode* temp = root->left; free(root); // 释放内存 return temp; // 返回左子树作为新的子树根节点 } // 如果当前节点既有左子树又有右子树,则找到右子树中的最小值节点 // 用该最小值节点的值替换当前节点的值,并删除右子树中的那个最小值节点 struct TreeNode* temp = minValueNode(root->right); // 找到右子树中的最小值节点 root->data = temp->data; // 用最小值节点的值替换当前节点的值 root->right = deleteNode(root->right, temp->data); // 删除右子树中的最小值节点 } // 返回更新后的子树根节点的指针 return root; } // 在给定的子树中找到最小值节点的函数 // 参数:node - 指向子树根节点的指针 // 返回值:指向最小值节点的指针 struct TreeNode* minValueNode(struct TreeNode* node) { struct TreeNode* current = node; // 从根节点开始 // 不断向左子树移动,直到找到没有左子树的节点(即最小值节点) while (current && current->left != NULL) current = current->left; // 返回最小值节点的指针 return current; }
四、红黑树(这东西难)(回顾一下之前写的性质)
1. 什么是红黑树?
红黑树是一种 自平衡的二叉搜索树(BST),它的主要目标是通过一些规则来确保树的高度保持相对较低,这样查找、插入和删除操作的效率会更高。树的平衡性越好,操作的时间复杂度就越接近 O(logn),其中 n 是树的节点数
2. 红黑树和普通的二叉搜索树(BST)有什么区别?
普通的二叉搜索树有个问题,如果插入的节点顺序不太好(例如递增顺序),树就会变成一条“链”(也就是所谓的退化成链表),查找效率就会变差,接近 O(n)。这是因为树不平衡,导致树的高度变得很高
红黑树通过一系列规则来避免这种情况,保证树的平衡性。它引入了“颜色”和“旋转”的概念,确保在插入或删除节点时,树始终保持一个相对的平衡状态
3. 插入操作简化理解(回顾一下之前写的性质)
插入节点的步骤可以概括为三部分:
像二叉搜索树那样找到插入的位置:这意味着你要找到合适的位置插入新节点,仍然保证左子树比根小,右子树比根大
新插入的节点总是着色为红色:因为如果插入的是黑色节点,可能会直接破坏红黑树的黑高平衡。所以,所有新插入的节点默认是红色
通过颜色和旋转来恢复红黑树的平衡:插入一个红色节点后,可能会出现两个连续的红色节点的情况(违反规则4)。此时就需要通过“颜色修正”和“旋转”来解决
4. 旋转
可以把红黑树的 旋转操作 想象成是你在做“微调”,调整树的结构,使其更加平衡。旋转分为 左旋 和 右旋 两种方式
- 左旋:将节点的右孩子提升,节点自己变成左孩子
- 右旋:与左旋相反,将节点的左孩子提升,节点自己变成右孩子
每次旋转操作并不会破坏二叉搜索树的基本性质(左小右大的结构),而是帮助恢复平衡
注意一下这里的“孩子”:
在讨论二叉树的旋转操作时,术语“左孩子”和“右孩子”指的是二叉树节点之间的连接关系。在二叉树中,每个节点最多有两个子节点,分别称为左孩子和右孩子
5.插入
假设我们有一棵红黑树,刚插入了一个节点,导致违反了连续红色节点的规则:
- 如果父节点是红色,且叔叔节点也是红色:只需要将父节点和叔叔节点涂黑,然后将祖父节点涂红即可。
- 如果父节点是红色,而叔叔节点是黑色或
NULL
:这时需要进行旋转操作,以保持红黑树的平衡
红黑树的关键是它引入了颜色和旋转操作,以保证树在插入或删除节点后仍然保持平衡
下面详细介绍红黑树的插入操作,以及如何通过旋转和颜色调整来维持红黑树的平衡
还是写一下性质吧:
红黑树的性质:
- 节点是红色或黑色
- 根节点是黑色
- 所有叶子节点(
NULL
节点)都是黑色 - 如果一个节点是红色,则它的子节点必须是黑色(不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(黑高)
-
新插入的节点默认是红色:这有助于保持红黑树的平衡,避免直接破坏黑高
-
根节点始终着色为黑色:在修复过程中,可能会将根节点着色为红色,需要在最后将根节点重新着色为黑色
插入操作主要包括以下步骤:
- 按照二叉搜索树的规则插入节点:找到适当的位置插入新节点,保证二叉搜索树的性质
- 将新插入的节点着色为红色:这有助于保持红黑树的特性,特别是保持红黑树的黑高平衡
- 通过调整颜色和旋转来修复红黑树的性质:插入红色节点可能会破坏红黑树的平衡,需要通过颜色变换和旋转来恢复
enum Color { RED, BLACK }; struct RBTreeNode { int data; struct RBTreeNode *left, *right, *parent; enum Color color; }; // 左旋操作 void leftRotate(struct RBTreeNode **root, struct RBTreeNode *x) { struct RBTreeNode *y = x->right; // y为x的右孩子 x->right = y->left; if (y->left != NULL) y->left->parent = x; y->parent = x->parent; if (x->parent == NULL) *root = y; // x是根节点 else if (x == x->parent->left) x->parent->left = y; else x->parent->right = y; y->left = x; x->parent = y; } // 右旋操作 void rightRotate(struct RBTreeNode **root, struct RBTreeNode *y) { struct RBTreeNode *x = y->left; // x为y的左孩子 y->left = x->right; if (x->right != NULL) x->right->parent = y; x->parent = y->parent; if (y->parent == NULL) *root = x; // y是根节点 else if (y == y->parent->left) y->parent->left = x; else y->parent->right = x; x->right = y; y->parent = x; } // 插入修复函数 void insertFixUp(struct RBTreeNode **root, struct RBTreeNode *z) { while (z->parent != NULL && z->parent->color == RED) { if (z->parent == z->parent->parent->left) { struct RBTreeNode *y = z->parent->parent->right; // y为叔叔节点 if (y != NULL && y->color == RED) { // Case 1: 叔叔是红色 z->parent->color = BLACK; y->color = BLACK; z->parent->parent->color = RED; z = z->parent->parent; // 向上继续修复 } else { if (z == z->parent->right) { // Case 2: 叔叔是黑色,且当前节点是右孩子 z = z->parent; leftRotate(root, z); } // Case 3: 叔叔是黑色,且当前节点是左孩子 z->parent->color = BLACK; z->parent->parent->color = RED; rightRotate(root, z->parent->parent); } } else { // 对称的情况 struct RBTreeNode *y = z->parent->parent->left; // y为叔叔节点 if (y != NULL && y->color == RED) { // Case 1 z->parent->color = BLACK; y->color = BLACK; z->parent->parent->color = RED; z = z->parent->parent; } else { if (z == z->parent->left) { // Case 2 z = z->parent; rightRotate(root, z); } // Case 3 z->parent->color = BLACK; z->parent->parent->color = RED; leftRotate(root, z->parent->parent); } } } (*root)->color = BLACK; // 根节点始终为黑色 } // 创建新节点 struct RBTreeNode* createRBNode(int data) { struct RBTreeNode* node = (struct RBTreeNode*)malloc(sizeof(struct RBTreeNode)); node->data = data; node->left = node->right = node->parent = NULL; node->color = RED; // 新插入的节点默认为红色 return node; } // 插入节点 void insertRBTree(struct RBTreeNode **root, int data) { struct RBTreeNode *z = createRBNode(data); struct RBTreeNode *y = NULL; struct RBTreeNode *x = *root; // 找到插入位置 while (x != NULL) { y = x; if (z->data < x->data) x = x->left; else x = x->right; } z->parent = y; if (y == NULL) *root = z; // 树为空,新节点为根 else if (z->data < y->data) y->left = z; else y->right = z; // 插入后修复红黑树性质 insertFixUp(root, z); }
代码解释:
-
左旋和右旋操作:
- 左旋:以节点
x
为支点,将其右孩子y
上移,x
成为y
的左孩子,y
的左子树成为x
的右子树 - 右旋:与左旋对称,以节点
y
为支点,将其左孩子x
上移,y
成为x
的右孩子,x
的右子树成为y
的左子树
- 左旋:以节点
-
插入修复函数
insertFixUp
:-
目的:在插入新节点后,通过重新着色和旋转操作,修复红黑树可能违反的性质,确保树的平衡
-
修复逻辑:
-
Case 1(叔叔节点是红色):
- 将父节点和叔叔节点着色为黑色
- 将祖父节点着色为红色
- 将当前节点指向祖父节点,继续向上修复
-
Case 2(叔叔节点是黑色,当前节点是右孩子):
- 以父节点为支点进行左旋
- 左旋后,当前节点指向父节点(旋转后的)
-
Case 3(叔叔节点是黑色,当前节点是左孩子):
- 将父节点着色为黑色,祖父节点着色为红色
- 以祖父节点为支点进行右旋
-
-
对称处理:如果当前节点的父节点是祖父节点的右孩子,那么上述情况的左右操作需要互换,即左旋变为右旋,右旋变为左旋
-
-
插入操作
insertRBTree
:- 按照二叉搜索树的插入方式找到插入位置
- 插入新节点后,调用
insertFixUp
进行修复