相关笔记

概览

本节介绍二叉搜索树(BST)的两个核心动态操作——插入与删除。插入操作通过从根节点出发沿搜索路径找到合适位置,将新节点作为叶节点挂入树中,时间复杂度为 ,其中 为树高。删除操作较为复杂,需要分三种情况处理:被删节点无子节点、仅有一个子节点、有两个子节点。第三种情况通过找到后继节点(或前驱节点)替换被删节点来保持BST性质。本节还引入了一个关键的辅助过程 TRANSPLANT,用于将一棵子树替换为另一棵子树,它是删除操作的核心子程序。


知识结构总览

flowchart TD
    A["12.3 插入和删除"] --> B["TREE-INSERT 插入操作"]
    A --> C["TREE-DELETE 删除操作"]

    B --> B1["从根出发沿搜索路径下降"]
    B --> B2["找到NIL位置,将z作为叶节点插入"]
    B --> B3["时间复杂度 O(h)"]
    B --> B4["循环不变式证明正确性"]

    C --> C0["TRANSPLANT 辅助过程"]
    C --> C1["情况1:z无子节点"]
    C --> C2["情况2:z仅有一个子节点"]
    C --> C3["情况3:z有两个子节点"]

    C0 --> C0a["将子树v替换子树u的位置"]
    C0 --> C0b["处理父指针和根指针"]

    C3 --> C3a["3a:后继y不是z的直接右孩子"]
    C3 --> C3b["3b:后继y是z的直接右孩子"]

    C3a --> C3a1["TRANSPLANT(y, y.right)"]
    C3a --> C3a2["y.right = z.right"]
    C3a --> C3a3["TRANSPLANT(z, y)"]
    C3a --> C3a4["y.left = z.left"]

    C3b --> C3b1["TRANSPLANT(z, y)"]
    C3b --> C3b2["y.left = z.left"]

核心思想

核心思路

二叉搜索树的插入和删除是维持动态集合的关键操作。插入的核心思想是利用BST的有序性质,从根节点出发,根据key的大小关系逐层向下搜索,直到找到一个空位置(NIL),然后将新节点挂在该位置上。由于新节点总是作为叶节点插入,因此不会破坏已有节点的BST性质。

删除的核心思想则更为复杂,因为被删节点可能在树的中间位置,直接移除会留下”空洞”。删除的关键在于引入 TRANSPLANT 子程序——它将一棵子树整体替换为另一棵子树,同时正确维护父指针和根指针。基于TRANSPLANT,删除操作分三种情况处理:无子节点时直接移除,单子节点时用子节点替代,双子节点时用后继节点替换被删节点(后继节点是右子树中的最小节点),再删除后继节点的原位置。

算法执行流程

  1. 初始化:y = NIL(跟踪父节点),x = T.root(当前搜索节点)
  2. 沿树下降:比较 z.key 与 x.key,小于则走左子树,大于等于则走右子树,y 始终跟踪 x 的父节点
  3. 找到空位置:当 x == NIL 时,y 即为新节点的父节点
  4. 插入节点:设置 z.p = y,根据 z.key 与 y.key 的比较,将 z 挂为 y 的左孩子或右孩子
flowchart TD
    A["开始 TREE-INSERT"] --> B["y = NIL, x = T.root"]
    B --> C{"x == NIL?"}
    C -- "否" --> D["y = x"]
    D --> E{"z.key < x.key?"}
    E -- "是" --> F["x = x.left"]
    E -- "否" --> G["x = x.right"]
    F --> C
    G --> C
    C -- "是" --> H["z.p = y"]
    H --> I{"y == NIL?"}
    I -- "是" --> J["T.root = z"]
    I -- "否" --> K{"z.key < y.key?"}
    K -- "是" --> L["y.left = z"]
    K -- "否" --> M["y.right = z"]

TREE-INSERT —— 伪代码

TREE-INSERT(T, z)
 1  y = NIL
 2  x = T.root
 3  while x ≠ NIL
 4     y = x
 5     if z.key < x.key
 6        x = x.left
 7     else x = x.right
 8  z.p = y
 9  if y == NIL
10     T.root = z
11     elseif z.key < y.key
12        y.left = z
13     else y.right = z

TREE-INSERT 定义

输入:一棵二叉搜索树 和一个待插入节点 ( 已设置,)。

