第十章 二叉搜索树:从理论到实战
二叉搜索树,简称 BST。说实话,这是我在工作中用得最多的数据结构之一。无论是做嵌入式系统的快速查找,还是做算法题的底层优化,BST 都是绕不开的基础。
今天我就带你彻底搞懂它。从定义到操作,从代码到性能,咱们一步步来。
什么是二叉搜索树?
先看定义。二叉搜索树是一棵二叉树,每个节点最多有两个孩子。但它有个特殊性质:
- 左子树上所有节点的值,都小于根节点的值
- 右子树上所有节点的值,都大于根节点的值
- 左右子树也分别是二叉搜索树
说白了,就是中序遍历的结果是一个递增序列。这个性质非常有用。
核心要点:BST 的每个节点都满足「左小右大」。这是所有操作的基础。
BST 的知识体系
下面这张图,是我自己总结的 BST 知识框架。你一看就明白。
查找操作
查找是 BST 最基础的操作。思路很简单:从根开始,比当前节点小就往左走,大就往右走,相等就找到了。
我个人习惯用递归实现,代码更清晰。但如果你对栈深度有要求,迭代版本更安全。
// 递归查找
TreeNode* bst_search(TreeNode* root, int key) {
if (root == NULL || root->val == key) {
return root;
}
if (key < root->val) {
return bst_search(root->left, key);
} else {
return bst_search(root->right, key);
}
}
// 迭代查找(推荐在嵌入式场景使用)
TreeNode* bst_search_iter(TreeNode* root, int key) {
while (root != NULL && root->val != key) {
if (key < root->val) {
root = root->left;
} else {
root = root->right;
}
}
return root;
}
我的经验:在嵌入式项目中,我一般用迭代版本。因为递归会消耗栈空间,而嵌入式系统的栈通常很小。有一次我在一个 STM32 项目里用递归,结果深度超过 100 层就栈溢出了。从那以后,我养成了「能用迭代就不用递归」的习惯。
插入操作
插入其实就是在查找的基础上,找到合适的位置挂上去。如果树是空的,直接新建节点。否则,比大小决定往左还是往右。
TreeNode* bst_insert(TreeNode* root, int key) {
if (root == NULL) {
TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
node->val = key;
node->left = node->right = NULL;
return node;
}
if (key < root->val) {
root->left = bst_insert(root->left, key);
} else if (key > root->val) {
root->right = bst_insert(root->right, key);
}
// 如果相等,什么也不做(或者根据需求覆盖)
return root;
}
你想想看,插入操作的时间复杂度是多少?其实就是查找的时间复杂度。平均 O(log n),最坏 O(n)。
注意:插入时如果 key 已经存在,不同场景处理方式不同。有的要求去重,有的要求计数。我在做数据库索引时,一般选择计数方式,保留重复值。
删除操作
删除是 BST 里最复杂的操作。为什么?因为删除一个节点后,还要保持 BST 的性质不变。
删除分三种情况:
- 情况一:删除叶子节点。直接删掉就行,父节点指向 NULL。
- 情况二:删除只有一个孩子的节点。让孩子顶替它的位置。
- 情况三:删除有两个孩子的节点。这个最麻烦,需要找前驱或后继来替换。
前驱就是左子树中最大的节点,后继就是右子树中最小的节点。我一般用后继,因为代码写起来顺手。
// 找最小值节点(用于删除操作)
TreeNode* find_min(TreeNode* root) {
while (root->left != NULL) {
root = root->left;
}
return root;
}
TreeNode* bst_delete(TreeNode* root, int key) {
if (root == NULL) return NULL;
if (key < root->val) {
root->left = bst_delete(root->left, key);
} else if (key > root->val) {
root->right = bst_delete(root->right, key);
} else {
// 找到了要删除的节点
// 情况一:叶子节点
if (root->left == NULL && root->right == NULL) {
free(root);
return NULL;
}
// 情况二:只有一个孩子
else if (root->left == NULL) {
TreeNode* temp = root->right;
free(root);
return temp;
} else if (root->right == NULL) {
TreeNode* temp = root->left;
free(root);
return temp;
}
// 情况三:有两个孩子
else {
TreeNode* successor = find_min(root->right);
root->val = successor->val;
root->right = bst_delete(root->right, successor->val);
}
}
return root;
}
避坑指南:我曾经在删除操作中忘记释放内存,导致内存泄漏。在嵌入式系统中,内存泄漏是致命问题。每次 free 之后,最好把指针置为 NULL,防止野指针。
性能分析
BST 的性能取决于树的高度。理想情况下,树是平衡的,高度为 O(log n)。最坏情况下,树退化成链表,高度为 O(n)。
| 操作 | 平均时间复杂度 | 最坏时间复杂度 |
|---|---|---|
| 查找 | O(log n) | O(n) |
| 插入 | O(log n) | O(n) |
| 删除 | O(log n) | O(n) |
| 中序遍历 | O(n) | O(n) |
为什么会退化?如果你按递增顺序插入数据,BST 就变成了一条右斜链。比如插入 1, 2, 3, 4, 5,树就长成了只有右孩子的链表。
为了解决这个问题,就有了平衡二叉树,比如 AVL 树和红黑树。这些是 BST 的进阶版本,后面章节会讲。
我的建议:如果你确定数据是随机分布的,普通 BST 完全够用。但如果数据有序或接近有序,一定要用平衡树。我在做日志系统时,时间戳是递增的,用普通 BST 直接崩了,换成 AVL 树才解决问题。
完整代码示例
下面是一个完整的 BST 实现,包含创建、插入、查找、删除和中序遍历。
#include <stdio.h>
#include <stdlib.h>
typedef struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
TreeNode* create_node(int val) {
TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
node->val = val;
node->left = node->right = NULL;
return node;
}
TreeNode* insert(TreeNode* root, int val) {
if (root == NULL) return create_node(val);
if (val < root->val)
root->left = insert(root->left, val);
else if (val > root->val)
root->right = insert(root->right, val);
return root;
}
TreeNode* search(TreeNode* root, int val) {
while (root && root->val != val) {
if (val < root->val) root = root->left;
else root = root->right;
}
return root;
}
TreeNode* find_min(TreeNode* root) {
while (root->left) root = root->left;
return root;
}
TreeNode* delete_node(TreeNode* root, int val) {
if (root == NULL) return NULL;
if (val < root->val)
root->left = delete_node(root->left, val);
else if (val > root->val)
root->right = delete_node(root->right, val);
else {
if (root->left == NULL) {
TreeNode* temp = root->right;
free(root);
return temp;
}
if (root->right == NULL) {
TreeNode* temp = root->left;
free(root);
return temp;
}
TreeNode* temp = find_min(root->right);
root->val = temp->val;
root->right = delete_node(root->right, temp->val);
}
return root;
}
void inorder(TreeNode* root) {
if (root == NULL) return;
inorder(root->left);
printf("%d ", root->val);
inorder(root->right);
}
int main() {
TreeNode* root = NULL;
int arr[] = {5, 3, 7, 2, 4, 6, 8};
for (int i = 0; i < 7; i++)
root = insert(root, arr[i]);
printf("中序遍历: ");
inorder(root);
printf("\n");
root = delete_node(root, 5);
printf("删除5后: ");
inorder(root);
printf("\n");
return 0;
}
运行结果:
中序遍历: 2 3 4 5 6 7 8
删除5后: 2 3 4 6 7 8
嗯,到这里 BST 的核心内容就讲完了。记住三个关键点:左小右大的定义、递归的思维方式、以及删除操作的三种情况。把这些搞明白,后面的平衡树学起来就轻松多了。