
文章目录
- [阶段一:树与二叉树的核心概念与 408 必考数学性质](#阶段一:树与二叉树的核心概念与 408 必考数学性质)
-
-
- 第一步:什么是树?(把它当成"倒立生长的家族族谱")
- 第二步:为什么计算机特别偏爱"二叉树"?
- 第三步:二叉树界的两大型男------"满二叉树"与"完全二叉树"
-
- [1. 满二叉树:绝对对称的完美金字塔](#1. 满二叉树:绝对对称的完美金字塔)
- [2. 完全二叉树:按学号从左往右报数,中间绝不空缺!](#2. 完全二叉树:按学号从左往右报数,中间绝不空缺!)
- [第四步:408 核心公式------为什么 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1?](#第四步:408 核心公式——为什么 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1?)
- 第五步:完全二叉树在内存里的"神奇数组阵法"
- 第六步:二叉链表与著名的"空指针大坑"
-
- [1. 结点结构长相](#1. 结点结构长相)
- [2. 空指针计数(线索二叉树的核心伏笔)](#2. 空指针计数(线索二叉树的核心伏笔))
- 试刀验证
-
- [练习 1 答案与解析](#练习 1 答案与解析)
- [练习 2 答案与解析](#练习 2 答案与解析)
-
- 阶段二:普通树的三大存储结构与森林转换魔法
-
-
- [第一部分:双亲表示法(顺序存储,数组)------ 并查集的物理基石](#第一部分:双亲表示法(顺序存储,数组)—— 并查集的物理基石)
-
- [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(反向结构判定题))
-
- 阶段三:二叉树四大遍历全掌握与反向构造定理
-
-
- 第一部分:为什么二叉树要分先、中、后、层序遍历?
- 第二部分:先序、中序、后序的本质(以"根(当前结点)"的访问时机命名)
- [第三部分:复杂二叉树三种深度遍历的"描边法"(考场 3 秒出结果神技)](#第三部分:复杂二叉树三种深度遍历的“描边法”(考场 3 秒出结果神技))
- 第四部分:层序遍历(推土机战术)与队列实现模型
- [第五部分:408 必考大题/选择题------二叉树的"反向重构定理"](#第五部分: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. 找结点 `p` 的中序后继(NextNode)](#1. 找结点
- 本阶段巩固练习与解析
-
- [题目 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。 -
树的度:全家族里"生育能力最强的那个人生了几个"。
-
这张图里
A和D都生了 3 个,没人比他们更多,所以这棵树的度是 3。 -
树的高度 / 深度 :家族总共盖了多少层楼。从顶层根结点算第 1 层,向下数到底,这棵树的高(深)度是 4。
第二步:为什么计算机特别偏爱"二叉树"?
普通树里,一个爹可以生任意多个娃(生 3 个、5 个甚至 100 个),这在计算机内存里极其难设计:每个结点到底该预留几个指针指向它的孩子?
- 预留少了不够用,预留多了(比如每个结点开 10 个指针)大部分指针又都是空的,严重浪费内存。
于是,计算机科学家给家族立了一个硬规矩------严格的二胎政策:
- 每个结点最多只能生两个孩子(结点的度最大只能是 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:一棵二叉树里,生了两个孩子的爹(度为 2 的结点)一共有 15 个,生了一个孩子的单亲爹一共有 8 个,请问:
- 这棵树一共有多少个叶子结点?
- 这棵树总共有多少个结点?
- 练习 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的长子是A;A的亲兄弟是B;B的亲兄弟是C。 -
A的长子是D;D的亲兄弟是E。 -
C的长子是F;F的长子是G;G的亲兄弟是H;H的亲兄弟是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 选择题高频避坑指南
- 树的叶子结点在转换后的二叉树中呈现什么特征?
- 结论 :其左指针必定为空(
lchild == NULL)!因为叶子结点没有长子。但请注意:它的右指针可以非空,因为叶子结点可以有亲兄弟。
- 一棵树转换为二叉树后,根结点的右指针为什么必定为
NULL?
- 结论 :因为全树只有一个老祖宗根结点,它在全树中绝无任何亲兄弟!
- 森林转换成二叉树时,根结点的右指针何时非空?
- 结论 :森林由多棵树组成,第二棵树、第三棵树的根,都是第一棵树根的"兄弟"。因此,从第一棵树的根结点的右指针出发,串联起来的正是后面整片森林!
本阶段巩固练习与解析
题目 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 == NULL 且 X->nextsibling != NULL。请问该结点 X X X 在原本的普通树中是什么身份?
答案与解析:
-
答案 :它在原本的普通树中是一个叶子结点,并且它有亲兄弟。
-
详细解析:
-
firstchild代表自己的长子。由于firstchild == NULL,说明该结点生不出任何后代,因此在原普通树中必定是叶子结点。 -
nextsibling代表自己的下一个兄弟。由于nextsibling != NULL,说明在原本的普通树中,它右侧排着同一辈的亲兄弟。
阶段三:二叉树四大遍历全掌握与反向构造定理
这是 408 考研初试中分值占比极高的一块(选择题必考遍历序列反求与计算,算法大题的核心框架更是 100% 依赖遍历展开)。
第一部分:为什么二叉树要分先、中、后、层序遍历?
在线性表、栈、队列中,元素排成一条直队,遍历只有"从前往后"一条路。
而二叉树是非线性结构,每一个结点都有可能延伸出左、右两条分叉。
按照计算机搜索路径的不同,遍历分为两大门派:
-
广度优先搜索(BFS) :层序遍历(一层一层推土机式横扫,依靠队列实现)。
-
深度优先搜索(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
底层工作引擎:必须借助【队列】驱动
-
根入队:老祖宗 A 进队。
-
循环执行出队与带娃操作:
-
弹出队头结点,访问它(打印)。
-
如果它有左孩子,左孩子入队。
-
如果它有右孩子,右孩子入队。
- 队列为空时,全部层级遍历结束。
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
解题三步走口诀:
-
看先序抓根 :先序第一个元素是
a,所以整棵大树的**根必定是a**。 -
看中序切西瓜 :在
d g b a e c h f中找到a:
-
a左边的部分[d g b],必定全部属于a的左子树! -
a右边的部分[e c h f],必定全部属于a的右子树!
- 递归切子树:
- 左子树成员
[d g b]在先序中先出现的是b,所以b是左子树的根;在d g b中b的左边是[d g],右边没有东西,说明b只有左孩子。 - 右子树成员
[e c h f]在先序中先出现的是c,所以c是右子树的根;在e c h f中,e在c左边(左孩子),[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
- 推导过程:
-
后序序列的最后一个字符是
A,说明A是整棵树的根结点。 -
观察中序序列,在
A处一刀两断:
-
A左边是[B D C E](整棵左子树)。 -
A右边是[F H G](整棵右子树)。
- 分析左子树
[B D C E]:
- 它们在后序序列中对应的片段是
D E C B,后序末尾是B,故B是左子树的根。 - 在中序中,
B左边为空,右边是[D C E],说明B没有左孩子,只有右子树。 - 子树
[D C E]在后序中末尾是C,故C是根;在中序中D在C左边,E在C右边,所以C左孩子为D,右孩子为E。
- 分析右子树
[F H G]:
-
对应后序片段是
H G F,后序末尾是F,故F是右子树的根。 -
在中序中,
F的左边为空,右边是[H G],说明F无左孩子,只有右子树。 -
对应后序末尾是
G,G的中序左边是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, B和B, 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)------ ltag 和 rtag:
text
+----------+----------+----------+----------+----------+
| lchild | ltag | data | rtag | rchild |
+----------+----------+----------+----------+----------+
408 统一规范定义:
-
ltag == 0:lchild指向的是亲生左孩子。 -
ltag == 1:lchild指向的是遍历序列中的直接前驱(线索)。 -
rtag == 0:rchild指向的是亲生右孩子。 -
rtag == 1:rchild指向的是遍历序列中的直接后继(线索)。
直观口诀 :"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 时:
-
搞定
p的前驱线索 :如果
p->lchild == NULL,说明p缺左孩子,赶紧让它的左手牵住刚刚访问的pre(p->lchild = pre; p->ltag = 1;)。 -
搞定
pre的后继线索 :如果
pre存在并且pre->rchild == NULL,说明pre缺右孩子,赶紧让pre的右手牵住当前的p作为后继(pre->rchild = p; pre->rtag = 1;)! -
步进游标 :本次访问结束,将
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 选择题真题变形)
在中序线索二叉树中,已知结点 X 的 rtag == 0(即存在亲生右孩子),那么在中序遍历序列中,结点 X 的直接后继结点是哪一个?
答案与解析:
- 答案 :结点
X的右子树中最左下角的结点(即右子树中沿左孩子指针一路走到底的结点)。 - 详细解析:
- 中序遍历的顺序是:左子树 → \to → 根结点 → \to → 右子树。
- 访问完当前根结点
X后,接下来一定会进入X的整个右子树进行遍历。 - 进入右子树后,首先被访问的肯定是右子树里最靠左边的结点(即一路取
lchild直到没有左孩子的那个结点)。 - 故
X的后继必定是其右子树中最左下角的结点。
阶段五:二叉搜索树(BST)与二叉平衡树(AVL)
在前面的四个阶段里,我们掌握了树的物理存储、四大遍历以及线索化。但在实际计算机系统中,树结构最广泛的应用场景是海量数据的极速检索与动态维护。
为了在动态插入和删除的同时实现类似折半查找的效率,二叉搜索树(BST) 与 平衡二叉树(AVL) 应运而生。
第一部分:二叉搜索树(BST)------ "左小右大"的有序世界
1. 核心定义与铁律
二叉搜索树(Binary Search Tree,又称二叉排序树)可以是一棵空树,或者是具有下列性质的二叉树:
- 若左子树非空,则左子树上所有结点的值,均小于它的根结点的值。
- 若右子树非空,则右子树上所有结点的值,均大于它的根结点的值。
- 它的左、右子树也分别为二叉搜索树。
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 只能取
-1、0、1这三个值。 - 一旦某个结点的 ∣ 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 → 右),单次旋转无法抚平。
- 解决策略:
- 先对左孩子
B进行一次 左旋转(RR 型调整),把拐弯拉直成 LL 型; - 再对祖先
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)上插入新结点。 - 解决策略:
- 先对右孩子
B进行一次 右旋转(LL 型调整),把拐弯拉直成 RR 型; - 再对祖先
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 树),给出构建过程中的失衡调整类型,并画出最终生成的平衡二叉树。
答案与解析:
- 逐步推演:
- 插入
15,根为 15。 - 插入
3,挂在 15 的左边。 - 插入
7:
- 7 比 15 小(往左),比 3 大(挂在 3 的右边)。
- 此时 15 的 BF 变为 2,失衡路径为 15 的左孩子(3)的右子树(7),发生 LR 型失衡!
- 调整:以 7 为新根,3 为左孩子,15 为右孩子。
- 当前树形态:
text
[ 7 ]
/ \
[ 3 ] [ 15 ]
- 插入
10:挂在 15 的左边,全树依然平衡。 - 插入
9:
- 9 挂在 10 的左边。
- 此时结点 15 的左子树高 2,右子树高 0(失衡!),失衡根为 15。
- 失衡路径为 15 的左孩子(10)的左子树(9),发生 LL 型失衡!
- 调整(围绕 15 右单旋):10 成为局部新根,9 为左孩子,15 为右孩子。
- 此时整棵树平衡,形态为:
text
[ 7 ]
/ \
[ 3 ] [ 10 ]
/ \
[ 9 ] [ 15 ]
- 插入
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)
江湖开始结盟与火拼:
- 0 号与 1 号结拜,0 资历老当掌门:1 号认 0 号当师傅,1 号头上牵一根线指向 0。
- 2 号与 3 号结拜,2 当掌门:3 号认 2 号当师傅。
- 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 个顶点和一堆边,求该图共有多少个独立的连通块。
- 秒杀思路:
- 初始化
parent数组为全-1。 - 遍历每条边 ( u , v ) (u, v) (u,v),调用
Union(parent, u, v)。 - 遍历完所有边后,数一下
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)
请写出执行完毕后:
- 整个图一共有多少个连通分量?
- 此时
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。- 推导过程:
Union(0, 1)→ \to → 规模各为 1,0 做根,parent[0] = -2, parent[1] = 0。Union(2, 3)→ \to → 2 做根,parent[2] = -2, parent[3] = 2。Union(4, 5)→ \to → 4 做根,parent[4] = -2, parent[5] = 4。Union(0, 2)→ \to → 根 0 与根 2 规模同为 2,0 做根,把 2 挂在 0 下:parent[0] = -4, parent[2] = 0。Union(0, 4)→ \to → 根 0 规模为 4,根 4 规模为 2,小树并入大树,4 挂在 0 下:parent[0] = -4 + (-2) = -6,parent[4] = 0。- 元素 6 从始至终未参与合并,
parent[6] = -1。 - 数组中只有
parent[0]和parent[6]两个负数,故连通分量为 2 个。
练习 2(复杂度思辨题)
采用路径压缩 和按秩合并 双重优化后的并查集,在对包含 n n n 个元素的集合执行连续 m m m 次 Find 和 Union 操作时,其平均时间复杂度是多少?
答案与解析:
- 答案 :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) 常数级,效率高到极致。