进阶 #algorithm#bst#tree

二叉搜索树(BST)

二叉搜索树(Binary Search Tree, BST)对每个节点,左子树所有值 < 节点值 < 右子树所有值——查找、插入、删除平均 O(log n)

🗂️ 如何组织大量数据——让查找变得更快?

上一节学了二叉树——数据有了层次结构。但有一个问题:你怎么在树里快速找到某个数?

如果一棵树只是随便组织——根节点是 1,左子是 9,右子是 3——那你要找一个数,最坏情况还是要把所有节点都看一遍。

二叉搜索树(Binary Search Tree, BST) 给树加了一个”规则”:

对于树的每一个节点:左子树的所有值 < 节点值 < 右子树的所有值

这个简单的规则,让查找变得像二分搜索一样快。

🏫 类比:图书馆的图书分类

你去图书馆找一本书。图书馆的书是按字母分区的:

  • A-K 区在左边(左子树)
  • L-Z 区在右边(右子树)

你走到 L-Z 区,发现里面又分了两个分区:

  • L-R 区在左
  • S-Z 区在右

你走进 S-Z 区……每次你只需要决定”往左还是往右”,不用遍历整个图书馆。

这就是 BST 的核心思想——每次比较都能排除一半的可能


📐 BST 的定义和性质

定义

一棵二叉搜索树满足:对于树中的每一个节点:

  1. 如果左子树不为空,左子树中所有节点的值都小于该节点的值
  2. 如果右子树不为空,右子树中所有节点的值都大于该节点的值
  3. 左右子树也都是 BST
      8
     / \
    3   10
   / \    \
  1   6    14
     / \   /
    4   7 13

验证:

  • 节点 3:左子树 {1} 都小于 3,右子树 {4,6,7} 都大于 3 ✅
  • 节点 10:右子树 {13,14} 都大于 10 ✅
  • 节点 6:左子树 {4} 小于 6,右子树 {7} 大于 6 ✅

关键性质

# BST 的中序遍历 = 从小到大排序
def inorder(node):
    if not node:
        return
    inorder(node.left)
    print(node.val)      # 输出:1, 3, 4, 6, 7, 8, 10, 13, 14
    inorder(node.right)

这是 BST 最重要的性质:中序遍历的结果是升序序列。这个性质你在很多树相关的问题中都会用到。


🔍 BST 的查找

查找过程

从根节点开始比较:

查找 7:
    8  → 7 < 8 → 去左子树

    3  → 7 > 3 → 去右子树

    6  → 7 > 6 → 去右子树

    7  → 7 == 7 → 找到了! ✅
def search_bst(root, target):
    if not root or root.val == target:
        return root
    
    if target < root.val:
        return search_bst(root.left, target)
    else:
        return search_bst(root.right, target)

每次比较排除一整棵子树——这就是 BST 查找 O(log n) 的来源。

💡 和二分搜索的对比:BST 的查找和二分搜索的本质是一样的——每次比较排除一半数据。区别在于:

  • 二分搜索在数组上工作,要求数组有序
  • BST 在树上工作,树的构造过程就”自带”有序性

➕ BST 的插入

插入的逻辑和查找几乎一样——先找到应该存放的位置,然后插入:

def insert_bst(root, val):
    if not root:
        return TreeNode(val)  # 找到空位,插入
    
    if val < root.val:
        root.left = insert_bst(root.left, val)
    elif val > root.val:
        root.right = insert_bst(root.right, val)
    # 如果 val == root.val,根据具体需求处理(通常不插入重复值)
    
    return root
插入 5 到 BST 中:
       8                  8
      / \                / \
     3   10   →         3   10
    / \                / \
   1   6              1   6
                     /
                    5    ← 插在这里(因为 5 < 6 且 5 > 3)

➖ BST 的删除(最复杂)

BST 的删除分三种情况:

情况 1:删除叶子节点(没有子节点)

最简单——直接删除。

删除 4:
      8              8
     / \            / \
    3   7    →     3   7
   /              /
  2              2
   \
    4   ← 删除
if not root.left and not root.right:
    return None  # 直接删掉

情况 2:删除有一个子节点的节点

用子节点替换要删除的节点。

删除 7(右子节点为 9):
      8              8
     / \            / \
    3   7    →     3   9
       / \
      6   9
if not root.left:
    return root.right  # 用右子替换
if not root.right:
    return root.left   # 用左子替换

情况 3:删除有两个子节点的节点

这是最复杂的情况。策略:用右子树中的最小节点替换被删除节点

删除 3(有两个子节点):
      8                      8
     / \                    / \
    3   10    →            4   10
   / \                    / \
  1   6                  1   6
     / \                    / \
    4   7                  5   7
         \
          5  ← 右子树中的最小节点
# 找到右子树的最小节点
min_node = find_min(root.right)
root.val = min_node.val       # 用最小值替换当前节点
root.right = delete(root.right, min_node.val)  # 删除那个最小节点

📊 复杂度分析

操作平均情况最坏情况
查找O(log n)O(n)
插入O(log n)O(n)
删除O(log n)O(n)

BST 的致命缺陷——退化

BST 的 O(log n) 是”平均情况”——它依赖树的形状。如果插入的数据是已经有序的

# 按顺序插入 1, 2, 3, 4, 5, 6
root = None
for i in [1, 2, 3, 4, 5, 6]:
    root = insert_bst(root, i)

# 结果树:
# 1
#  \
#   2
#    \
#     3
#      \
#       4
#        \
#         5
#          \
#           6

这棵树退化成了链表!查找变成 O(n),完全失去了 BST 的优势。

💡 为什么会退化? 因为 BST 没有”自动平衡”的机制——插入数据的顺序决定了树的形状。有序数据让 BST 变成单向延伸。这正是 平衡树(AVL/红黑树) 要解决的问题——通过旋转保持树的平衡。


🏢 BST 的实际应用

场景说明
C++ STL map/set用红黑树(平衡 BST)实现,支持有序遍历
Java TreeMap同样用红黑树实现
数据库索引(非主流)大多数数据库用 B+ 树(BST 的变体)
表达式树编译器用 BST 表示算术表达式
区间查找BST 可以高效地查找某个范围内的所有值

Python 中为什么不直接用 BST?

Python 标准库没有直接的 BST 实现(因为大多数场景有更好的替代):

  • 需要键值对查找 → 用 dict(哈希表)
  • 需要有序数据 → 用 list + sort
  • 需要有序键值对 → 用 bisect 模块 + dict

📝 小结

概念一句话
BST 性质左子树 < 节点 < 右子树
中序遍历输出 BST 的升序排列
查找比较→左或右→递归——O(log n)
插入找到空位插入——和查找类似
删除三种情况:无子、一子、两子
退化有序插入导致 BST 退化成链表 O(n)

🎯 思考题:给定一个数组,如何构造一棵”最优”的 BST——即查找效率最高的 BST?(提示:这和二分查找的 mid 选择有关。如果你能选择根节点,应该选哪个?)

为什么先学这个? BST 解决了”查找”和”插入”的动态平衡问题,但它有退化风险。平衡树(AVL/红黑树)通过旋转解决了退化问题。另一个分支是——一种不完全排序但部分有序的树结构。