输出:将 插入到 中的适当位置,使得插入后 仍然满足BST性质。

基本思路:使用指针 从根节点出发沿树下降,指针 始终跟踪 的父节点。当 到达 NIL 时, 就是新节点的父节点,根据key大小将 挂为 的左孩子或右孩子。

循环不变式与正确性证明

循环不变式

在 TREE-INSERT 的 while 循环(第3-7行)中,维护以下不变式:

在每次循环迭代开始时:

  • 是 的父节点(当 时),或者 (初始时)
  • 从根节点到 的路径上所有节点的key值保持BST性质不变
  • 尚未被插入树中

初始化: 在第一次循环迭代开始之前(第1-2行之后),,。此时不变式自然成立: 是 的父节点(根节点没有父节点,对应 ),树本身满足BST性质, 尚未插入。

【插入不变式维护(BST性质保证搜索方向正确)】

维护: 假设在某次循环迭代开始时不变式成立。循环体执行 (第4行),然后根据 与 的比较结果,将 移动到 或 (第5-7行)。由于 满足BST性质, 中所有节点的key都小于 , 中所有节点的key都大于等于 。因此:

  • 若 ,则 应该插入到 的左子树中, 是正确的搜索方向;
  • 若 ,则 应该插入到 的右子树中, 是正确的搜索方向。 更新后 仍然是 的父节点,BST性质未被破坏, 仍未被插入。不变式得以维护。

【插入不变式终止(叶节点挂载保持BST性质)】

终止: 当循环终止时,。根据不变式, 是 (即 NIL)的父节点,也就是新节点 应该挂载的位置的父节点。第8-13行根据 是否为 NIL(即树是否为空)以及 与 的大小关系,将 正确地挂为 的左孩子或右孩子。插入后, 作为叶节点,不破坏任何已有节点的BST性质,且 自身也满足BST性质(因为它没有子节点)。因此 TREE-INSERT 正确地将 插入到BST中。

TRANSPLANT —— 伪代码

TRANSPLANT(T, u, v)
 1  if u.p == NIL
 2     T.root = v
 3  elseif u == u.p.left
 4     u.p.left = v
 5  else u.p.right = v
 6  if v ≠ NIL
 7     v.p = u.p

TRANSPLANT 定义

输入:二叉搜索树 ,节点 (被替换的子树根),节点 (替换子树的根)。

功能:用子树 的根替换子树 的根。具体来说,将 的父节点与 建立链接,使得 成为 原来父节点的对应子节点。

重要说明:TRANSPLANT 不会更新 的子节点指针,也不会处理 原来子节点的去向。它仅负责”嫁接”操作——将 接到 原来所在的位置。因此,调用者需要自行处理 原有子树的后续安排。

算法执行流程

  1. 判断子节点情况:检查 z 的左子节点和右子节点是否存在
  2. 无左子节点:直接用 z.right 替换 z(TRANSPLANT)
  3. 无右子节点:直接用 z.left 替换 z(TRANSPLANT)
  4. 有两个子节点:找到后继 y = TREE-MINIMUM(z.right)
  5. 处理后继位置:若 y 不是 z 的直接右孩子,先用 y.right 替换 y,再将 z.right 赋给 y
  6. 替换被删节点:用 y 替换 z,将 z.left 赋给 y
flowchart TD
    A["开始 TREE-DELETE"] --> B{"z.left == NIL?"}
    B -- "是" --> C["TRANSPLANT: 用 z.right 替换 z"]
    B -- "否" --> D{"z.right == NIL?"}
    D -- "是" --> E["TRANSPLANT: 用 z.left 替换 z"]
    D -- "否" --> F["y = TREE-MINIMUM(z.right)"]
    F --> G{"y.p == z?"}
    G -- "否" --> H["TRANSPLANT: 用 y.right 替换 y"]
    H --> I["y.right = z.right"]
    G -- "是" --> J["TRANSPLANT: 用 y 替换 z"]
    I --> J
    J --> K["y.left = z.left"]

TREE-DELETE —— 伪代码

TREE-DELETE(T, z)
 1  if z.left == NIL
 2     TRANSPLANT(T, z, z.right)
 3  elseif z.right == NIL
 4     TRANSPLANT(T, z, z.left)
 5  else y = TREE-MINIMUM(z.right)
 6     if y.p ≠ z
 7        TRANSPLANT(T, y, y.right)
 8        y.right = z.right
 9        y.right.p = y
