Part 2.8:二叉搜索树:有序世界的高效查找
引言:在有序中快速查找
在前面的学习中,我们接触了各种数据结构:数组、链表、哈希表、堆……它们各有所长。今天,我们将学习一种兼具”有序性”和”高效性”的树结构——**二叉搜索树 (Binary Search Tree, BST)**。
想象你在图书馆找一本书。如果书是随机摆放的,你只能一本一本地找(O(n))。但如果书是按书名字母顺序排列的,你就可以用”二分查找”的思想,快速定位到目标区域(O(log n))。
二叉搜索树就是这样一种”有序”的树结构。它通过一个简单而强大的性质——左小右大——实现了在树结构中的高效查找。
为什么 BST 如此重要?
- 面试价值:BST 是面试中的高频考点。从基本的查找、插入、删除,到验证 BST、求第 K 小元素、BST 的序列化……这些题目都是对你树结构理解的深度考察。
- 工程价值:BST 是许多高级数据结构(如 AVL 树、红黑树、B 树)的基础。数据库索引、文件系统、编译器的符号表……都离不开这类有序树结构的身影。
本文将带你深入理解 BST 的原理、实现和应用,让你掌握这个在有序世界中游刃有余的数据结构。
1. BST 的核心原理
直觉:一个”自动排序”的二叉树
BST 是一棵特殊的二叉树,它满足一个核心性质:
BST 性质:
- 左子树上所有节点的值都小于根节点的值。
- 右子树上所有节点的值都大于根节点的值。
- 左右子树也分别为二叉搜索树(递归定义)。
这个性质保证了:BST 的中序遍历结果是一个有序序列。
关键图示:一棵 BST

对于任意节点,其左子树所有节点的值都小于它,右子树所有节点的值都大于它。
2. 核心操作
2.1 查找 (Search)
思路:从根节点开始,如果目标值小于当前节点,就去左子树找;如果大于,就去右子树找;如果相等,就找到了。
这个过程类似于二分查找。
伪代码
function search(root, target):
if root is NULL or root.value == target:
return root
if target < root.value:
return search(root.left, target)
else:
return search(root.right, target)Python 实现 (迭代版本)
def search_bst(root, target):
while root:
if target == root.value:
return root
elif target < root.value:
root = root.left
else:
root = root.right
return None # 未找到时间复杂度:O(h),其中 h 是树的高度。平衡时 h = log n,退化时 h = n。
2.2 插入 (Insert)
思路:先像查找一样找到应该插入的位置(叶子节点的下方),然后创建新节点并连接。
动画式案例:向 BST 插入 25

图片来源: visualgo.net
过程解析:
- 查找路径:从根节点
50开始,根据“左小右大”的原则向下查找,路径为50 -> 30 -> 20。 - 找到插入点:在
20节点,因为25 > 20,所以应该插入到其右子位置。 - 插入新节点:在
20的右侧插入新节点25。
伪代码 (递归)
function insert(root, value):
if root is NULL:
return new TreeNode(value)
if value < root.value:
root.left = insert(root.left, value)
else if value > root.value:
root.right = insert(root.right, value)
// 如果 value == root.value,通常不插入(BST 不允许重复值)
return root时间复杂度:O(h)。
2.3 删除 (Delete)
删除是 BST 中最复杂的操作,因为需要考虑三种情况。
三种情况
- 被删除节点是叶子节点:直接删除。
- 被删除节点只有一个子节点:用其子节点替换它。
- 被删除节点有两个子节点:
- 找到其右子树中的最小节点(也叫后继节点),或左子树中的最大节点(也叫前驱节点)。
- 用后继节点的值替换被删除节点的值。
- 递归删除后继节点(后继节点最多只有一个子节点)。
动画式案例:删除有两个子节点的节点

