数据结构与算法之美:树、森林与二叉树全解析

文章目录

  • [阶段一:树与二叉树的核心概念与 408 必考数学性质](#阶段一:树与二叉树的核心概念与 408 必考数学性质)
  • 阶段二:普通树的三大存储结构与森林转换魔法
      • [第一部分:双亲表示法(顺序存储,数组)------ 并查集的物理基石](#第一部分:双亲表示法(顺序存储,数组)—— 并查集的物理基石)
        • [1. 核心设计思想](#1. 核心设计思想)
        • [2. 纯字符画模型解析](#2. 纯字符画模型解析)
        • [3. 408 核心考点与性能权衡](#3. 408 核心考点与性能权衡)
      • [第二部分:孩子表示法(顺序表 + 单链表)------ 图的邻接表前身](#第二部分:孩子表示法(顺序表 + 单链表)—— 图的邻接表前身)
        • [1. 核心设计思想](#1. 核心设计思想)
        • [2. 纯字符画模型解析](#2. 纯字符画模型解析)
        • [3. 408 核心考点与性能权衡](#3. 408 核心考点与性能权衡)
      • [第三部分:孩子兄弟表示法(二叉链表)------ 408 最核心的"转换魔法"](#第三部分:孩子兄弟表示法(二叉链表)—— 408 最核心的“转换魔法”)
        • [1. 核心设计思想](#1. 核心设计思想)
        • [2. 纯字符画推导演示:把任意形态的普通树拍平为二叉树](#2. 纯字符画推导演示:把任意形态的普通树拍平为二叉树)
        • [3. 408 选择题高频避坑指南](#3. 408 选择题高频避坑指南)
      • 本阶段巩固练习与解析
        • [题目 1(408 森林转换真题计算)](#题目 1(408 森林转换真题计算))
        • [题目 2(反向结构判定题)](#题目 2(反向结构判定题))
  • 阶段三:二叉树四大遍历全掌握与反向构造定理
  • 阶段四:线索二叉树与前驱后继判定
      • 第一部分:为什么要有"线索二叉树"?(痛点与核心动机)
        • [痛点 1:大量的指针空间被白白浪费](#痛点 1:大量的指针空间被白白浪费)
        • [痛点 2:找一个结点的"前驱"和"后继"代价极高](#痛点 2:找一个结点的“前驱”和“后继”代价极高)
      • 第二部分:线索化的致命冲突与"标志位"设计
        • [408 统一规范定义:](#408 统一规范定义:)
      • 第三部分:字符画深度拆解------中序线索二叉树
        • [1. 写出中序遍历序列(左根右)](#1. 写出中序遍历序列(左根右))
        • [2. 开始线索化改造(填补原本为 NULL 的空手)](#2. 开始线索化改造(填补原本为 NULL 的空手))
        • [3. 改造后的纯字符画实况图(实线为树枝,虚线为线索)](#3. 改造后的纯字符画实况图(实线为树枝,虚线为线索))
      • 第四部分:线索化算法的核心------双指针游标跟踪机制
      • [第五部分:中序线索二叉树找前驱与后继(408 大题与选择题核心考点)](#第五部分:中序线索二叉树找前驱与后继(408 大题与选择题核心考点))
        • [1. 找结点 `p` 的中序后继(NextNode)](#1. 找结点 p 的中序后继(NextNode))
        • [2. 找结点 `p` 的中序前驱(PriorNode)](#2. 找结点 p 的中序前驱(PriorNode))
      • 本阶段巩固练习与解析
        • [题目 1(线索数量直接秒杀题)](#题目 1(线索数量直接秒杀题))
        • [题目 2(408 选择题真题变形)](#题目 2(408 选择题真题变形))
  • 阶段五:二叉搜索树(BST)与二叉平衡树(AVL)
      • [第一部分:二叉搜索树(BST)------ "左小右大"的有序世界](#第一部分:二叉搜索树(BST)—— “左小右大”的有序世界)
        • [1. 核心定义与铁律](#1. 核心定义与铁律)
        • [2. BST 的核心查找逻辑](#2. BST 的核心查找逻辑)
        • [3. BST 的插入操作](#3. BST 的插入操作)
        • [4. BST 结点的删除操作(408 大题与选择题必考重点)](#4. BST 结点的删除操作(408 大题与选择题必考重点))
      • [第二部分:BST 的致命软肋与退化危机](#第二部分:BST 的致命软肋与退化危机)
      • [第三部分:平衡二叉树(AVL 树)------ 强制约束高度](#第三部分:平衡二叉树(AVL 树)—— 强制约束高度)
        • [1. 核心定义](#1. 核心定义)
        • [2. 平衡因子(Balance Factor, BF)](#2. 平衡因子(Balance Factor, BF))
      • [第四部分:AVL 树的四大旋转调整(408 每年必考手推题)](#第四部分:AVL 树的四大旋转调整(408 每年必考手推题))
        • [1. LL 型失衡:右单旋转(顺时针旋转)](#1. LL 型失衡:右单旋转(顺时针旋转))
        • [2. RR 型失衡:左单旋转(逆时针旋转)](#2. RR 型失衡:左单旋转(逆时针旋转))
        • [3. LR 型失衡:先左旋后右旋(双旋转)](#3. LR 型失衡:先左旋后右旋(双旋转))
        • [4. RL 型失衡:先右旋后左旋(双旋转)](#4. RL 型失衡:先右旋后左旋(双旋转))
      • [第五部分:AVL 树的四大旋转速记矩阵](#第五部分:AVL 树的四大旋转速记矩阵)
      • [第六部分:AVL 树的最少结点数公式(408 极高频数值考点)](#第六部分:AVL 树的最少结点数公式(408 极高频数值考点))
      • 本阶段巩固练习与解析
        • [题目 1(408 真实大题变形:连续插入与平衡构建)](#题目 1(408 真实大题变形:连续插入与平衡构建))
        • [题目 2(最少结点数反求树高)](#题目 2(最少结点数反求树高))
  • [阶段六:并查集(Disjoint-Set Union)------ 极简高效的集合管理者](#阶段六:并查集(Disjoint-Set Union)—— 极简高效的集合管理者)
      • 第一步:生活中的并查集("武林门派认师傅")
      • [第二步:并查集的物理载体------极简的一维数组 `parent\[\]`](#第二步:并查集的物理载体——极简的一维数组 parent[])
        • [1. 数组定义与物理含义](#1. 数组定义与物理含义)
        • [2. 初始状态与合并后的内存数组切片对照](#2. 初始状态与合并后的内存数组切片对照)
      • [第三步:两大核心操作与 408 优化杀招](#第三步:两大核心操作与 408 优化杀招)
        • [1. 查操作(Find)与路径压缩(Path Compression)](#1. 查操作(Find)与路径压缩(Path Compression))
        • [2. 并操作(Union)与小树合并到大树(Union by Size / Rank)](#2. 并操作(Union)与小树合并到大树(Union by Size / Rank))
      • [第四步:408 并查集经典命题模型(解题模板)](#第四步:408 并查集经典命题模型(解题模板))
        • [模型 1:无向图的连通分量计数](#模型 1:无向图的连通分量计数)
        • [模型 2:无向图环路判定(2022 年 408 统考 15 分大题原型)](#模型 2:无向图环路判定(2022 年 408 统考 15 分大题原型))
        • [模型 3:最小生成树 Kruskal 算法的唯一避环工具](#模型 3:最小生成树 Kruskal 算法的唯一避环工具)
      • 本阶段巩固练习与解析
        • [练习 1(408 选择题真题变形:并查集状态逆推)](#练习 1(408 选择题真题变形:并查集状态逆推))
        • [练习 2(复杂度思辨题)](#练习 2(复杂度思辨题))

阶段一:树与二叉树的核心概念与 408 必考数学性质

第一步:什么是树?(把它当成"倒立生长的家族族谱")

生活中的大树是根在下、树枝朝上长;而在计算机科学里,所有的树都是倒立生长的------老祖宗在最顶上,子孙后代往下方繁衍。

看下面这幅家族族谱字符画:

text 复制代码
第 1 层 (顶层)                  [ 老祖宗 A ]  <--- 【根结点 (Root)】
                               /     |    \
                              /      |     \
第 2 层                [ 儿子 B ] [ 儿子 C ] [ 儿子 D ]
                        /     \      |       /   |   \
                       /       \     |      /    |    \
第 3 层            [ 孙 E ] [ 孙 F ] [ 孙 G ] [H]  [I]  [J]
                    /    \             |       |
                   /      \            |       |
第 4 层          [K]      [L]         [M]     [N]  <--- 【叶子结点 (Leaf)】

对着这幅家族图,所有的 408 核心术语都能一眼对号入座:

  • 根结点(Root) :顶上的老祖宗 A。全天下只有它没有老爹(没有前驱)

  • 叶子结点(Leaf / 终端结点) :最底部的 K, L, F, M, N, I, J。他们断子绝孙,没生任何孩子(度为 0)

  • 分支结点(非终端结点) :只要生了娃的人(如 A, B, C, D, E, G, H)。

  • 结点的度(Degree)这个人亲生了几个娃

  • A 生了 3 个娃(B, C, D),所以 A 的度是 3

  • B 生了 2 个娃(E, F),所以 B 的度是 2

  • K 没生娃,所以 K 的度是 0

  • 树的度:全家族里"生育能力最强的那个人生了几个"。

  • 这张图里 AD 都生了 3 个,没人比他们更多,所以这棵树的度是 3

  • 树的高度 / 深度 :家族总共盖了多少层楼。从顶层根结点算第 1 层,向下数到底,这棵树的高(深)度是 4


第二步:为什么计算机特别偏爱"二叉树"?

普通树里,一个爹可以生任意多个娃(生 3 个、5 个甚至 100 个),这在计算机内存里极其难设计:每个结点到底该预留几个指针指向它的孩子?

  • 预留少了不够用,预留多了(比如每个结点开 10 个指针)大部分指针又都是空的,严重浪费内存。

于是,计算机科学家给家族立了一个硬规矩------严格的二胎政策

  1. 每个结点最多只能生两个孩子(结点的度最大只能是 2)。
  2. 孩子必须分长幼(严格分左右):左边的叫"左孩子(左子树)",右边的叫"右孩子(右子树)",次序绝对不能颠倒!

这就是二叉树(Binary Tree)

二叉树的 5 种基本形态一眼看清:

text 复制代码
(1) 空二叉树      (2) 只有根        (3) 只有左孩子     (4) 只有右孩子     (5) 左右双全
     (空)             A                 A                 A                 A
                                       /                   \               / \
                                      B                     C             B   C

第三步:二叉树界的两大型男------"满二叉树"与"完全二叉树"

408 考卷上极其喜欢在这两个概念上挖坑,我们用排队做操的比喻来理解。

1. 满二叉树:绝对对称的完美金字塔

每一层都排得严丝合缝,所有有孩子的爹都生满了 2 个,所有叶子全都整整齐齐地站在最底层:

text 复制代码
第 1 层                 (1)
                      /     \
第 2 层             (2)       (3)
                   /   \     /   \
第 3 层          (4)   (5) (6)   (7)
  • 特点 :如果高是 h h h,总人数必须是死脑筋的 2 h − 1 2^h - 1 2h−1。差一个人都不叫满二叉树!
2. 完全二叉树:按学号从左往右报数,中间绝不空缺!

满二叉树太苛刻了。如果最后一行人没站满,怎么算规范?

标准是:从第 1 层到倒数第 2 层全满;最后一层就算没满,所有人也必须从最左边开始,一个挨着一个向右站,中间绝对不能跳过任何空位!

看对比:

text 复制代码
   【标准完全二叉树】                         【不是完全二叉树(作废!)】
           (1)                                       (1)
         /     \                                   /     \
       (2)     (3)                               (2)     (3)
      /   \    /                                 /   \      \
    (4)   (5)(6)                               (4)   (5)    (6) <--- 错!
    (最后一层 4,5,6 靠左紧挨着)               (左边的 3 号下面空了,右边却冒出了 6,断层了!)

第四步:408 核心公式------为什么 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1?

这是 408 每年必考的王牌计算。我们用"断子绝孙的叶子数""生了双胞胎的爹数"的字符逻辑来证明它。

我们把树里的所有人分成 3 类:

  • n 0 n_0 n0:生了 0 个孩子的叶子(丁克/没后代)。
  • n 1 n_1 n1:生了 1 个孩子的单亲爹。
  • n 2 n_2 n2:生了 2 个孩子的双亲爹。
动手算两本账:

第一本账:按"人头"数总人数(记为 n n n)

n = n 0 + n 1 + n 2 ------ 全家族就是这三类人组成的 n = n_0 + n_1 + n_2 \quad \text{------ 全家族就是这三类人组成的} n=n0+n1+n2------ 全家族就是这三类人组成的

第二本账:按"抓手/牵线(树枝边)"来算总人数

每个孩子头上都有一根绳子被爹牵着,老祖宗根结点头上没有绳子:

  • n 2 n_2 n2(双亲爹)射出 2 根绳子,贡献了 2 × n 2 2 \times n_2 2×n2 个孩子。
  • n 1 n_1 n1(单亲爹)射出 1 根绳子,贡献了 1 × n 1 1 \times n_1 1×n1 个孩子。
  • n 0 n_0 n0 没射出任何绳子。
  • 把被牵着的孩子加上顶上没人管的老祖宗(+1),总人数就是:

n = 2 n 2 + 1 n 1 + 1 n = 2n_2 + 1n_1 + 1 n=2n2+1n1+1

两本账对账:

n 0 + n 1 + n 2 = 2 n 2 + n 1 + 1 n_0 + n_1 + n_2 = 2n_2 + n_1 + 1 n0+n1+n2=2n2+n1+1

两边同时减掉一个 n 1 n_1 n1,再减掉一个 n 2 n_2 n2,瞬间得出绝妙结论:

n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1

大白话秒杀规律

叶子的数量,永远比生了两个孩子的爹多整整 1 个!不管树里有多少个生单胎的 n 1 n_1 n1,都完全影响不了这个平衡!


第五步:完全二叉树在内存里的"神奇数组阵法"

为什么完全二叉树如此重要?因为它根本不需要指针,直接用普通数组就能存,而且找爹找儿子快如闪电!

把这棵完全二叉树按从上到下、从左到右贴上学号:

text 复制代码
           [1:A]
          /     \
      [2:B]     [3:C]
      /   \     /
   [4:D] [5:E] [6:F]

我们把它直接塞进一个普通连续数组里(下标从 1 开始使用):

text 复制代码
数组下标:   0     1     2     3     4     5     6
存储内容: [空] [ A ] [ B ] [ C ] [ D ] [ E ] [ F ]

不需要任何链表指针,只要利用下标的乘除整除法,关系的推导一清二楚:

text 复制代码
已知当前你在下标 i:

1. 找你的亲生老爹:
   直接除以 2 下取整!爹的下标 = i / 2
   (比如 5 号 E 的爹:5 / 2 = 2,是 B;4 号 D 的爹:4 / 2 = 2,也是 B!)

2. 找你的大儿子(左孩子):
   直接乘 2!左孩子下标 = 2 * i
   (比如 2 号 B 的左儿子:2 * 2 = 4,是 D!)

3. 找你的小儿子(右孩子):
   直接乘 2 加 1!右孩子下标 = 2 * i + 1
   (比如 2 号 B 的右儿子:2 * 2 + 1 = 5,是 E!)

408 核心考点结论

如果总人数是 n n n:

  • 最后一个有孩子的长辈编号必定是 ⌊ n / 2 ⌋ \lfloor n / 2 \rfloor ⌊n/2⌋
  • 大于 ⌊ n / 2 ⌋ \lfloor n / 2 \rfloor ⌊n/2⌋ 的所有人( ⌊ n / 2 ⌋ + 1 ∼ n \lfloor n / 2 \rfloor + 1 \sim n ⌊n/2⌋+1∼n),全都是叶子结点,没有后代

第六步:二叉链表与著名的"空指针大坑"

普通二叉树如果不是完全二叉树,用数组存会有大量空间浪费。在真实工程和 408 大题中,最标准的做法是二叉链表(两个指针)

1. 结点结构长相

每个结点由三个抽屉拼成:

text 复制代码
+--------------+--------------+--------------+
| lchild (左手) |  data (数据) | rchild (右手) |
+--------------+--------------+--------------+

用 C 语言写出来就是:

c 复制代码
typedef struct BiTNode {
    int data;                   // 数据域
    struct BiTNode *lchild;     // 左孩子指针(左手)
    struct BiTNode *rchild;     // 右孩子指针(右手)
} BiTNode, *BiTree;
2. 空指针计数(线索二叉树的核心伏笔)

看一棵由 3 个结点组成的简单二叉树:

text 复制代码
                 +---+---+---+
                 | * | A | * |
                 +---+---+---+
                   /       \
                  /         \
        +---+---+---+     +---+---+---+
NULL <--| ^ | B | ^ |     | ^ | C | ^ |--> NULL
        +---+---+---+     +---+---+---+
              |             |
            NULL          NULL

数一数手上的指针:

  • 3 个结点,每个结点有左右两只手,全树一共有 3 × 2 = 6 3 \times 2 = 6 3×2=6 个指针域。
  • 其中牵着孩子的手只有 2 根(A 牵 B,A 牵 C)。
  • 空着没事干的手(指向 NULL 的指针)有: 6 − 2 = 4 6 - 2 = 4 6−2=4 个!

408 必考定理推导

如果一棵二叉树一共有 n n n 个结点

  • 所有人总共有 2 n 2n 2n 个指针域
  • 除了老祖宗根结点之外,每个孩子头顶上都被且仅被一根指针牵着,所以总共用掉了 n − 1 n - 1 n−1 根有效指针。
  • 剩下空闲的 NULL 指针数量必定是:

空指针数 = 2 n − ( n − 1 ) = n + 1 \text{空指针数} = 2n - (n - 1) = \mathbf{n + 1} 空指针数=2n−(n−1)=n+1

口诀n n n 个结点的二叉树,必有 n + 1 n + 1 n+1 个空指针!

(后面我们会讲到的"线索二叉树",就是专门看中了这 n + 1 n+1 n+1 个被浪费的空指针,拿它们来做前驱后继加速遍历的!)


试刀验证

用这套全新的直观逻辑,快速口算并检验一下这两道 408 原型题:

  1. 练习 1:一棵二叉树里,生了两个孩子的爹(度为 2 的结点)一共有 15 个,生了一个孩子的单亲爹一共有 8 个,请问:
  • 这棵树一共有多少个叶子结点?
  • 这棵树总共有多少个结点?
  1. 练习 2 :学校有 100 个人排成了一棵标准的完全二叉树(编号从 1 到 100):
  • 最后一个拥有亲生孩子的长辈编号是多少?
  • 编号为 35 的人的亲生父亲编号是多少?他的左孩子编号是多少?他的右孩子编号是多少?

练习 1 答案与解析

题目:一棵二叉树里,度为 2 的结点有 15 个,度为 1 的结点有 8 个。

  • 叶子结点数16 个
  • 总结点数39 个

详细解析

  • 求叶子结点数 n 0 n_0 n0
    直接使用 408 核心公式 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1。
    题目给出度为 2 的结点数 n 2 = 15 n_2 = 15 n2=15。
    代入公式计算:

n 0 = 15 + 1 = 16 n_0 = 15 + 1 = 16 n0=15+1=16

  • 求总结点数 n n n
    整棵树由度为 0、度为 1、度为 2 的三类结点构成,即:

n = n 0 + n 1 + n 2 n = n_0 + n_1 + n_2 n=n0+n1+n2

将 n 0 = 16 n_0 = 16 n0=16、 n 1 = 8 n_1 = 8 n1=8、 n 2 = 15 n_2 = 15 n2=15 代入求和:

n = 16 + 8 + 15 = 39 n = 16 + 8 + 15 = 39 n=16+8+15=39


练习 2 答案与解析

题目:100 个结点构成的完全二叉树(编号 1 到 100)。

  • 最后一个有孩子的长辈编号50
  • 35 号结点的亲生父亲编号17
  • 35 号结点的左孩子编号70
  • 35 号结点的右孩子编号71

详细解析

  • 求最后一个分支结点编号
    根据完全二叉树编号性质,总结点数为 n = 100 n = 100 n=100 时,最后一个非叶子结点编号为:

⌊ n / 2 ⌋ = ⌊ 100 / 2 ⌋ = 50 \lfloor n / 2 \rfloor = \lfloor 100 / 2 \rfloor = 50 ⌊n/2⌋=⌊100/2⌋=50

(编号为 51 到 100 的结点全是叶子结点,没有孩子)。

  • 求 35 号的亲生父亲
    除根结点外,任意结点 i i i 的双亲编号为 ⌊ i / 2 ⌋ \lfloor i / 2 \rfloor ⌊i/2⌋:

⌊ 35 / 2 ⌋ = 17 \lfloor 35 / 2 \rfloor = 17 ⌊35/2⌋=17

  • 求 35 号的左孩子
    结点 i i i 的左孩子编号为 2 i 2i 2i。
    2 × 35 = 70 2 \times 35 = 70 2×35=70。
    由于 70 ≤ 100 70 \le 100 70≤100(未超出总结点数),因此 35 号存在左孩子,编号为 70
  • 求 35 号的右孩子
    结点 i i i 的右孩子编号为 2 i + 1 2i + 1 2i+1。
    2 × 35 + 1 = 71 2 \times 35 + 1 = 71 2×35+1=71。
    由于 71 ≤ 100 71 \le 100 71≤100(未超出总结点数),因此 35 号存在右孩子,编号为 71

阶段二:普通树的三大存储结构与森林转换魔法

在阶段一中,我们知道了树是"一对多"的家族关系。但在现实计算机中,普通树面临一个非常棘手的问题:

一个老爹可以生任意多个孩子(生 1 个、3 个甚至 100 个),计算机在内存中到底该如何存储?

如果每个结点都按最大可能性开辟固定大小的指针数组,会产生大量的空指针浪费;开少了又装不下。为了解决"孩子数量不固定"的矛盾,计算机科学家提出了三种经典存储结构。


第一部分:双亲表示法(顺序存储,数组)------ 并查集的物理基石

1. 核心设计思想

开辟一段连续的数组空间。每个结点只需记录两样东西:自己的数据(data自己亲生父亲在数组中的下标(parent

  • 规定 :根结点是全家族的老祖宗,没有父亲,因此其 parent 统一固定标记为 -1
2. 纯字符画模型解析

以下面这棵普通树为例:

text 复制代码
                [ R ] (根结点)
              /   |   \
            /     |     \
         [ A ]  [ B ]  [ C ]
         /   \           |
       [ D ] [ E ]     [ F ]
                      /  |  \
                    [G] [H] [K]

用"双亲表示法"在连续数组中的存放形式如下:

text 复制代码
数组下标    data (数据)    parent (双亲下标)
  [ 0 ]         R              -1          <--- 根结点没有老爹,设为 -1
  [ 1 ]         A               0          (A 的老爹是 R,位于下标 0)
  [ 2 ]         B               0          (B 的老爹是 R,位于下标 0)
  [ 3 ]         C               0          (C 的老爹是 R,位于下标 0)
  [ 4 ]         D               1          (D 的老爹是 A,位于下标 1)
  [ 5 ]         E               1          (E 的老爹是 A,位于下标 1)
  [ 6 ]         F               3          (F 的老爹是 C,位于下标 3)
  [ 7 ]         G               6          (G 的老爹是 F,位于下标 6)
  [ 8 ]         H               6          (H 的老爹是 F,位于下标 6)
  [ 9 ]         K               6          (K 的老爹是 F,位于下标 6)
3. 408 核心考点与性能权衡
  • 找爹极快( O ( 1 ) O(1) O(1)) :给定任意结点的下标 i i i,一步就能定位其父亲:p = tree[i].parent

  • 找孩子极慢( O ( n ) O(n) O(n)) :若想查询"A 有哪些孩子",计算机必须把整个数组从头到尾扫一遍,找出所有 parent == 1 的结点。

  • 考研强关联双亲表示法是 408 必考算法大题"并查集(Disjoint Set)"的唯一下层物理实现


第二部分:孩子表示法(顺序表 + 单链表)------ 图的邻接表前身

1. 核心设计思想

为了彻底解决"双亲表示法找孩子太慢"的缺陷,孩子表示法采用了组合结构:

  • 依然用数组顺序存放各个结点。

  • 但每个结点后面,都单独挂一条单链表(孩子链表),专门用来记录该结点的所有亲生孩子在数组中的下标。

2. 纯字符画模型解析
text 复制代码
顺序数组表头                  每个结点单独挂载的孩子链表
[0] R  | head  ---> [ 1 ] -> [ 2 ] -> [ 3 ] ^   (R 的孩子是 A, B, C)
[1] A  | head  ---> [ 4 ] -> [ 5 ] ^             (A 的孩子是 D, E)
[2] B  | head  ---> ^ (NULL,叶子结点链表为空)
[3] C  | head  ---> [ 6 ] ^                      (C 的孩子是 F)
[4] D  | head  ---> ^ (叶子)
[5] E  | head  ---> ^ (叶子)
[6] F  | head  ---> [ 7 ] -> [ 8 ] -> [ 9 ] ^   (F 的孩子是 G, H, K)
[7] G  | head  ---> ^ (叶子)
[8] H  | head  ---> ^ (叶子)
[9] K  | head  ---> ^ (叶子)
3. 408 核心考点与性能权衡
  • 找孩子极快:只要顺着当前结点的链表头指针向下遍历,就能快速获取所有亲生孩子的下标。

  • 找爹极慢( O ( n ) O(n) O(n)):若要查找某结点的双亲,必须逐一遍历全树所有结点的孩子链表,看谁的链表里包含了该结点的编号。

  • 空指针计数(选择题必考)

    若整棵树共有 n n n 个结点,会有多少个空链表指针?

    答案:叶子结点数 n 0 n_0 n0 个! 只有叶子结点生不出孩子,它的链表头指针必定为 NULL

    (注:如果需要找爹也快,教材中常扩展出"带双亲的孩子表示法",即在数组表头中额外增加一个 parent 域)。


第三部分:孩子兄弟表示法(二叉链表)------ 408 最核心的"转换魔法"

这是 408 考研初试中出题频率最高、理解最精妙的普通树存储结构。

1. 核心设计思想

它彻底摆脱了复杂的链表嵌套,让每个结点只固定保留左右两只手(标准的二叉链表结构):

text 复制代码
+-----------------------+--------------+------------------------+
| firstchild (左孩子指针) |  data (数据) | nextsibling (右兄弟指针) |
+-----------------------+--------------+------------------------+

牢记八字真言口诀:【左孩子,右兄弟】

  • 左指针(firstchild :指向自己的第一个亲生孩子(长子)

  • 右指针(nextsibling :指向紧挨着自己的下一个亲兄弟

2. 纯字符画推导演示:把任意形态的普通树拍平为二叉树

在原本的普通树中:

  • R 的长子是 AA 的亲兄弟是 BB 的亲兄弟是 C

  • A 的长子是 DD 的亲兄弟是 E

  • C 的长子是 FF 的长子是 GG 的亲兄弟是 HH 的亲兄弟是 K

遵循"左孩子,右兄弟"规则转换后,原本分支杂乱的普通树,直接转化成了一棵标准的二叉树

text 复制代码
                  [ R ]
                 /     \ (R 是老祖宗,没有亲兄弟,所以右孩子必定为 NULL!)
                /       \
             [ A ]      NULL
            /     \
           /       \ (A 的亲兄弟是 B)
        [ D ]     [ B ]
          \         \
           \ (D的兄弟E)\ (B 的亲兄弟是 C)
          [ E ]     [ C ]
                    /
                   / (C 的长子是 F)
                [ F ]
                /
               / (F 的长子是 G)
            [ G ]
              \
               \ (G 的亲兄弟是 H)
              [ H ]
                \
                 \ (H 的亲兄弟是 K)
                [ K ]
3. 408 选择题高频避坑指南
  1. 树的叶子结点在转换后的二叉树中呈现什么特征?
  • 结论其左指针必定为空(lchild == NULL!因为叶子结点没有长子。但请注意:它的右指针可以非空,因为叶子结点可以有亲兄弟。
  1. 一棵树转换为二叉树后,根结点的右指针为什么必定为 NULL
  • 结论 :因为全树只有一个老祖宗根结点,它在全树中绝无任何亲兄弟
  1. 森林转换成二叉树时,根结点的右指针何时非空?
  • 结论 :森林由多棵树组成,第二棵树、第三棵树的根,都是第一棵树根的"兄弟"。因此,从第一棵树的根结点的右指针出发,串联起来的正是后面整片森林

本阶段巩固练习与解析

题目 1(408 森林转换真题计算)

设一棵森林 F F F 中共有 3 棵树,第一棵树有 5 个结点,第二棵树有 3 个结点,第三棵树有 4 个结点。若将该森林转换为对应的二叉树 B B B,请问:

  • 二叉树 B B B 的根结点的右子树上,共有多少个结点?

答案与解析

  • 答案7 个

  • 详细解析

  • 根据森林转换二叉树规则"左孩子,右兄弟":

  • 第一棵树的根结点作为整棵二叉树 B B B 的根结点。

  • 第一棵树根结点下面的子树,全部挂在二叉树 B B B 的左子树上。

  • 森林中其余所有树(第二棵树、第三棵树)的根结点,均被视为第一棵树根结点的"兄弟",因此第二棵树与第三棵树的全部结点都会挂在二叉树 B B B 的右子树上

  • 第二棵树有 3 个结点,第三棵树有 4 个结点,相加即为右子树总结点数: 3 + 4 = 7 3 + 4 = 7 3+4=7 个。


题目 2(反向结构判定题)

已知某棵普通树采用"孩子兄弟表示法"存储,某个结点 X X X 的指针状态为:X->firstchild == NULLX->nextsibling != NULL。请问该结点 X X X 在原本的普通树中是什么身份?

答案与解析

  • 答案它在原本的普通树中是一个叶子结点,并且它有亲兄弟。

  • 详细解析

  • firstchild 代表自己的长子。由于 firstchild == NULL,说明该结点生不出任何后代,因此在原普通树中必定是叶子结点

  • nextsibling 代表自己的下一个兄弟。由于 nextsibling != NULL,说明在原本的普通树中,它右侧排着同一辈的亲兄弟。


阶段三:二叉树四大遍历全掌握与反向构造定理

这是 408 考研初试中分值占比极高的一块(选择题必考遍历序列反求与计算,算法大题的核心框架更是 100% 依赖遍历展开)。


第一部分:为什么二叉树要分先、中、后、层序遍历?

在线性表、栈、队列中,元素排成一条直队,遍历只有"从前往后"一条路。

而二叉树是非线性结构,每一个结点都有可能延伸出左、右两条分叉。

按照计算机搜索路径的不同,遍历分为两大门派:

  1. 广度优先搜索(BFS) :层序遍历(一层一层推土机式横扫,依靠队列实现)。

  2. 深度优先搜索(DFS) :先序、中序、后序遍历(一条道走到黑,依靠栈/递归实现)。


第二部分:先序、中序、后序的本质(以"根(当前结点)"的访问时机命名)

不管是先序、中序还是后序,必须牢记一个永恒铁律:左子树永远排在右子树前面!

所谓"先、中、后",仅仅是指老爹(根结点)什么时候出来被访问(打印/处理)

  • 先序遍历(PreOrder) → \to → 左 → \to → 右(老爹排第一)

  • 中序遍历(InOrder) :左 → \to → → \to → 右(老爹排中间)

  • 后序遍历(PostOrder) :左 → \to → 右 → \to → (老爹排最后)

字符画演示:一个标准三节点二叉树
text 复制代码
       ( 根 A )
       /      \
  ( 左 B )   ( 右 C )
  • 先序(根左右) :先访问根 A,再访问左 B,最后访问右 C → \rightarrow → A, B, C
  • 中序(左根右) :先访问左 B,再访问根 A,最后访问右 C → \rightarrow → B, A, C
  • 后序(左右根) :先访问左 B,再访问右 C,最后访问根 A → \rightarrow → B, C, A

第三部分:复杂二叉树三种深度遍历的"描边法"(考场 3 秒出结果神技)

以此棵二叉树为例:

text 复制代码
             A
           /   \
          B     E
           \     \
            C     F
           /     /
          D     G
               / \
              H   K
什么是"描边走线法"?

想象从根结点 A 的头顶开始,用一支画笔顺时针紧贴着整棵树的轮廓边缘画一圈线。线走在结点的不同方位时,就对应不同的遍历顺序:

text 复制代码
       (先序看左边 • )          (中序看底边)           (后序看右边 • )
             A                       A                      A
           /   \                   /   \                  /   \
         •       •               •       •              •       •
  • 先序遍历 :沿轮廓线逆时针绕树一圈,每一次画笔从左侧路过该结点时打卡

    从 A 出发 → \to → 遇 A(左) → \to → 遇 B(左) → \to → 遇 C(左) → \to → 遇 D(左) → \to → 遇 E(左) → \to → 遇 F(左) → \to → 遇 G(左) → \to → 遇 H(左) → \to → 遇 K(左)。

    序列:A B C D E F G H K

    即先打次卡再遍历左和右

  • 中序遍历每一次画笔从正下方滑过该结点时打卡

    在最底端掉头时打卡:最先触底的是 B 的左下方(空) → \to → 接着绕到底部的是 D → \to → 然后往上返回滑过 C 的下方 → \to → 经过 B 的下方 → \to → 穿过整棵树回到了 A 的下方 → \to → 走到右边逐个触底滑过 H → \to → G → \to → K → \to → F → \to → E。

    序列:B D C A E H G K F

    即先遍历左,左边没东西了打次卡再遍历右

  • 后序遍历每一次画笔绕完所有子树,从右侧向上返回时打卡

    离开该结点、再也不回来了才打卡:D(右) → \to → C(右) → \to → B(右) → \to → H(右) → \to → K(右) → \to → G(右) → \to → F(右) → \to → E(右) → \to → A(右)。

    序列:D C B H K G F E A

    即先遍历左和右,左右都没东西了再打卡


第四部分:层序遍历(推土机战术)与队列实现模型

层序遍历就是按照树的层级,从第 1 层到最后一层,每一层从左到右依次巡检

对于上面的二叉树:

  • 第 1 层:A
  • 第 2 层:B, E
  • 第 3 层:C, F
  • 第 4 层:D, G
  • 第 5 层:H, K
    层序遍历结果:A B E C F D G H K
底层工作引擎:必须借助【队列】驱动
  1. 根入队:老祖宗 A 进队。

  2. 循环执行出队与带娃操作

  • 弹出队头结点,访问它(打印)。

  • 如果它有左孩子,左孩子入队。

  • 如果它有右孩子,右孩子入队。

  1. 队列为空时,全部层级遍历结束。
text 复制代码
出队并打印: A      队列剩余元素: [ B, E ]
出队并打印: B      队列剩余元素: [ E, C ] (B带入了它的右孩子C)
出队并打印: E      队列剩余元素: [ C, F ] (E带入了它的右孩子F)
出队并打印: C      队列剩余元素: [ F, D ] (C带入了它的左孩子D)
... 以此类推,天然满足"先来后到"的一层层输出!

第五部分:408 必考大题/选择题------二叉树的"反向重构定理"

在 408 考卷中,经常给你两个遍历序列,让你求出原二叉树的形态或第三种遍历序列。

定理核心:【中序遍历序列】是重构二叉树必须具备的"定位锚点"!

给定已知序列组合 能否唯一确定一棵二叉树? 为什么?
先序 + 中序 能唯一确定 先序找老爹(根),中序切左右子树。
后序 + 中序 能唯一确定 后序找老爹(根在最后),中序切左右子树。
层序 + 中序 能唯一确定 层序找老爹(排在最前面),中序切左右子树。
先序 + 后序 不能唯一确定! 只能知道祖孙关系,无法确定是左子树还是右子树!
实战切刀法:先序 + 中序 还原二叉树

假设题目给出:

  • 先序序列a b d g c e f h

  • 中序序列d g b a e c h f

解题三步走口诀:

  1. 看先序抓根 :先序第一个元素是 a,所以整棵大树的**根必定是 a**

  2. 看中序切西瓜 :在 d g b a e c h f 中找到 a

  • a 左边的部分 [d g b],必定全部属于 a 的左子树

  • a 右边的部分 [e c h f],必定全部属于 a 的右子树

  1. 递归切子树
  • 左子树成员 [d g b] 在先序中先出现的是 b,所以 b 是左子树的根;在 d g bb 的左边是 [d g],右边没有东西,说明 b 只有左孩子。
  • 右子树成员 [e c h f] 在先序中先出现的是 c,所以 c 是右子树的根;在 e c h f 中,ec 左边(左孩子),[h f]c 右边。

通过这样逐层剥洋葱,整棵二叉树的骨架就完全立起来了:

text 复制代码
            a
          /   \
         b     c
        /     / \
       d     e   f
        \       /
         g     h

此时再去写它的后序遍历(左右根)

左子树处理完是 g d b,右子树处理完是 e h f c,最后是根 a

后序序列:g d b e h f c a


本阶段巩固练习与解析

题目 1(408 选择题高频真题变式)

已知一棵二叉树的中序遍历 序列为 B D C E A F H G后序遍历 序列为 D E C B H G F A,请画出二叉树并写出其先序遍历序列。

答案与解析

  • 先序序列A B C D E F G H
  • 推导过程
  1. 后序序列的最后一个字符是 A,说明 A 是整棵树的根结点

  2. 观察中序序列,在 A 处一刀两断:

  • A 左边是 [B D C E](整棵左子树)。

  • A 右边是 [F H G](整棵右子树)。

  1. 分析左子树 [B D C E]
  • 它们在后序序列中对应的片段是 D E C B,后序末尾是 B,故 B 是左子树的根
  • 在中序中,B 左边为空,右边是 [D C E],说明 B 没有左孩子,只有右子树。
  • 子树 [D C E] 在后序中末尾是 C,故 C 是根;在中序中 DC 左边,EC 右边,所以 C 左孩子为 D,右孩子为 E
  1. 分析右子树 [F H G]
  • 对应后序片段是 H G F,后序末尾是 F,故 F 是右子树的根

  • 在中序中,F 的左边为空,右边是 [H G],说明 F 无左孩子,只有右子树。

  • 对应后序末尾是 GG 的中序左边是 H,右边为空,所以 G 的左孩子为 H

  • 还原后的二叉树骨架

text 复制代码
        A
       / \
      B   F
       \   \
        C   G
       / \  /
      D  E H

由树形态可直接写出先序遍历(根左右):A B C D E F G H


题目 2(概念思辨题)

若已知一棵二叉树的"先序遍历"和"后序遍历",在什么情况下可以唯一确定这棵二叉树的形态?

答案与解析

  • 答案当且仅当该二叉树中不存在度为 1 的结点(即每个结点的度要么是 0,要么是 2,也就是满二叉树形态或严格正则二叉树)时,先序和后序才能唯一确定这棵树。
  • 解析
    若存在度为 1 的单分支结点(比如 A 只有一个孩子 B),那么无论 B 是 A 的左孩子还是右孩子:
  • 先序遍历都是:A, B
  • 后序遍历都是:B, A
    计算机仅凭 A, BB, A 根本无法断定 B 到底是在左边还是在右边,因而产生二义性。只有全树都是双分支或叶子时,左、右子树的边界才能被严格锁死。

阶段四:线索二叉树与前驱后继判定

线索二叉树在 408 考研初试中,是选择题的高频命题点(经常给出一棵二叉树,让你直接数中序/先序线索化后一共有多少条线索、某结点的左右指针分别指向谁),同时也是大题考察指针操作与遍历逻辑的经典题型。


第一部分:为什么要有"线索二叉树"?(痛点与核心动机)

在普通的二叉链表存储中,有两个严重的痛点:

痛点 1:大量的指针空间被白白浪费

在阶段一我们严格推导过:一棵拥有 n n n 个结点的二叉树,总共有 2 n 2n 2n 个指针域,其中有且仅有 n − 1 n-1 n−1 个指针牵着孩子,剩下整整 n + 1 n+1 n+1 个指针全是 NULL

如果一棵树有 1000 个结点,就有 1001 个指针是空着的。这在注重内存效率的底层系统里是极大的浪费。

痛点 2:找一个结点的"前驱"和"后继"代价极高

如果二叉树已经建好,突然问你:"在中序遍历 下,结点 D 的前一个结点(前驱)是谁?后一个结点(后继)是谁?"

  • 在普通二叉链表里,结点只有指向左、右孩子的指针,根本没有指向前驱或后继的直接指针

  • 你必须从根结点开始,重新跑一遍中序遍历(还要借助系统栈或递归),一路找到 D,才能知道它前后是谁。

解决方案:

能不能废物利用 ,把那 n + 1 n+1 n+1 个空闲的 NULL 指针利用起来,直接指向该结点在某种遍历次序下的前驱后继

这种指向前驱和后继的指针,就被形象地称为"线索";给二叉树加上线索的过程,就叫做"线索化"。


第二部分:线索化的致命冲突与"标志位"设计

当把空指针改成线索后,计算机遇到了一个新的困惑:

如果拿到了一个指针 p->lchild,计算机怎么知道它存的到底是一个亲生左孩子 ,还是一个前驱结点的线索

为了解决这个二义性冲突,408 标准规定:给每个结点各增设两个布尔标志位(Tag)------ ltagrtag

text 复制代码
+----------+----------+----------+----------+----------+
|  lchild  |   ltag   |   data   |   rtag   |  rchild  |
+----------+----------+----------+----------+----------+
408 统一规范定义:
  • ltag == 0lchild 指向的是亲生左孩子

  • ltag == 1lchild 指向的是遍历序列中的直接前驱(线索)

  • rtag == 0rchild 指向的是亲生右孩子

  • rtag == 1rchild 指向的是遍历序列中的直接后继(线索)

直观口诀"0 是真孩子,1 是认线索"

用 C 语言代码表达就是:

c 复制代码
// 408 统编标准:线索二叉树结点定义
typedef struct ThreadNode {
    int data;                       // 数据域
    struct ThreadNode *lchild;     // 左孩子 / 前驱线索
    struct ThreadNode *rchild;     // 右孩子 / 后继线索
    int ltag, rtag;                 // 左右标志位:0 为孩子,1 为线索
} ThreadNode, *ThreadTree;

第三部分:字符画深度拆解------中序线索二叉树

在 408 考查的所有线索树中,中序线索二叉树(InOrder Threaded Tree) 占据了 90% 以上的考卷篇幅。

看这棵原型树:

text 复制代码
           A
         /   \
        B     C
       / \
      D   E
1. 写出中序遍历序列(左根右)

中序遍历的结果是:D, B, E, A, C

根据这个线性的队形,每一个结点的前驱与后继一目了然:

  • D 的前驱是 无(NULL) ,后继是 B

  • B 的前驱是 D ,后继是 E

  • E 的前驱是 B ,后继是 A

  • A 的前驱是 E ,后继是 C

  • C 的前驱是 A ,后继是 无(NULL)

2. 开始线索化改造(填补原本为 NULL 的空手)
  • 观察 D:叶子结点,原本左右手都是 NULL。

  • 左手原本是 NULL → \to → 改成指向前驱:D->lchild = NULL; D->ltag = 1;

  • 右手原本是 NULL → \to → 改成指向后继:D->rchild = B; D->rtag = 1;

  • 观察 B:左右手都有亲生孩子(D 和 E),所以 ltag = 0, rtag = 0,指针不用动。

  • 观察 E:叶子结点,原本左右手都是 NULL。

  • 左手原本是 NULL → \to → 改成指向前驱:E->lchild = B; E->ltag = 1;

  • 右手原本是 NULL → \to → 改成指向后继:E->rchild = A; E->rtag = 1;

  • 观察 A:左右手都有孩子,ltag = 0, rtag = 0,指针不用动。

  • 观察 C:只有头顶的连接,左右手都是 NULL。

  • 左手原本是 NULL → \to → 改成指向前驱:C->lchild = A; C->ltag = 1;

  • 右手原本是 NULL → \to → 序列结束:C->rchild = NULL; C->rtag = 1;

3. 改造后的纯字符画实况图(实线为树枝,虚线为线索)
text 复制代码
                  +-----+-----+-----+-----+-----+
                  |  *  |  0  |  A  |  0  |  *  |
                  +-----+-----+-----+-----+-----+
                   /                           \
                  /                             \ (实线孩子)
     +-----+-----+-----+-----+-----+           +-----+-----+-----+-----+-----+
     |  *  |  0  |  B  |  0  |  *  |           |  *  |  1  |  C  |  1  | NULL|
     +-----+-----+-----+-----+-----+           +-----+-----+-----+-----+-----+
      /           \                            : (虚线前驱指向 A)
     /             \ (实线孩子)                 +-------------------------------------+
    v               v                                                                |
+-------+       +-------+                                                            |
| D (叶)|       | E (叶) |                                                            |
+-------+       +-------+                                                            |
  :   :           :   :                                                              |
  :   +----(后继) -+   +----(后继指向 A)-----------------------------------------------+
  :        指向 B      :
  :                   +----(前驱指向 B)
  +--------(前驱为 NULL)

看到没有?经过这番改造后,整棵树在底层逻辑上已经被串成了一个双向链表


第四部分:线索化算法的核心------双指针游标跟踪机制

如何通过代码把这棵树自动线索化呢?

在 408 中,线索化的代码模板非常固定:在边做遍历的同时,使用一个全局/传入的指针 pre 时刻记录"刚刚访问过的前一个结点"

text 复制代码
访问顺序:  ... ---> [ 上一个结点 pre ] ====> [ 当前正在访问的结点 p ] ---> ...

在处理当前结点 p 时:

  1. 搞定 p 的前驱线索

    如果 p->lchild == NULL,说明 p 缺左孩子,赶紧让它的左手牵住刚刚访问的 prep->lchild = pre; p->ltag = 1;)。

  2. 搞定 pre 的后继线索

    如果 pre 存在并且 pre->rchild == NULL,说明 pre 缺右孩子,赶紧让 pre 的右手牵住当前的 p 作为后继(pre->rchild = p; pre->rtag = 1;)!

  3. 步进游标 :本次访问结束,将 pre = p;,让 pre 紧跟 p 的脚步。


第五部分:中序线索二叉树找前驱与后继(408 大题与选择题核心考点)

一旦一棵树完成了中序线索化,我们要找某一个结点 p 的后继或者前驱,就不再需要递归,也不需要辅助栈,可以在常数或极短时间内推导出来

1. 找结点 p 的中序后继(NextNode)
  • 情况一(好办) :如果 p->rtag == 1,它的右指针本身就是现成的后继线索,直接返回 p->rchild

  • 情况二(有右孩子) :如果 p->rtag == 0,说明 p 有右子树。

  • 根据中序遍历"左 → \to → 根 → \to → 右"的规律,p 的后继必然是右子树里第一个被访问的结点

  • 右子树里谁最先被访问? 当然是右子树中最左下角的那个人

  • 操作 :先走向右孩子 p = p->rchild,然后一路往左走到底(while(p->ltag == 0) p = p->lchild;)。

2. 找结点 p 的中序前驱(PriorNode)
  • 情况一(好办) :如果 p->ltag == 1,它的左指针本身就是前驱线索,直接返回 p->lchild

  • 情况二(有左孩子) :如果 p->ltag == 0,说明 p 有左子树。

  • p 的前驱必然是左子树里最后一个被访问的结点

  • 左子树里谁最后被访问? 当然是左子树中最右下角的那个人

  • 操作 :先走向左孩子 p = p->lchild,然后一路往右走到底(while(p->rtag == 0) p = p->rchild;)。


本阶段巩固练习与解析

题目 1(线索数量直接秒杀题)

已知一棵二叉树共有 12 个结点,对其进行线索化操作(无论是先序、中序还是后序),请问全树被改造出来的线索指针(即 tag 为 1 的指针域)一共有多少个?

答案与解析

  • 答案13 个

  • 详细解析

  • 根据二叉链表的通用数学性质: n n n 个结点的二叉树,总指针域为 2 n 2n 2n 个,非空指针(分支边)为 n − 1 n - 1 n−1 个,原本指向 NULL 的空指针域必为 n + 1 n + 1 n+1 个

  • 线索化的本质,就是把所有原先为 NULL 的空指针全部填上线索

  • 因此,线索的总个数严格等于原空指针的个数:

线索总数 = n + 1 = 12 + 1 = 13 \text{线索总数} = n + 1 = 12 + 1 = 13 线索总数=n+1=12+1=13

  • 也就是说,不论这棵树形态如何扭曲,线索一定正好是 13 条!

题目 2(408 选择题真题变形)

在中序线索二叉树中,已知结点 Xrtag == 0(即存在亲生右孩子),那么在中序遍历序列中,结点 X 的直接后继结点是哪一个?

答案与解析

  • 答案结点 X 的右子树中最左下角的结点(即右子树中沿左孩子指针一路走到底的结点)。
  • 详细解析
  • 中序遍历的顺序是:左子树 → \to → 根结点 → \to → 右子树
  • 访问完当前根结点 X 后,接下来一定会进入 X 的整个右子树进行遍历。
  • 进入右子树后,首先被访问的肯定是右子树里最靠左边的结点(即一路取 lchild 直到没有左孩子的那个结点)。
  • X 的后继必定是其右子树中最左下角的结点。

阶段五:二叉搜索树(BST)与二叉平衡树(AVL)

在前面的四个阶段里,我们掌握了树的物理存储、四大遍历以及线索化。但在实际计算机系统中,树结构最广泛的应用场景是海量数据的极速检索与动态维护

为了在动态插入和删除的同时实现类似折半查找的效率,二叉搜索树(BST)平衡二叉树(AVL) 应运而生。


第一部分:二叉搜索树(BST)------ "左小右大"的有序世界

1. 核心定义与铁律

二叉搜索树(Binary Search Tree,又称二叉排序树)可以是一棵空树,或者是具有下列性质的二叉树:

  1. 若左子树非空,则左子树上所有结点的值,均小于它的根结点的值
  2. 若右子树非空,则右子树上所有结点的值,均大于它的根结点的值
  3. 它的左、右子树也分别为二叉搜索树。
text 复制代码
               [ 50 ]
              /      \
          [ 30 ]    [ 70 ]
          /    \    /    \
        [10]  [40] [60]  [80]

408 核心命题定理

对任何一棵二叉搜索树进行【中序遍历】(左 → \to → 根 → \to → 右),得到的序列必定是一个严格单调递增的有序序列!

对上图进行中序遍历:10, 30, 40, 50, 60, 70, 80。反之,如果中序遍历不递增,则绝不是合格的 BST。

2. BST 的核心查找逻辑

要在 BST 中查找关键字 key,过程与折半查找几乎一致:

  • 若树为空,查找失败。
  • key == root->key,查找成功。
  • key < root->key,说明目标只可能藏在左边,递归/迭代深入左子树
  • key > root->key,说明目标只可能藏在右边,递归/迭代深入右子树
3. BST 的插入操作
  • 若树为空,直接生成新结点作为根。
  • 若树非空,沿着查找路径向下比较:比当前结点小就往左走,比当前结点大就往右走,直到遇到 NULL,新结点就作为叶子结点挂在该空位上
  • 注意 :新插入的结点必定是作为叶子结点添加进去的,不会破坏原树已有的相对拓扑结构。
4. BST 结点的删除操作(408 大题与选择题必考重点)

删除是 BST 中最精细的操作。假设要删除结点 z,必须分三种情况处理:

  • 情况 1:z 是叶子结点(无孩子)

  • 直接把老爹指向它的指针置为 NULL,然后释放 z

  • 情况 2:z 只有一棵子树(单亲爹,只有左孩子或只有右孩子)

  • z 的亲生孩子直接越级继承 z 原本的位置(由老爹接管孙子),释放 z

  • 情况 3:z 左右孩子双全(最复杂、考频最高)

  • 寻找 z直接后继(右子树的最左下角结点)直接前驱(左子树的最右下角结点) ,记为 s

  • s 的数据值覆盖写入 z 的数据域。

  • 转化为**在子树中删除结点 s**(由于 s 是最左下或最右下结点,它的度至多为 1,问题瞬间降解为情况 1 或情况 2 处理)。

text 复制代码
删除双全结点 50:
          [ 50 ] (待删)                       [ 60 ] (后继顶替原位)
         /      \                            /      \
     [ 30 ]    [ 70 ]        ===>        [ 30 ]    [ 70 ]
               /    \                              /    \
            [ 60 ]  [ 80 ]                      (删掉)  [ 80 ]
           (后继s)

第二部分:BST 的致命软肋与退化危机

BST 的查找效率完全取决于树的形态(树的高度)

最好情况与最坏情况对比
  • 最好情况 :树的形态均衡(类似满二叉树),高度为 ⌈ log ⁡ 2 ( n + 1 ) ⌉ \lceil \log_2(n+1) \rceil ⌈log2(n+1)⌉,平均查找长度 ASL 为 O ( log ⁡ 2 n ) O(\log_2 n) O(log2n)
  • 最坏情况 :输入的关键字原本就是有序的(如依次插入 10, 20, 30, 40, 50)。
    每个新结点都只能挂在上一个结点的右边,树退化成了一条单链表
text 复制代码
有序序列插入退化实况:
    [10]
       \
       [20]
          \
          [30]
             \
             [40]   <--- 树高直接飙升至 n,查找效率断崖式下跌为 O(n)!
                \
                [50]

为了彻底解决 BST 遇有序数据退化为单链表的问题,苏联数学家 Adelson-Velsky 和 Landis 提出了自平衡机制------AVL 树(平衡二叉树)


第三部分:平衡二叉树(AVL 树)------ 强制约束高度

1. 核心定义

一棵 AVL 树首先必须是一棵 二叉搜索树(BST),并且满足:

  • 树中任意结点的左子树和右子树的高度之差的绝对值不超过 1
2. 平衡因子(Balance Factor, BF)
  • 定义 :某个结点的左子树高度减去右子树高度的值。

BF = h l e f t − h r i g h t \text{BF} = h_{left} - h_{right} BF=hleft−hright

  • 合规要求 :整棵树中每一个结点的 BF 只能取 -101 这三个值。
  • 一旦某个结点的 ∣ BF ∣ > 1 \vert{}\text{BF}\vert{} > 1 ∣BF∣>1(变成了 2 2 2 或 − 2 -2 −2),说明这棵子树已经严重失衡,必须立即启动旋转操作来恢复平衡。
text 复制代码
   [合规 AVL 结点]           [失衡结点(违规!)]
        ( 0 )                     ( 2 )  <--- 左高 2,右高 0,失衡!
        [ A ]                     [ A ]
       /     \                   /
     (-1)    (0)               (1)
     [ B ]   [ C ]             [ B ]
        \                     /
        [ D ]               [ C ]

第四部分:AVL 树的四大旋转调整(408 每年必考手推题)

插入一个新结点后,整棵树可能会失衡。

调整原则 :从插入点向上回溯 ,找到离插入点最近的、平衡因子绝对值大于 1 的祖先结点,以该结点为根的子树称为"最小不平衡子树"。只要把它转平,整棵树就会恢复平衡。

根据新结点插入的位置相对于最小不平衡子树根结点(设为 A)的方位,失衡分为四种类型:


1. LL 型失衡:右单旋转(顺时针旋转)
  • 成因 :在 A 的左孩子(L) 左子树(L)上插入新结点,导致 A 的平衡因子由 1 变成 2。
  • 口诀抓牢中间结点,老爹向右倒下做右孩子,原右子树过继给老爹做左孩子
text 复制代码
    【LL 失衡态】                      【右单旋转后恢复平衡】
        [ A ] (BF=2)                         [ B ] (BF=0)
       /     \                              /     \
     [ B ]   (AR)          ====>          [ C ]   [ A ]
    /     \                              /       /     \
  [ C ]   (BR)                         (CL)    (BR)   (AR)
  /
(CL) (新插入使高度增加)

2. RR 型失衡:左单旋转(逆时针旋转)
  • 成因 :在 A 的右孩子(R) 右子树(R)上插入新结点,导致 A 的平衡因子由 -1 变成 -2。
  • 口诀抓牢中间结点,老爹向左倒下做左孩子,原左子树过继给老爹做右孩子
text 复制代码
    【RR 失衡态】                      【左单旋转后恢复平衡】
        [ A ] (BF=-2)                        [ B ] (BF=0)
       /     \                              /     \
     (AL)    [ B ]         ====>          [ A ]   [ C ]
            /     \                      /     \       \
          (BL)   [ C ]                 (AL)   (BL)     (CR)
                     \
                     (CR) (新插入使高度增加)

3. LR 型失衡:先左旋后右旋(双旋转)
  • 成因 :在 A 的左孩子(L)右子树(R)上插入新结点。
  • 本质 :失衡路径拐了个弯(左 → \to → 右),单次旋转无法抚平。
  • 解决策略
  1. 先对左孩子 B 进行一次 左旋转(RR 型调整),把拐弯拉直成 LL 型;
  2. 再对祖先 A 进行一次 右旋转(LL 型调整),彻底平衡。
text 复制代码
  【LR 失衡态】             【第1步:对 B 左旋拉直】      【第2步:对 A 右旋放平】
      [ A ] (BF=2)                 [ A ]                       [ C ]
     /     \                      /     \                     /     \
   [ B ]   (AR)      ====>      [ C ]   (AR)     ====>      [ B ]   [ A ]
  /     \                      /     \                     /   \   /   \
(BL)   [ C ]                 [ B ]   (CR)                (BL)(CL)(CR)(AR)
      /     \               /     \
    (CL)   (CR)           (BL)   (CL)

4. RL 型失衡:先右旋后左旋(双旋转)
  • 成因 :在 A 的右孩子(R)左子树(L)上插入新结点。
  • 解决策略
  1. 先对右孩子 B 进行一次 右旋转(LL 型调整),把拐弯拉直成 RR 型;
  2. 再对祖先 A 进行一次 左旋转(RR 型调整)

第五部分:AVL 树的四大旋转速记矩阵

失衡类型 导致失衡的插入位置 调整旋转动作 旋转后的新子树根结点
LL 型 根的左孩子的左子树 右单旋转(围绕 A 顺时针旋转) 原左孩子 B 成为新根
RR 型 根的右孩子的右子树 左单旋转(围绕 A 逆时针旋转) 原右孩子 B 成为新根
LR 型 根的左孩子的右子树 先对左孩子左旋,再对根右旋 原左孩子的右孙子 C 成为新根
RL 型 根的右孩子的左子树 先对右孩子右旋,再对根左旋 原右孩子的左孙子 C 成为新根

考场秒杀技巧

无论怎么旋转,旋转调整后的子树必须依然严格遵守 BST 的中序遍历有序性

做完旋转如果发现破坏了"左小右大",说明挂接子树时挂错了分支。


第六部分:AVL 树的最少结点数公式(408 极高频数值考点)

在 408 试卷中,经常反向提问:"高度为 h h h 的平衡二叉树,至少需要多少个结点?"

  • 设高度为 h h h 的 AVL 树拥有的最少结点数为 N h N_h Nh:

  • N 0 = 0 N_0 = 0 N0=0(空树)

  • N 1 = 1 N_1 = 1 N1=1(只有 1 个根结点)

  • N 2 = 2 N_2 = 2 N2=2(根 + 1 个孩子)

  • 要想让整棵树高度为 h h h 且结点最少,必须让一侧子树高度为 h − 1 h-1 h−1,另一侧子树高度为 h − 2 h-2 h−2,并且两边都取最少结点。因此推导出递推公式:

N h = N h − 1 + N h − 2 + 1 N_h = N_{h-1} + N_{h-2} + 1 Nh=Nh−1+Nh−2+1

数列前几项取值必须熟记于心:

树高 h h h 最少结点数 N h N_h Nh 递推计算式
1 1 N 1 = 1 N_1 = 1 N1=1
2 2 N 2 = 2 N_2 = 2 N2=2
3 4 N 3 = N 2 + N 1 + 1 = 2 + 1 + 1 = 4 N_3 = N_2 + N_1 + 1 = 2 + 1 + 1 = 4 N3=N2+N1+1=2+1+1=4
4 7 N 4 = N 3 + N 2 + 1 = 4 + 2 + 1 = 7 N_4 = N_3 + N_2 + 1 = 4 + 2 + 1 = 7 N4=N3+N2+1=4+2+1=7
5 12 N 5 = N 4 + N 3 + 1 = 7 + 4 + 1 = 12 N_5 = N_4 + N_3 + 1 = 7 + 4 + 1 = 12 N5=N4+N3+1=7+4+1=12
6 20 N 6 = N 5 + N 4 + 1 = 12 + 7 + 1 = 20 N_6 = N_5 + N_4 + 1 = 12 + 7 + 1 = 20 N6=N5+N4+1=12+7+1=20

本阶段巩固练习与解析

题目 1(408 真实大题变形:连续插入与平衡构建)

已知关键字输入序列为:{ 15, 3, 7, 10, 9, 8 }

请按照该序列依次插入构建一棵平衡二叉树(AVL 树),给出构建过程中的失衡调整类型,并画出最终生成的平衡二叉树。

答案与解析

  • 逐步推演
  1. 插入 15,根为 15。
  2. 插入 3,挂在 15 的左边。
  3. 插入 7
  • 7 比 15 小(往左),比 3 大(挂在 3 的右边)。
  • 此时 15 的 BF 变为 2,失衡路径为 15 的左孩子(3)的右子树(7),发生 LR 型失衡
  • 调整:以 7 为新根,3 为左孩子,15 为右孩子。
  • 当前树形态:
text 复制代码
      [ 7 ]
     /     \
   [ 3 ]   [ 15 ]
  1. 插入 10:挂在 15 的左边,全树依然平衡。
  2. 插入 9
  • 9 挂在 10 的左边。
  • 此时结点 15 的左子树高 2,右子树高 0(失衡!),失衡根为 15。
  • 失衡路径为 15 的左孩子(10)的左子树(9),发生 LL 型失衡
  • 调整(围绕 15 右单旋):10 成为局部新根,9 为左孩子,15 为右孩子。
  • 此时整棵树平衡,形态为:
text 复制代码
        [ 7 ]
       /     \
     [ 3 ]   [ 10 ]
            /      \
          [ 9 ]    [ 15 ]
  1. 插入 8
  • 8 挂在 9 的左边。

  • 此时根结点 7 的左高 1,右高 3(BF = -2,全树失衡!),失衡根为 7。

  • 从 7 向下看:右孩子是 10,10 的左孩子是 9(插入发生在 9 的左边),失衡路径为 7 的"右孩子的左子树",发生 RL 型失衡

  • 调整:先对 10 右单旋,再对 7 左单旋(中间结点 9 升为全树新根)。

  • 最终生成的 AVL 树形态

text 复制代码
             [ 9 ]
            /     \
        [ 7 ]     [ 10 ]
        /   \          \
      [ 3 ] [ 8 ]      [ 15 ]
  • 验证中序遍历:3, 7, 8, 9, 10, 15,完全递增,且每个结点的 ∣ BF ∣ ≤ 1 \vert{}\text{BF}\vert{} \le 1 ∣BF∣≤1。

题目 2(最少结点数反求树高)

若一棵平衡二叉树共有 13 个结点,请问这棵树的最大高度是多少?

答案与解析

  • 答案5

  • 详细解析

  • 题目要求"最大高度",即寻找在该高度下,树能成立所允许的"最少结点数"。

  • 查阅高度与最少结点数对应关系:

  • 高度 h = 4 h = 4 h=4 最少需要 N 4 = 7 N_4 = 7 N4=7 个结点。

  • 高度 h = 5 h = 5 h=5 最少需要 N 5 = 12 N_5 = 12 N5=12 个结点。

  • 高度 h = 6 h = 6 h=6 最少需要 N 6 = 20 N_6 = 20 N6=20 个结点。

  • 现在给定总结点数 n = 13 n = 13 n=13:

  • 因为 13 ≥ 12 13 \ge 12 13≥12(满足了高度为 5 的门槛),所以它完全可以达到高度 5

  • 但因为 13 < 20 13 < 20 13<20(不够高度为 6 的最低门槛),所以它绝不可能达到高度 6

  • 故该平衡二叉树的最大高度为 5


阶段六:并查集(Disjoint-Set Union)------ 极简高效的集合管理者

并查集是 408 考研中代码行数最少、概念最凝练,但在选择题与算法大题(如 2022 年真题)中出镜率极高的结构。它天生用于解决"谁和谁是一伙的"(动态连通性 / 等价类划分)问题。


第一步:生活中的并查集("武林门派认师傅")

假设江湖上有 6 个独行侠客,编号为 0 到 5。起初互不相识,各自成立门派,自己当掌门:

  • 每个人心里只记一件事:我的直属师傅是谁?
  • 如果自己就是掌门,头上没人,就标记为特殊的负数(如 -1)。

看字符画演变过程:

text 复制代码
【初始状态:6 个独立门派】
   [0]      [1]      [2]      [3]      [4]      [5]
  (-1)     (-1)     (-1)     (-1)     (-1)     (-1)
(掌门 0)  (掌门 1)  (掌门 2)  (掌门 3)  (掌门 4)  (掌门 5)

江湖开始结盟与火拼:

  1. 0 号与 1 号结拜,0 资历老当掌门:1 号认 0 号当师傅,1 号头上牵一根线指向 0。
  2. 2 号与 3 号结拜,2 当掌门:3 号认 2 号当师傅。
  3. 1 号与 3 号发生交集:门派合并不能私定终身,1 号请出大掌门 0,3 号请出大掌门 2。0 号与 2 号合并,假设 2 号带着整个门派归顺 0 号。
text 复制代码
【结盟合并后的门派树】
           [ 0 ] (武林总盟主 / 根)
          /     \
       [ 1 ]   [ 2 ]
                 |
               [ 3 ]

此时:

  • 问:"3 号和 1 号是一伙的吗?"
  • 3 号顺藤摸瓜找到总掌门是 0;1 号顺藤摸瓜找到总掌门也是 0。总掌门相同,必定是一伙的!
  • 这种"查掌门(Find)" "门派结盟(Union)"的操作,在计算机中就是并查集。

第二步:并查集的物理载体------极简的一维数组 parent[]

普通树可能一个父亲带 10 个孩子,如果用孩子指针存储会产生大量开销。

然而在并查集中,我们只需要逆向找祖宗,永远不需要顺向找孩子

因此,并查集采用阶段二讲过的 双亲表示法(顺序存储,静态数组)

1. 数组定义与物理含义

开辟一个一维连续数组 int parent[n],数组下标代表元素编号:

  • parent[i] >= 0 :表示元素 i亲生父亲下标是 parent[i]
  • parent[i] < 0 :表示元素 i 头上没有父亲,它自身就是这棵集合树的根结点(代表元/盟主)
  • 408 统编教材高分技巧 :根结点存的值不仅是负数,其绝对值 ∣ p a r e n t i ∣ \vert{}parenti\vert{} ∣parenti∣ 恰好记录该集合当前拥有的结点总数(集合规模)
2. 初始状态与合并后的内存数组切片对照
text 复制代码
【初始状态:每个人自成一派,各自规模为 1】
下标 i:        0     1     2     3     4     5
parent[i]:   -1    -1    -1    -1    -1    -1
含义:       (根,1) (根,1) (根,1) (根,1) (根,1) (根,1)

【合并 0 与 1、2 与 3,再合并 0 与 2 后】
此时集合树形态:
         [ 0 ] (规模累加为 4)
        /     \
     [ 1 ]   [ 2 ]
               |
             [ 3 ]

数组物理存储状态:
下标 i:        0     1     2     3     4     5
parent[i]:   -4     0     0     2    -1    -1
              ^     ^     ^     ^     ^     ^
              |     |     |     |     +-----+--- 4 和 5 没结盟,依然是独立根
              |     |     |     +--- 3 的爹是 2
              |     +-----+--------- 1 和 2 的爹都是 0
              +--------------------- 0 是根,其值为 -4(旗下聚集了 4 个结点)

第三步:两大核心操作与 408 优化杀招

并查集的朴素实现如果处理不当,树容易退化成单链表(导致 Find 操作退化为 O ( n ) O(n) O(n))。为此,408 考纲明确要求掌握两大满分优化:路径压缩小树合并到大树(按秩合并)

1. 查操作(Find)与路径压缩(Path Compression)
  • 朴素查找 :顺着 parent 数组一级级向上跳,直到遇到负数。

  • 缺点:如果树很深,每次找祖宗都要向上攀爬很久。

  • 路径压缩策略 :既然全派系只有一个总盟主,那么所有小弟完全可以直接和总盟主单线联系,何必层层汇报?

  • 做法:在递归查找根结点的回溯过程中,把沿途经过的所有结点的父指针,全部顺手修改为总根结点

看字符画对比:

text 复制代码
【压缩前:深层链状结构】               【执行一次 Find(4) 路径压缩后】
        [ 0 ] (根)                                [ 0 ] (根)
          |                                     /  |  \  \
        [ 1 ]                                [ 1 ][ 2 ][ 3 ][ 4 ]
          |                                (所有结点直接挂在根下,
        [ 2 ]                               树的高度被瞬间拍平成 2!)
          |
        [ 3 ]
          |
        [ 4 ] <--- 从这里发起查找
  • 代码考场核心
c 复制代码
int Find(int parent[], int x) {
    if (parent[x] < 0) return x; // 找到根
    return parent[x] = Find(parent, parent[x]); // 赋值同时回溯压缩
}

2. 并操作(Union)与小树合并到大树(Union by Size / Rank)

如果盲目地把大门派归顺到小门派底下,树的高度会白白增加。

  • 优化法则矮树/小树必须合并到高树/大树下
  • 判定依据:检查两棵树根结点存储的负数绝对值(即规模)。
  • 绝对值小的(结点少),其根结点的父指针修改为大树根的编号。
  • 绝对值大的(结点多),将其负数值加上小树的负数值(累加规模)。
text 复制代码
【两棵待合并的树】
       [ 0 ] (规模 4, parent[0]=-4)             [ 5 ] (规模 2, parent[5]=-2)
      /  |  \                                     |
    [1] [2] [3]                                 [ 6 ]

【合并原则:小集合 5 归顺到大集合 0 门下】
               [ 0 ] (规模变为 4+2=6, parent[0] 变为 -6)
             /  |  \  \
           [1] [2] [3] [ 5 ] <--- 原小树根直接作为 0 的孩子
                         |
                       [ 6 ]

第四步:408 并查集经典命题模型(解题模板)

在 408 真题中,只要看到以下三种业务场景,直接条件反射搬出并查集:

模型 1:无向图的连通分量计数
  • 题目原型 :给定 n n n 个顶点和一堆边,求该图共有多少个独立的连通块。
  • 秒杀思路
  1. 初始化 parent 数组为全 -1
  2. 遍历每条边 ( u , v ) (u, v) (u,v),调用 Union(parent, u, v)
  3. 遍历完所有边后,数一下 parent 数组里一共有多少个负数,负数的总个数就是连通分量的数量!
模型 2:无向图环路判定(2022 年 408 统考 15 分大题原型)
  • 题目原型:依次加入边,判断图是否产生环。
  • 秒杀思路
  • 对于新读入的边 ( u , v ) (u, v) (u,v):
  • 先求 rootU = Find(u); rootV = Find(v);
  • rootU == rootV :说明 u u u 和 v v v 早就已经连通了,此时再添一条边,必定直接成环!
  • rootU != rootV :说明两点尚未连通,安全,执行 Union(rootU, rootV)
模型 3:最小生成树 Kruskal 算法的唯一避环工具
  • Kruskal 算法的核心是"贪心挑短边"。但挑选过程中如何保证不选出回路?
  • 完全依靠并查集 :每次挑选权重最小的边 ( u , v ) (u, v) (u,v),用 Find 检测两端点。不同集合才选入并 Union;同一集合直接扔掉。

本阶段巩固练习与解析

练习 1(408 选择题真题变形:并查集状态逆推)

已知一个集合的元素为 { 0 , 1 , 2 , 3 , 4 , 5 , 6 } \{0, 1, 2, 3, 4, 5, 6\} {0,1,2,3,4,5,6},采用双亲表示法存储。初始时 parent 全为 -1

依次执行以下合并操作(平手时,序号小的做根;且合并小树到大树):

  • Union(0, 1)
  • Union(2, 3)
  • Union(4, 5)
  • Union(0, 2)
  • Union(0, 4)

请写出执行完毕后:

  1. 整个图一共有多少个连通分量?
  2. 此时 parent[0] 的值是多少?parent[4] 的值是多少?

答案与解析

  • 连通分量个数2 个 (集合一: { 0 , 1 , 2 , 3 , 4 , 5 } \{0, 1, 2, 3, 4, 5\} {0,1,2,3,4,5},集合二:独立未被碰过的 { 6 } \{6\} {6})。
  • parent[0] 的值-6
  • parent[4] 的值0
  • 推导过程
  1. Union(0, 1) → \to → 规模各为 1,0 做根,parent[0] = -2, parent[1] = 0
  2. Union(2, 3) → \to → 2 做根,parent[2] = -2, parent[3] = 2
  3. Union(4, 5) → \to → 4 做根,parent[4] = -2, parent[5] = 4
  4. Union(0, 2) → \to → 根 0 与根 2 规模同为 2,0 做根,把 2 挂在 0 下:parent[0] = -4, parent[2] = 0
  5. Union(0, 4) → \to → 根 0 规模为 4,根 4 规模为 2,小树并入大树,4 挂在 0 下:parent[0] = -4 + (-2) = -6parent[4] = 0
  6. 元素 6 从始至终未参与合并,parent[6] = -1
  7. 数组中只有 parent[0]parent[6] 两个负数,故连通分量为 2 个。

练习 2(复杂度思辨题)

采用路径压缩按秩合并 双重优化后的并查集,在对包含 n n n 个元素的集合执行连续 m m m 次 FindUnion 操作时,其平均时间复杂度是多少?

答案与解析

  • 答案O ( m ⋅ α ( n ) ) O(m \cdot \alpha(n)) O(m⋅α(n)) ,其中 α ( n ) \alpha(n) α(n) 为阿克曼函数(Ackermann Function)的反函数
  • 解析
  • 反阿克曼函数 α ( n ) \alpha(n) α(n) 的增长极其缓慢。对于宇宙中任何物理级别的天文数字 n n n(比如 n = 10 80 n = 10^{80} n=1080), α ( n ) ≤ 4 \alpha(n) \le 4 α(n)≤4。
  • 因此在工程与考研理论中,经过双重优化的并查集,单次操作的平摊时间复杂度可以近似看作 O ( 1 ) O(1) O(1) 常数级,效率高到极致。

相关推荐
木井巳1 小时前
【记忆化搜索】最长递增子序列
java·算法·leetcode·深度优先·剪枝·推荐算法
2601_955518181 小时前
如何学好C语言:从入门到精通,掌握编程基石
c语言·指针·项目实战·调试技巧·编程基础
东方护航数据恢复(深圳)1 小时前
本地小算力设备数据恢复经典案例实操全解_东方护航数据恢复深圳店
大数据·服务器·算法·ai·gpu算力
Omics Pro1 小时前
上海AI Lab孙思琦×高张阳:虚拟细胞代码库智能体
数据库·人工智能·算法·机器学习·自然语言处理
goujunwe1 小时前
企业 GEO 落地三步法:诊断、内容重构、AI 引用效果监测
大数据·人工智能·学习方法·传媒
Y幽谷客1 小时前
图像识别入门习题整理
算法
是隼人1 小时前
buuctf-pwn ciscn_2019_sw_1(32位只有一次格式化机会)题解(学习过程持续更新)
c语言·学习·安全·pwn入门·ctf入门
careathers2 小时前
【数据结构】队列
java·数据结构
点纭2 小时前
C 语言 第七章 常用函数(3)
c语言·开发语言·算法·oracle·c#