10     TRANSPLANT(T, z, y)
11     y.left = z.left
12     y.left.p = y

TREE-DELETE 定义

输入:二叉搜索树 和待删除节点 。

输出:从 中删除节点 ,并保持BST性质。

三种情况:

  • 情况1(第1-2行): 没有左孩子。用 的右孩子(可能为 NIL)替换 。这同时覆盖了 没有任何子节点的情况(此时 ,相当于直接移除 )。
  • 情况2(第3-4行): 没有右孩子。用 的左孩子替换 。
  • 情况3(第5-12行): 有两个非 NIL 子孩子。找到 的后继节点 (即 右子树中的最小节点),用 替换 ,再处理 原来位置的修复。
    • 情况3a(第6-9行): 不是 的直接右孩子(即 )。需要先将 从其原位置移出(用 替换 ),然后将 的右子树赋给 。
    • 情况3b(第10-12行, 时跳过第6-9行直接执行): 是 的直接右孩子。直接用 替换 ,然后将 的左子树赋给 。

TREE-DELETE 正确性说明

TREE-DELETE 正确性

TREE-DELETE 的正确性可以从三种情况分别论证:

【删除情况1-2(无子节点或单子节点:子树直接替换保持BST性质)】

情况1():用 替换 。由于 没有左子树, 中所有节点的key都大于等于 ,而 的父节点中,若 是左孩子则父节点的key大于 ,若 是右孩子则父节点的key小于等于 。用 替换 后,BST性质在 的父节点处仍然成立。

情况2():与情况1对称,用 替换 ,BST性质同样保持。

【删除情况3(双子节点:后继替换+原位置修复)】

情况3( 有两个子节点): 是 的后继节点。关键性质:

  • 是大于 的最小key值(后继定义)
  • 没有左孩子(因为它是右子树中的最小节点)
  • 中所有节点的key都大于

用 替换 后: 大于 左子树中所有key(因为 ), 小于等于 右子树中除 以外的所有key(因为 是右子树最小值)。因此替换后BST性质在 处成立。同时, 原来位置的修复(情况3a中用 替换 )属于情况1,正确性已证。

时间复杂度分析

时间复杂度

  • TREE-INSERT:while 循环从根节点遍历到叶节点,经过的路径长度最多为树高 ,因此时间复杂度为 。
  • TRANSPLANT:仅包含常数次指针操作,时间复杂度为 。
  • TREE-DELETE:主要开销在于 TREE-MINIMUM(情况3),其时间复杂度为 ;其余操作均为常数时间。因此 TREE-DELETE 的总时间复杂度为 。

在一棵含 个节点的二叉搜索树中, 的范围是 (平衡树)到 (退化为链表)。因此,插入和删除在最坏情况下需要 时间。


补充理解与拓展

BST退化与自平衡树的工程选择

二叉搜索树的插入和删除操作的时间复杂度均为 ,其中 为树高。然而,BST的形状高度依赖于插入和删除的顺序。在最坏情况下(例如按升序依次插入),BST会退化为一条链表,此时 ,所有操作退化为 ,与无序链表无异。

为了保证 ,研究者提出了多种自平衡二叉搜索树:

AVL树(Adelson-Velsky & Landis, 1962):最早的平衡二叉搜索树,通过限制左右子树高度差不超过1来维持平衡。AVL树的查找性能更优(树更矮),但插入和删除时可能需要更多旋转操作。1

红黑树(Bayer, 1972;Guibas & Sedgewick, 1978):通过为节点着色并遵循5条性质来近似平衡。红黑树比AVL树略高(最多2倍),但插入和删除仅需 次旋转(最多3次),以变色操作为主,工程实现更高效。2

AVL vs 红黑树的工程选择: 2024年的一项对比研究(Stewart, Software: Practice and Experience, 2024)通过基准测试发现:AVL树在查找操作上通常比红黑树更快(因为树更矮),但在插入和删除上,红黑树(尤其是bottom-up版本)在随机有序数据上表现更好。Java选择红黑树作为TreeMap的实现,C++选择红黑树作为std::map的实现,核心原因就是插入/删除的旋转次数更少,在频繁修改的场景下整体性能更优。3

左倾红黑树(Sedgewick, 1993):简化了红黑树的实现,仅需2种旋转(左旋和右旋),而非标准红黑树的4种。这一简化使得左倾红黑树成为算法教学(如Princeton COS 226)和许多开源库的首选实现。