图片来源: visualgo.net
过程解析 (以删除值为 30 的节点为例):
- 找到后继节点:在被删除节点的右子树中,找到值最小的节点。在这个例子中,是
40。 - 值替换:将后继节点的值
40覆盖掉被删除节点的值30。 - 删除后继节点:现在问题转化为删除原来的后继节点
40。由于后继节点最多只有一个右子节点,删除它就变得简单了。
伪代码
function delete(root, value):
if root is NULL:
return NULL
if value < root.value:
root.left = delete(root.left, value)
else if value > root.value:
root.right = delete(root.right, value)
else: // 找到了要删除的节点
// 情况1: 叶子节点或只有一个子节点
if root.left is NULL:
return root.right
else if root.right is NULL:
return root.left
// 情况2: 有两个子节点
// 找到右子树的最小节点
min_node = find_min(root.right)
// 用后继节点的值替换当前节点的值
root.value = min_node.value
// 递归删除后继节点
root.right = delete(root.right, min_node.value)
return root
function find_min(node):
while node.left is not NULL:
node = node.left
return node时间复杂度:O(h)。
3. BST 的优势与劣势
优势
- 有序性:中序遍历可得有序序列。
- 高效性:在平衡状态下,查找、插入、删除都是
O(log n)。 - 灵活性:支持动态插入和删除,比静态的有序数组更灵活。
劣势:退化问题
BST 最大的问题是:在最坏情况下,它会退化成一条链表。
场景:如果按有序序列插入元素(如 1, 2, 3, 4, 5),BST 会变成:
按顺序插入元素会导致二叉搜索树退化成链表,失去性能优势。
此时,所有操作的时间复杂度都退化为 O(n),失去了树结构的优势。
解决方案:平衡树
为了避免退化,人们发明了自平衡二叉搜索树,如:
- AVL 树:严格平衡,任何节点的左右子树高度差不超过 1。
- 红黑树:近似平衡,通过颜色标记和旋转操作维持平衡。
- B 树 / B+ 树:多路搜索树,常用于数据库和文件系统。
这些平衡树能保证在任何情况下,树的高度都维持在 O(log n),从而保证操作的高效性。
4. 复杂度总结
| 操作 | 平均情况 | 最坏情况 |
|---|---|---|
| 查找 | O(log n) | O(n) |
| 插入 | O(log n) | O(n) |
| 删除 | O(log n) | O(n) |
| 中序遍历 | O(n) | O(n) |
空间复杂度:O(n),需要存储 n 个节点。
5. 应用场景
- 动态有序数据集:需要频繁插入、删除,同时又需要保持有序的场景。
- 范围查询:查找某个区间内的所有元素(如查找所有分数在 60-80 之间的学生)。
- 第 K 小/大元素:通过中序遍历或在节点中维护子树大小信息来实现。
- 数据库索引:虽然实际使用的是 B+ 树,但其核心思想源于 BST。
6. 经典面试题
- 验证二叉搜索树:判断一棵二叉树是否是有效的 BST。
- BST 中第 K 小的元素:利用中序遍历。
- BST 的最近公共祖先:利用 BST 的有序性,比普通二叉树更简单。
- 将有序数组转换为 BST:递归地选择中间元素作为根节点。
7. 总结
二叉搜索树通过”左小右大”这一简单而强大的性质,实现了在树结构中的高效有序查找。
- 核心性质:左子树 < 根 < 右子树。
- 关键特征:中序遍历得到有序序列。
- 三大操作:查找、插入、删除,平均时间复杂度
O(log n)。 - 致命弱点:可能退化成链表,时间复杂度退化为
O(n)。 - 进化方向:自平衡树(AVL、红黑树)。
掌握了 BST,你就理解了所有有序树结构的基础。在后续的学习中,我们将接触到更多基于 BST 思想的高级数据结构和算法。
至此,Part 2:基础数据结构的所有内容已经完成!我们从线性结构(数组、链表、栈、队列、哈希表)到非线性结构(树、堆、并查集、BST),系统地学习了数据结构的核心知识。
在下一个 Part 中,我们将进入排序与查找的世界,学习计算机科学中最经典、最重要的算法。