相关笔记

概览

本节介绍在二叉搜索树上执行的基本查询操作,包括搜索(TREE-SEARCH / ITERATIVE-TREE-SEARCH)、最小值(TREE-MINIMUM)、最大值(TREE-MAXIMUM)、后继(TREE-SUCCESSOR)和前驱(TREE-PREDECESSOR)。所有操作均可在 时间内完成,其中 为树高。本节重点讲解迭代式搜索的循环不变式证明、TREE-SUCCESSOR 的两种情况分析(有右子树 vs 无右子树需上溯),以及这些操作在实际系统中的应用场景。


知识结构总览

flowchart TD
    A["BST查询操作"] --> B["搜索 TREE-SEARCH"]
    A --> C["最小值 TREE-MINIMUM"]
    A --> D["最大值 TREE-MAXIMUM"]
    A --> E["后继 TREE-SUCCESSOR"]
    A --> F["前驱 TREE-PREDECESSOR"]

    B --> B1["递归版本 TREE-SEARCH"]
    B --> B2["迭代版本 ITERATIVE-TREE-SEARCH"]
    B --> B3["循环不变式证明"]
    B --> B4["时间复杂度 O(h)"]

    C --> C1["沿左链一直向下"]
    D --> D1["沿右链一直向下"]

    E --> E1["情况1:有右子树 → 右子树最小值"]
    E --> E2["情况2:无右子树 → 上溯找最低祖先"]

    F --> F1["与SUCCESSOR对称"]

核心思想

核心思路

BST查询操作的核心思想是利用BST性质在树中导航。搜索操作类似于二分查找:每次与当前节点比较后,决定走向左子树还是右子树。最小值/最大值操作沿左链/右链一直向下走即可。后继/前驱操作稍复杂——需要区分节点有右子树无右子树两种情况,后者需要沿父指针上溯,找到第一个”拐弯”的祖先。所有操作的时间复杂度均为 ,在平衡树中为

TREE-SEARCH —— 递归搜索伪代码

TREE-SEARCH(x, k)
1  if x == NIL or k == x.key
2      return x
3  if k < x.key
4      return TREE-SEARCH(x.left, k)
5  else return TREE-SEARCH(x.right, k)

该过程从根节点开始,递归地在左子树或右子树中搜索关键字 。如果找到匹配的节点则返回该节点,否则返回 NIL。

ITERATIVE-TREE-SEARCH —— 迭代搜索伪代码

ITERATIVE-TREE-SEARCH(x, k)
1  while x ≠ NIL and k ≠ x.key
2      if k < x.key
3          x = x.left
4      else x = x.right
5  return x

迭代版本与递归版本功能完全相同,但在大多数计算机上运行更快,且避免了递归调用的栈开销。

ITERATIVE-TREE-SEARCH 循环不变式与正确性证明

循环不变式

在 while 循环(第1-4行)的每次迭代开始时:

  1. 节点 是以当前搜索起始节点为根的子树的根
  2. 如果关键字 存在于以当前 为根的子树中,那么 最终会返回包含 的节点

【迭代搜索不变式(BST性质保证搜索方向正确)】

初始化: 在第一次迭代之前, 是整棵BST的根。如果 存在于树中,那么它一定在以 为根的子树中。不变式成立。

【不变式维护(左走或右走均保持目标在子树中)】

维护: 分两种情况:

  • :由BST性质, 如果存在,只可能在 的左子树中。第3行将 更新为 ,此时 指向的子树仍然包含 (如果 存在的话)。不变式保持。
  • :由BST性质, 如果存在,只可能在 的右子树中。第4行将 更新为 ,同理不变式保持。

【不变式终止(NIL或命中均正确)】

终止: while 循环在以下两种条件下终止:

  1. :说明搜索路径已到达叶子节点的空孩子, 不存在于树中,返回 NIL 是正确的
  2. :找到了关键字 ,返回 是正确的

两种情况下返回值都正确。

TREE-MINIMUM —— 最小值伪代码

TREE-MINIMUM(x)
1  while x.left ≠ NIL
2      x = x.left
3  return x

由BST性质,最小关键字一定在从根出发沿左链一直向下的最左节点上。

TREE-MAXIMUM —— 最大值伪代码

TREE-MAXIMUM(x)
1  while x.right ≠ NIL
2      x = x.right
3  return x

由BST性质,最大关键字一定在从根出发沿右链一直向下的最右节点上。

TREE-SUCCESSOR —— 后继伪代码