BST删除的工程实现挑战

在实际工程中,BST删除操作面临若干需要注意的问题:

1. 迭代器失效语义: 在C++ STL中,std::map::erase(iterator) 删除节点后,仅被删除元素的迭代器失效,指向其他节点的迭代器仍然有效。这是因为红黑树的删除操作仅修改局部指针(通过TRANSPLANT),不影响树的其他部分。CLRS第4版采用的TRANSPLANT方法恰好满足这一语义要求——实际移除节点 ,而非仅复制其关键字。4

2. 惰性删除(Lazy Deletion): 某些场景下,为了避免删除操作的复杂性,可以采用”标记删除”策略——不真正移除节点,而是设置一个删除标记(如deleted = true)。查询时跳过已标记的节点。这种策略简化了删除逻辑,但会增加内存开销,且需要定期清理。惰性删除在并发数据结构中尤其常见,因为它避免了修改树结构的同步开销。

3. 后继 vs 前驱的选择: 当被删节点有两个子节点时,既可以用后继(右子树最小值)也可以用前驱(左子树最大值)来替换。CLRS选择了后继方案。两种方案在对称性上完全等价,但在某些实现中,选择前驱可能在缓存局部性上更有优势(例如,如果左子树比右子树更常被访问)。在实际的标准库实现中,C++ std::map 和 Java TreeMap 均采用后继方案。


易混淆点与辨析

删除三种情况的区分

❌ 错误理解:删除有三种情况——无子节点、单子节点、双子节点,它们是完全独立的,需要分别处理。

✅ 正确理解:实际上,CLRS的实现将”无子节点”归入”无左孩子”的情况(情况1),因为无子节点时 ,TRANSPLANT 用 NIL 替换 ,效果等同于直接移除。所以代码层面只有两个分支加上情况3的子分支:

代码分支条件实际涵盖的物理情况
第1-2行无子节点 或 仅有右孩子
第3-4行仅有左孩子
第5-12行两个子节点均非NIL有两个孩子

注意:情况1和情况2不会同时为真(如果 且 ,走情况1即可),所以用 if-elseif 结构是正确的。

TRANSPLANT 的语义——链接替换 vs 内容复制

❌ 错误理解:TRANSPLANT 是将节点 的内容(key、satellite data)复制到节点 中,然后删除 。

✅ 正确理解:TRANSPLANT 是结构性替换——它修改的是指针链接,而非节点内容。具体来说,TRANSPLANT 将 “嫁接”到 原来所在的位置,使得 成为 原来父节点的子节点。节点 和 本身的内容不会被修改。

为什么选择链接替换而非内容复制?

  • 内容复制需要修改key值,而如果树的外部有指向节点的引用/指针,内容复制会导致这些引用指向错误的数据。
  • 链接替换只修改局部指针,不影响树中其他节点与被操作节点之间的引用关系。
  • 在有卫星数据(satellite data)的节点中,内容复制还需要复制所有卫星数据,开销更大。

情况3中 y.p ≠ z 的判断

❌ 错误理解:情况3中 一定是 的右孩子,所以 总是成立,不需要情况3a。

✅ 正确理解: 是 右子树中的最小节点。如果 的右孩子 没有左孩子,那么 ,此时 (情况3b)。但如果 有左子树, 会沿着左链一直下降到最左下方的节点,此时 (情况3a)。两种子情况的处理方式不同:情况3a需要先将 从原位置移出,而情况3b不需要。


习题精选

题号题目描述难度考察重点
12.3-1给定一棵BST,依次插入key为5、3、7、2、4、6、8的节点,画出结果树⭐TREE-INSERT执行过程
12.3-2给定一棵BST,分别删除叶子节点、仅有一个子节点的节点、有两个子节点的节点,画出每步结果⭐⭐TREE-DELETE三种情况
12.3-3证明:在TREE-DELETE的情况3中, 的key值等于TREE-SUCCESSOR()的key值⭐⭐后继节点性质
12.3-4证明:TREE-DELETE运行时间为 ⭐⭐时间复杂度分析
12.3-5假设使用前驱而非后继来替换被删节点,给出修改后的TREE-DELETE伪代码⭐⭐⭐前驱/后驱对称性
12.3-6当节点 有两个子节点时,如果其前驱也在右子树中,说明此时前驱和后继的关系⭐⭐⭐BST结构深入理解
12.3-7证明:在一棵有 个节点的BST中,TREE-INSERT和TREE-DELETE的最坏情况运行时间为 ⭐⭐退化BST分析

