🌳在计算机科学中,二叉搜索树(Binary Search Tree,简称 BST)是一种非常重要的数据结构。它不仅结构清晰、逻辑优美,而且在实际应用中支持高效的查找、插入和删除操作。本文将从基本定义出发,深入讲解其核心性质、常见操作的原理,并结合代码示例详细说明其实现方式。
🔍 什么是二叉搜索树?
二叉搜索树(也称为排序二叉树)是一种特殊的二叉树,具有如下递归定义:
-
空树是一棵二叉搜索树;
-
非空树由一个根节点、一棵左子树和一棵右子树组成,且满足:
- 左子树中的所有节点的值都严格小于根节点的值;
- 右子树中的所有节点的值都严格大于根节点的值;
- 左子树和右子树本身也必须是二叉搜索树。
⚠️ 注意:通常不允许重复值。若需支持重复,可约定“左 ≤ 根 < 右”或使用计数字段等方式处理。
这种结构性质使得 BST 具备天然的有序性,从而支持高效的查找操作。
⏱️ 时间复杂度分析
由于 BST 的结构依赖于插入顺序,其性能表现分为两种情况:
💡 为避免最坏情况,可使用自平衡二叉搜索树(如 AVL 树、红黑树),但本文聚焦于基础 BST。
🔧 核心操作详解
BST 支持三大基本操作:查找、插入、删除。下面逐一展开。
🔎 查找(Search)
查找目标值 n 的过程如下:
- 从根节点开始;
- 若当前节点为空,说明未找到,返回;
- 若当前节点值等于
n,找到目标; - 若
n小于当前节点值,递归在左子树查找; - 若
n大于当前节点值,递归在右子树查找。
该过程充分利用了 BST 的有序性,每次比较都能排除一半的子树。
✅ 代码实现(来自 1.js)
function search(root, n) {
if (!root) {
return; // 未找到
}
if (root.val === n) {
console.log('目标节点', root);
} else if (root.val > n) {
search(root.left, n);
} else {
search(root.right, n);
}
}
此外,1.js 中还定义了 TreeNode 类,并构建了一个示例树:
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
const root = new TreeNode(6);
root.left = new TreeNode(3);
root.right = new TreeNode(8);
root.left.left = new TreeNode(1);
root.left.right = new TreeNode(4);
root.right.left = new TreeNode(7);
root.right.right = new TreeNode(9);
search(root, 7); // 输出:目标节点 {val: 7, left: null, right: null}
该树结构如下:
6
/ \
3 8
/ \ / \
1 4 7 9
➕ 插入(Insert)
插入新值 n 的规则:
-
若树为空,创建新节点作为根;
-
否则,从根开始比较:
- 若
n < root.val,递归插入左子树; - 若
n > root.val,递归插入右子树;
- 若
-
直到找到一个空位置(
null),在此处创建新节点。
插入操作不会破坏 BST 的性质,因为始终将新节点放在“正确”的叶子位置。
✅ 代码实现(来自 2.js)
function insertIntoBst(root, n) {
if (!root) {
root = new TreeNode(n);
return root;
}
if (root.val > n) {
root.left = insertIntoBst(root.left, n);
} else {
root.right = insertIntoBst(root.right, n);
}
return root;
}
📌 注意:此函数返回更新后的子树根节点,便于递归回溯时重新连接父节点指针。
❌ 删除(Delete)
删除操作是 BST 中最复杂的部分,需分三种情况处理:
情况一:待删节点为叶子节点(无左右子树)
- 直接删除,将其父节点对应指针设为
null。
情况二:待删节点只有一个子树
- 用其唯一子节点“顶替”该节点位置。
情况三:待删节点有两个子树
- 不能简单删除,否则会破坏 BST 结构。
- 解决方案:用中序前驱(左子树最大值)或中序后继(右子树最小值)替代该节点的值,然后递归删除那个替代节点(它必定最多只有一个子树)。
✅ 常见策略:
- 使用左子树的最大值(即最右节点);
- 或使用右子树的最小值(即最左节点)。
✅ 代码实现(来自 3.js)
function deleteNode(root, n) {
if (!root) {
return root;
}
if (root.val === n) {
// 情况一:叶子节点
if (!root.left && !root.right) {
root = null;
}
// 情况三(优先用左子树最大值)或情况二(只有左子树)
else if (root.left) {
const maxLeft = findMax(root.left); // 找左子树最大值
root.val = maxLeft.val; // 替换值
root.left = deleteNode(root.left, maxLeft.val); // 递归删除原最大值节点
}
// 情况二(只有右子树)或情况三(当无左子树时用右子树最小值)
else {
const minRight = findMin(root.right);
root.val = minRight.val;
root.right = deleteNode(root.right, minRight.val);
}
} else if (root.val > n) {
root.left = deleteNode(root.left, n);
} else {
root.right = deleteNode(root.right, n);
}
return root;
}
// 辅助函数:找左子树最大值(最右节点)
function findMax(root) {
while (root.right) {
root = root.right;
}
return root;
}
// 辅助函数:找右子树最小值(最左节点)
function findMin(root) {
while (root.left) {
root = root.left;
}
return root;
}
🧠 补充知识:中序遍历与有序性
BST 的一个重要特性是:中序遍历(左 → 根 → 右)的结果是一个严格递增的序列。
例如,对上述示例树进行中序遍历:
1 → 3 → 4 → 6 → 7 → 8 → 9
这正是 BST 被称为“排序二叉树”的原因。该性质可用于:
- 验证一棵树是否为 BST;
- 获取第 k 小元素;
- 实现范围查询等。
🛠️ 实际应用场景
BST 广泛应用于:
- 数据库索引(早期实现);
- 内存中的有序字典(如 C++ 的
std::set/map基于红黑树); - 动态集合操作(频繁插入/删除/查找);
- 表达式解析树(结合运算符优先级)。
尽管现代系统多采用自平衡树,但理解基础 BST 是掌握高级数据结构的前提。
📌 总结
| 操作 | 原理简述 | 时间复杂度(平均/最坏) |
|---|---|---|
| 查找 | 递归比较,向左或向右 | O(log n) / O(n) |
| 插入 | 找到合适叶子位置插入 | O(log n) / O(n) |
| 删除 | 分三类处理,用前驱/后继替代 | O(log n) / O(n) |
二叉搜索树以其简洁的结构和高效的平均性能,成为算法与数据结构学习中的基石。掌握其原理与实现,不仅能应对面试题,更为理解更复杂的平衡树打下坚实基础。
🌟 记住:BST 的美,在于“有序”;其弱点,在于“不平衡”。而计算机科学的魅力,正在于不断用更聪明的结构去弥补这些弱点!