TREE-SUCCESSOR(x)
1  if x.right ≠ NIL
2      return TREE-MINIMUM(x.right)
3  y = x.p
4  while y ≠ NIL and x == y.right
5      x = y
6      y = y.p
7  return y

TREE-SUCCESSOR 正确性论证

TREE-SUCCESSOR 正确性

为BST中的一个节点, 的后继定义为在中序遍历序列中紧排在 之后的那个节点。TREE-SUCCESSOR 分两种情况:

【后继情况1:有右子树时取右子树最小值】

情况1: 有右子树( 此时 的后继是右子树中的最小节点。原因:由中序遍历顺序(左→根→右),遍历完 后下一个访问的是 右子树中最左边的节点,即 。该节点是右子树中关键字最小的节点,也是整棵树中大于 的最小节点。

【后继情况2:无右子树时上溯找最低左拐祖先】

情况2: 没有右子树( 此时需要沿父指针上溯,找到第一个”左拐”的祖先。具体地,设 的最低祖先,且 左子树中(即 是第一个满足 的祖先)。那么 就是 的后继。原因:在中序遍历中, 的左子树(包含 )在 之前遍历,而 所在子树的根中第一个大于 的节点。

如果不存在这样的祖先(即 是整棵树的最大节点),则返回 NIL。

TREE-PREDECESSOR —— 前驱伪代码

TREE-PREDECESSOR(x)
1  if x.left ≠ NIL
2      return TREE-MAXIMUM(x.left)
3  y = x.p
4  while y ≠ NIL and x == y.left
5      x = y
6      y = y.p
7  return y

TREE-PREDECESSOR 与 TREE-SUCCESSOR 完全对称:有左子树时取左子树最大值,无左子树时上溯找第一个”右拐”的祖先。

时间复杂度分析

时间复杂度

所有六个查询操作的时间复杂度均为 ,其中 为树高:

  • TREE-SEARCH / ITERATIVE-TREE-SEARCH:每次迭代下降一层,最多 次迭代
  • TREE-MINIMUM / TREE-MAXIMUM:沿左链/右链走,最多
  • TREE-SUCCESSOR / TREE-PREDECESSOR:情况1调用 TREE-MINIMUM/MAXIMUM 为 ;情况2沿父指针上溯,最多

在最坏情况下(树退化为链表),,所有操作退化为 。在平衡BST中,,所有操作为


补充理解与拓展

前驱与后继的实际应用场景

前驱和后继操作在许多实际系统中有着重要应用:

数据库B+树范围查询: B+树是BST的多路推广,是数据库索引的核心数据结构。MySQL InnoDB引擎的索引完全基于B+树构建,默认页大小为16KB。数据库中的范围查询(如 SELECT * FROM table WHERE key BETWEEN 10 AND 50)本质上就是:先通过搜索定位到下界节点,然后反复调用SUCCESSOR操作,沿叶子节点的链表依次获取后继直到超过上界。1 MySQL还提供了Multi-Range Read(MRR)优化,先扫描索引收集所有匹配的主键,再按主键顺序回表查询,减少随机磁盘IO。

C++ STL迭代器: std::mapstd::set 的底层实现是红黑树(一种平衡BST)。STL提供的 lower_boundupper_bound 操作底层逻辑正是TREE-SUCCESSOR的变体——找到第一个不小于/大于给定键的节点。iterator++(递增迭代器)直接调用SUCCESSOR获取中序后继。根据C++标准,std::map::erase 仅使被删除元素的迭代器失效,其他迭代器保持有效——这与CLRS第4版TRANSPLANT方法的语义一致。2

文本编辑器与区间管理: 文本编辑器中的行号跳转、IDE中的断点管理、日历系统中的事件查询等功能,都可以利用BST的有序性来高效实现前驱/后继查找。

BST与快速排序的深刻对应关系

BST与快速排序之间存在精确的结构对应关系,这一对应关系在CLRS问题12-3中有系统阐述:

  • BST的根节点对应快速排序中选择的枢轴元素(pivot)
  • BST的左子树对应快速排序中对小于枢轴部分的递归排序
  • BST的右子树对应快速排序中对大于枢轴部分的递归排序
  • BST的搜索路径长度对应快速排序中元素的比较次数

这一对应关系意味着:快速排序在某个输入上的性能,与以该输入顺序插入BST后树的形状直接相关。如果快速排序每次都选到好的枢轴,对应BST就是平衡的;反之BST退化为链表。

随机BST的期望高度: Reed(2003)证明了若 个不同关键字以随机顺序插入BST,则期望高度为 ,其中 是方程 的较大根。这意味着随机BST的期望高度约为 ,非常接近最优的 3

期望总路径长度: 随机BST的期望总内部路径长度为 ,与快速排序的期望比较次数完全相同。这一结论可以通过指示器随机变量和期望的线性性严格证明。


易混淆点与辨析

搜索时等于号的处理方向

❌ 错误理解:在 TREE-SEARCH 中,当 时,应该继续向左或向右搜索以找到所有匹配项。

✅ 正确理解:当 时,第1行条件 成立,直接返回当前节点 。BST的搜索操作返回的是第一个找到的匹配节点,而非所有匹配节点。如果需要找到所有等于 的节点,需要额外操作(如找到后继续在中序序列中向两侧扩展)。

上溯找后继时"左拐"的理解

❌ 错误理解:TREE-SUCCESSOR 在情况2中,上溯时找的是第一个 左孩子的祖先。

✅ 正确理解:更精确地说,上溯时找的是最低的(即最近的)满足 的祖先 ,也就是 左子树中的那个祖先。注意 while 循环的条件是 x == y.right,即当 的右孩子时继续上溯——这意味着”我们是从右边来的,还没拐弯”。当 的左孩子时(或 ),循环终止,此时 就是后继。

直觉理解:想象你站在节点 上,要找中序遍历中下一个访问的节点。如果没有右子树,你需要”回头”往上走,直到找到一个你从左侧上来的祖先——那个祖先就是下一个要访问的节点。


习题精选

题号题目描述难度考察重点
12.2-1假设关键字互不相同,写出在图12-2(a)的BST中搜索关键字22的路径TREE-SEARCH执行过程
12.2-2写出递归版本的TREE-MINIMUM和TREE-MAXIMUM递归与迭代转换
12.2-3写出过程TREE-PREDECESSOR的伪代码前驱操作的对称性
12.2-4Professor Bunyan认为:先序遍历中节点 出现在 之前,则 的祖先。这正确吗?⭐⭐BST遍历性质
12.2-5证明:BST中以某个节点 为根的子树中所有节点的关键字,恰好介于 的前驱和后继的关键字之间⭐⭐⭐前驱后继与BST性质
12.2-6证明:ITERATIVE-TREE-SEARCH的最坏情况运行时间为 Θ(h)⭐⭐时间复杂度分析
12.2-7证明:一棵有 个节点的BST中,SUCCESSOR操作的最坏情况时间为 Θ(n)⭐⭐退化树分析

视频学习指南

资源主题链接说明
MIT 6.006 Lecture 5Binary Search Treeshttps://www.youtube.com/watch?v=9Jry5-82IqMErik Demaine讲解BST搜索、最小最大值操作
Abdul BariBinary Search Tree Searchhttps://www.youtube.com/watch?v=H5JubkIy_pI直观的BST搜索动画演示
Abdul BariBST Successor Predecessorhttps://www.youtube.com/watch?v=5cPbNCrtotg专门讲解后继和前驱操作
mycodeschoolBST Searchhttps://www.youtube.com/watch?v=COZK5HN2gMU搜索操作的代码实现与图解
GeeksforGeeksBST Operationshttps://www.youtube.com/watch?v=VCTk53Fk1GQ涵盖所有BST查询操作的完整教程

教材原文

CLRS 第4版 12.2节原文

我们经常需要在二叉搜索树中查找具有给定关键字的节点。除了搜索操作外,二叉搜索树还支持查询最小值、最大值、前驱和后继等操作。本节将介绍这些操作,并分析它们的运行时间。

搜索操作从根节点开始,沿着一条简单的路径向下搜索。对于每个节点 ,算法将关键字 进行比较。如果两个关键字相等,则搜索终止。如果 ,则搜索继续在 的左子树中进行;如果 ,则搜索继续在 的右子树中进行。

为了找到以某个节点为根的子树中的最小关键字,只需沿着左孩子指针一直向下走,直到遇到 NIL。类似地,最大关键字可以通过沿着右孩子指针一直向下走来找到。


参见Wiki

章节导航:

关联知识:

第12章-二叉搜索树 查询二叉搜索树

Footnotes

  1. MySQL Reference Manual. “InnoDB Physical Structure” 与 “Multi-Range Read Optimization”. https://dev.mysql.com/doc/refman/9.0/en/innodb-physical-structure.html

  2. cppreference.com. “std::map::erase.” https://en.cppreference.com/w/cpp/container/map/erase

  3. Reed, B. (2003). “The height of a random binary search tree.” Journal of the ACM, 50(3), 306–332.