视频学习指南

资源主题链接说明
MIT 6.006 Lecture 5Balanced BSTs, Insert & Deletehttps://www.youtube.com/watch?v=Fi5NvCk6aQIErik Demaine讲授BST插入删除基础
Abdul BariBinary Search Tree Insert & Deletehttps://www.youtube.com/watch?v=JnrbL0tvYd0直观动画演示BST插入删除过程
代码随想录二叉搜索树中的插入操作https://programmercarl.com/0070.%E4%BA%8C%E5%85%83%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E6%90%9C%E7%B4%A2.htmlLeetCode 701题解,含代码实现
代码随想录二叉搜索树中的删除操作https://programmercarl.com/0450.%E5%88%A0%E9%99%A4%E4%BA%8C%E5%85%83%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9.htmlLeetCode 450题解,三种情况详解
Michael SambolBST Insert & Deletehttps://www.youtube.com/watch?v=wcIRPqTR3Kc简洁清晰的伪代码逐步演示
WilliamFisetBST Delete visualizationhttps://www.youtube.com/watch?v=Z1pMjUcsR6E删除三种情况的动画可视化

教材原文

CLRS 第4版 12.3节原文(中文翻译)

插入

要将一个新值 插入到二叉搜索树 中,我们使用过程 TREE-INSERT。该过程接收一个节点 作为输入,其中 ,,,。该过程修改 和 的某些属性,将 插入到树中的适当位置。

如同过程 TREE-SEARCH 和 ITERATIVE-TREE-SEARCH 一样,TREE-INSERT 从树根开始,沿着指针向下遍历,直到 变为 NIL。指针 跟踪 的父节点。TREE-INSERT 维护这样的不变式:在循环的每次迭代开始时, 是 的父节点。初始化时, 为 NIL。当 下降经过树时, 跟随它。当搜索在 处终止时, 包含 的父节点,TREE-INSERT 将 放置为 的适当子节点。

删除

从二叉搜索树 中删除节点 的过程需要三个参数:、 以及 的父节点。删除过程需要考虑三种情况:

  • 如果 没有子节点,则简单地将其移除。
  • 如果 仅有一个子节点,则用该子节点替换 。
  • 如果 有两个子节点,则找到 的后继 (它在 的右子树中),用 替换 ,并修复 原来位置的子树结构。

为了实现节点替换,我们使用 TRANSPLANT 过程,它用一棵子树替换另一棵子树:当用子树 的根替换子树 的根时,TRANSPLANT 将 的父节点指向 ,并更新 的父指针。注意,TRANSPLANT 不会更新 的子节点,也不会处理 原有子节点的去向——这些工作留给调用者完成。

在删除有两个子节点的节点时,我们选择用后继节点替换被删节点。后继节点 是右子树中的最小值,因此 没有左孩子。如果 是 的直接右孩子,我们只需用 替换 ,并将 的左子树设为 的左子树。否则,我们需要先将 从其当前位置移出(用 的右孩子替换 ),然后才能用 替换 。


参见Wiki

章节导航:

关联知识:

  • 二叉搜索树性质 —— BST的有序性定义,是插入和删除操作正确性的基础
  • TREE-MINIMUM —— 删除情况3中寻找后继节点的子程序
  • TREE-SUCCESSOR —— 后继节点的定义与查找方法
  • 红黑树 —— 自平衡BST,解决退化问题的经典方案

第12章-二叉搜索树 插入和删除

Footnotes

  1. Adelson-Velsky, G. M., & Landis, E. M. (1962). “An algorithm for the organization of information.” Doklady Akademii Nauk SSSR, 146(2), 263–266. ↩

  2. Guibas, L. J., & Sedgewick, R. (1978). “A dichromatic framework for balanced trees.” Proceedings of the 19th Annual Symposium on Foundations of Computer Science (FOCS), 8–21. ↩

  3. Stewart, J. W. (2024). “Comparative Performance of the AVL Tree and Three Variants of the Red-Black Tree.” Software: Practice and Experience, 54(7), 1–22. arXiv:2406.05162. ↩

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