QEQuantumEinsteinSearchCtrl/⌘K

第五章 树与二叉树

14 分钟阅读6,07026 个小节
返回目录

第五章 树与二叉树

5.1 树的基本概念

一、树的定义

树(Tree)nnn0n \geq 0)个结点的有限集合。n=0n=0 时称为空树。在任一非空树中:

  1. 有且仅有一个特定的称为**根(Root)**的结点
  2. n>1n>1 时,其余结点可分为 mmm>0m>0)个互不相交的有限集合 T1,T2,,TmT_1, T_2, \ldots, T_m,其中每一个集合本身又是一棵树,称为根的子树(SubTree)

🗣️ 大白话:树就像一棵倒过来的真实的树——根在最上面,树枝往下分叉。一个根可以有很多树枝(子树),每个树枝又可以继续分叉。

二、基本术语

术语定义大白话
结点的度结点拥有的子树数目这个人有几个亲儿子
树的度树中结点的最大度数整棵树里儿子最多的那个人有几个儿子
叶子结点度为0的结点"绝后"的结点,没有儿子
分支结点度不为0的结点有儿子的结点
结点的层次根为第1层,根的孩子为第2层,以此类推第几代
树的高度(深度)结点的最大层次一共有几代人
路径从结点 aa 到结点 bb 所经过的结点序列从祖先到子孙走的路
路径长度路径上经过的边的条数走了几步
森林mmm0m \geq 0)棵互不相交的树的集合多棵树组成的树林

三、树的重要性质

  1. 树中的结点数 == 所有结点的度数之和 +1+ 1(即总边数 +1+ 1

    证明:每条边对应一个非根结点(边连接父→子),故边数 = n1n - 1。又边数 = 所有结点度数之和(每条边由父结点的度贡献)。因此 n1=din - 1 = \sum d_i,即 n=di+1n = \sum d_i + 1

  2. 度为 mm 的树中第 ii 层上至多有 mi1m^{i-1} 个结点(i1i \geq 1

  3. 高度为 hhmm 叉树至多有 mh1m1\frac{m^h - 1}{m - 1} 个结点(等比数列求和)

  4. 具有 nn 个结点的 mm 叉树的最小高度为 logm(n(m1)+1)\lceil \log_m(n(m-1)+1) \rceil

树的6个重要性质总结

性质公式/说明
结点数 = 总度数 + 1n=1+i=1minin = 1 + \sum_{i=1}^{m} i \cdot n_inin_i 为度为 ii 的结点数)
ii 层最多结点数mi1m^{i-1}
hhmm 叉树最多结点(mh1)/(m1)(m^h - 1)/(m-1)
nn 个结点的 mm 叉树最小高logm(n(m1)+1)\lceil \log_m(n(m-1)+1) \rceil
叶子结点数公式n0=1+i=2m(i1)nin_0 = 1 + \sum_{i=2}^{m}(i-1)n_i(由性质1推导)
nn 个结点的树边数n1n - 1

⚠️ 度为 mm 的树 vs mm 叉树

  • 度为 mm 的树:至少有一个结点的度为 mm(树中确实存在度为 mm 的结点)
  • mm 叉树:每个结点的度最多为 mm(允许所有结点度都小于 mm,甚至可以是空树)
  • 度为 mm 的树至少有 m+1m+1 个结点(根结点+mm 个孩子);mm 叉树可以是空树

5.2 二叉树

一、二叉树的定义

二叉树(Binary Tree):每个结点最多只有两棵子树,且子树有左右之分,次序不能颠倒。

五种基本形态

  1. 空二叉树
  2. 只有根结点
  3. 只有左子树
  4. 只有右子树
  5. 左右子树都有

特殊二叉树

类型定义特点
满二叉树所有分支结点都有两个孩子且叶子都在同一层高度为 hh 的满二叉树有 2h12^h - 1 个结点
完全二叉树与满二叉树对比,只有最后一层可能不满,且最后一层结点从左到右连续若有度为1的结点,则只有一个且它只有左孩子
二叉排序树(BST)左子树所有结点值 < 根 < 右子树所有结点值中序遍历得到递增序列
平衡二叉树(AVL)任意结点的左右子树高度差的绝对值 ≤ 1保证查找效率

二、二叉树的性质(高频考点!)

  1. 非空二叉树上,叶子结点数 n0n_0 与度为2的结点数 n2n_2 的关系:

n0=n2+1n_0 = n_2 + 1

🗣️ 大白话:叶子结点数 = 双分支结点数 + 1。这是二叉树最重要的公式之一!

证明:设 n0,n1,n2n_0, n_1, n_2 分别为度为 0、1、2 的结点数。

  • 结点总数:n=n0+n1+n2n = n_0 + n_1 + n_2
  • 边数:n1=n1+2n2n - 1 = n_1 + 2n_2(每个度为1的结点贡献1条边,度为2的贡献2条边)
  • 两式相减:1=n0n21 = n_0 - n_2,即 n0=n2+1n_0 = n_2 + 1
  1. 非空二叉树第 ii 层上至多有 2i12^{i-1} 个结点(i1i \geq 1
  2. 高度为 hh 的二叉树至多有 2h12^h - 1 个结点
  3. 完全二叉树的性质
    • 具有 nn 个结点的完全二叉树的高度为 log2n+1\lfloor \log_2 n \rfloor + 1log2(n+1)\lceil \log_2(n+1) \rceil
    • 对完全二叉树按层序编号(从1开始),对于结点 ii
      • 其父结点为 i/2\lfloor i/2 \rfloori1i \neq 1 时)
      • 左孩子为 2i2i2in2i \leq n 时存在)
      • 右孩子为 2i+12i+12i+1n2i+1 \leq n 时存在)
    • in/2i \leq \lfloor n/2 \rfloor,则结点 ii分支结点;否则是叶结点

💡 完全二叉树推算 n0,n1,n2n_0, n_1, n_2 的技巧

  • 完全二叉树中,度为1的结点 n1n_1 只可能是0或1
  • nn 为奇数:n1=0n_1 = 0n0=(n+1)/2n_0 = (n+1)/2n2=(n1)/2n_2 = (n-1)/2
  • nn 为偶数:n1=1n_1 = 1n0=n/2n_0 = n/2n2=n/21n_2 = n/2 - 1
  • 简记n0=n/2n_0 = \lceil n/2 \rceil(完全二叉树的叶子数始终占总结点数的一半左右)

⚠️ 更快捷的方法(2011年真题):完全二叉树的叶结点数 =nn/2= n - \lfloor n/2 \rfloor

  • :768个结点的完全二叉树,最后一个分支结点序号 = 768/2=384\lfloor 768/2 \rfloor = 384,叶结点数 = 768384=384768 - 384 = 384

💡 mm 树求叶结点数公式:设度为 ii 的结点有 nin_i 个,总结点 n=nin = \sum n_i,边数 =n1=ini= n-1 = \sum i \cdot n_i,可解出 n0n_0

  • (2010真题):度4的树,n1=1,n2=2,n3=3,n4=4n_1=1, n_2=2, n_3=3, n_4=4,边 n1=1+4+9+16=30n-1 = 1+4+9+16=30n=31n=31n0=311234=21n_0=31-1-2-3-4=21。⟹ 利用 n0=1+i=2m(i1)nin_0 = 1 + \sum_{i=2}^{m}(i-1)n_i 快速计算。

三、二叉树的存储结构

顺序存储:用数组按层序编号存储(适合完全二叉树)

链式存储(二叉链表,最常用):

c
typedef struct BiTNode {
    ElemType data;
    struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
java
class BiTNode {
    int data;
    BiTNode lchild, rchild;
    BiTNode(int data) {
        this.data = data;
        this.lchild = null;
        this.rchild = null;
    }
}

⚠️ 含 nn 个结点的二叉链表中有 n+1n + 1 个空链域(可利用这些空链域构成线索二叉树)。

⚠️ 完全二叉树编号陷阱(高频选择题!)

  • 结点 ii左孩子编号为 2i2i右孩子2i+12i+1父结点i/2\lfloor i/2 \rfloor
  • 但上述公式仅适用于编号从1开始的情况!若编号从0开始,则左孩子 2i+12i+1,右孩子 2i+22i+2,父结点 (i1)/2\lfloor (i-1)/2 \rfloor
  • 考试中必须注意题目编号起始是0还是1,否则全盘出错
  • 判断结点 ii 是叶子:2i>n2i > n(编号从1开始时)
  • 完全二叉树中度为1的结点最多1个,且若存在则只有左孩子

🔗 【跨学科联动·OS】 树结构在操作系统中常见应用:

  • 文件系统目录结构:就是一棵树(根目录→子目录→文件),ls -R/tree 命令本质是树的遍历
  • 进程树:Linux 中所有进程构成一棵以 init/systemd 为根的进程树,fork() 创建子进程就是添加子结点
  • 页表:多级页表本质是一棵多叉树(如 x86 的 4 级页表)

5.3 二叉树的遍历

一、先序、中序、后序遍历

遍历方式访问顺序递归过程
先序遍历(Pre-order)根→左→右NLR
中序遍历(In-order)左→根→右LNR
后序遍历(Post-order)左→右→根LRN
c
// 先序遍历
void PreOrder(BiTree T) {
    if (T != NULL) {
        visit(T);            // 访问根结点
        PreOrder(T->lchild); // 递归遍历左子树
        PreOrder(T->rchild); // 递归遍历右子树
    }
}

// 中序遍历
void InOrder(BiTree T) {
    if (T != NULL) {
        InOrder(T->lchild);
        visit(T);
        InOrder(T->rchild);
    }
}

// 后序遍历
void PostOrder(BiTree T) {
    if (T != NULL) {
        PostOrder(T->lchild);
        PostOrder(T->rchild);
        visit(T);
    }
}
java
// 先序遍历
void preOrder(BiTNode T) {
    if (T != null) {
        visit(T);
        preOrder(T.lchild);
        preOrder(T.rchild);
    }
}

// 中序遍历
void inOrder(BiTNode T) {
    if (T != null) {
        inOrder(T.lchild);
        visit(T);
        inOrder(T.rchild);
    }
}

// 后序遍历
void postOrder(BiTNode T) {
    if (T != null) {
        postOrder(T.lchild);
        postOrder(T.rchild);
        visit(T);
    }
}

时间复杂度O(n)O(n)空间复杂度O(h)O(h)hh 为树高,递归栈深度)

二、层序遍历(借助队列)

c
void LevelOrder(BiTree T) {
    InitQueue(Q);
    BiTree p;
    EnQueue(Q, T);           // 根结点入队
    while (!IsEmpty(Q)) {
        DeQueue(Q, p);       // 出队
        visit(p);
        if (p->lchild != NULL)  EnQueue(Q, p->lchild);
        if (p->rchild != NULL)  EnQueue(Q, p->rchild);
    }
}
java
void levelOrder(BiTNode T) {
    Queue<BiTNode> queue = new LinkedList<>();
    queue.offer(T);              // 根结点入队
    while (!queue.isEmpty()) {
        BiTNode p = queue.poll(); // 出队
        visit(p);
        if (p.lchild != null) queue.offer(p.lchild);
        if (p.rchild != null) queue.offer(p.rchild);
    }
}

三、由遍历序列构造二叉树(重要!)

已知哪些序列可以唯一确定一棵二叉树?

组合能否唯一确定
先序 + 中序✅ 能
后序 + 中序✅ 能
层序 + 中序✅ 能
先序 + 后序❌ 不能

💡 核心规律:中序序列是必需的!因为中序序列能帮助确定左右子树的划分。先序/后序/层序确定根结点,中序确定左右子树范围。

⚠️ 先序 + 后序虽不能唯一确定二叉树,但能确定祖先关系:对于两个结点 XXYY,若前序中 XXYY 前且后序中 YYXX 前(即前序 XYXY,后序 YXYX),则 XXYY 的祖先。

  • 2012年真题应用:前序 a,e,b,d,c,后序 b,c,d,e,a。前序 ae/后序 ea,可知 aaee 的祖先;前序 eb/后序 be,可知 eebb 的祖先。由此推出根 aa 的孩子只有 ee
  • 特殊情况:若前序与后序恰好相反(如 1,2,3,44,3,2,1),则每个结点最多只有一个孩子,二叉树高度等于结点数。

构造方法(以"先序+中序"为例):

  1. 先序的第一个元素是根
  2. 在中序中找到根,根左边是左子树,右边是右子树
  3. 递归处理左右子树

5.4 线索二叉树

一、线索化的目的

普通二叉链表中有很多空指针(n+1n+1 个)。线索二叉树利用这些空指针存储前驱/后继信息,加速遍历。

c
typedef struct ThreadNode {
    ElemType data;
    struct ThreadNode *lchild, *rchild;
    int ltag, rtag;  // 0=指向孩子,1=指向线索(前驱/后继)
} ThreadNode, *ThreadTree;
java
class ThreadNode {
    int data;
    ThreadNode lchild, rchild;
    int ltag, rtag;  // 0=指向孩子,1=指向线索(前驱/后继)
    ThreadNode(int data) {
        this.data = data;
        this.lchild = null;
        this.rchild = null;
        this.ltag = 0;
        this.rtag = 0;
    }
}
  • ltag = 0 时,lchild 指向左孩子;当 ltag = 1 时,lchild 指向中序前驱
  • rtag = 0 时,rchild 指向右孩子;当 rtag = 1 时,rchild 指向中序后继

🗣️ 大白话:线索二叉树就是把原来浪费的空指针利用起来,存上"前一个是谁""后一个是谁"的信息,这样遍历时就不需要递归(不需要栈)了。

在线索二叉树中找前驱/后继的方法总结

线索类型找后继找前驱
中序线索rtag=1,后继 = rchild;若 rtag=0,后继 = 右子树最左下结点ltag=1,前驱 = lchild;若 ltag=0,前驱 = 左子树最右下结点
先序线索rtag=1,后继 = rchild;若有左孩子则后继 = 左孩子;否则后继 = 右孩子较复杂,可能需要从根重新遍历或使用三叉链表
后序线索较复杂,可能需要从根重新遍历或使用三叉链表ltag=1,前驱 = lchild;若有右孩子则前驱 = 右孩子;否则前驱 = 左孩子

⚠️ 考研重点:中序线索二叉树的操作最常考。先序线索找前驱、后序线索找后继比较困难,可能需要三叉链表(增加parent指针)。有时考题会问"哪种线索二叉树不能直接找到某类前驱/后继"。

"三次路过"法理解遍历:在二叉树的递归遍历过程中,每个结点会被"路过"三次:

  1. 第一次路过时访问 → 先序遍历
  2. 第二次路过时访问 → 中序遍历
  3. 第三次路过时访问 → 后序遍历

💡 用"三次路过"法可以快速确定任意遍历序列:沿着树的外轮廓画一条线,每个结点被线"路过"三次,按照先/中/后的规则决定何时访问即可。


5.5 树、森林

一、树的存储结构

存储方式说明
双亲表示法每个结点存储其父结点的位置(数组实现),找父亲 O(1)O(1),找孩子 O(n)O(n)
孩子表示法每个结点有一个链表指向所有孩子
孩子兄弟表示法每个结点有两个指针:一个指向第一个孩子,一个指向下一个兄弟

💡 孩子兄弟表示法本质上就是用二叉树来存储树/森林!

二、树、森林与二叉树的转换

树 → 二叉树

  1. 在所有兄弟之间加一条连线
  2. 对每个结点,只保留与第一个孩子的连线,删除与其他孩子的连线
  3. 以根为轴心顺时针旋转 45°

森林 → 二叉树

  1. 先把每棵树转换为二叉树
  2. 把每棵二叉树的根相连(后一棵作为前一棵根的右子树)

⚠️ 树转二叉树后"无右孩子"的结点数(2011年真题核心考点):

在孩子兄弟法中,右指针指向"下一个兄弟"。无右孩子 = 没有下一个兄弟 = 每组兄弟中的最后一个。设树有 nn 个结点、pp 个分支结点(非叶结点),则有 pp 组兄弟(每个分支结点的所有孩子构成一组),每组最后一个没有右兄弟,加上根结点也没有右兄弟,所以:

无右孩子的结点数=p+1=分支结点数+1\text{无右孩子的结点数} = p + 1 = \text{分支结点数} + 1

:2011 年真题——2011个结点、116个叶结点的树,分支结点数 =2011116=1895= 2011 - 116 = 1895,无右孩子结点数 =1895+1=1896= 1895 + 1 = 1896

三、树和森林的遍历

树的遍历对应的二叉树遍历
树的先根遍历对应二叉树的先序遍历
树的后根遍历对应二叉树的中序遍历
森林的遍历对应的二叉树遍历
森林的先序遍历对应二叉树的先序遍历
森林的中序遍历对应二叉树的中序遍历

5.6 哈夫曼树与哈夫曼编码

一、哈夫曼树(最优二叉树)

带权路径长度WPL

WPL=i=1nwi×liWPL = \sum_{i=1}^{n} w_i \times l_i

其中 wiw_i 是第 ii 个叶子结点的权值,lil_i 是该叶子到根的路径长度。

哈夫曼树:在含有 nn 个带权叶子结点的二叉树中,WPL最小的二叉树称为哈夫曼树。

构造方法(贪心策略)

  1. nn 个权值作为 nn 棵只含根结点的树,构成森林 FF
  2. FF 中选取权值最小的两棵树,合并为一棵新树(根的权值为两棵子树之和)
  3. FF 中删除这两棵树,加入新树
  4. 重复2-3,直到只剩一棵树

哈夫曼树的特点

  • 没有度为1的结点(每个内部结点的度都为2)
  • nn 个叶子结点的哈夫曼树共有 2n12n-1 个结点
  • 哈夫曼树不唯一(左右子树可以互换),但WPL唯一
  • 哈夫曼树不一定是完全二叉树(2010年真题考点!)
  • 两个权值最小的结点一定是兄弟结点
  • 任一非叶结点的权值一定不小于下一层任一结点的权值(因为非叶结点权值 = 左右子树权值之和)

二、哈夫曼编码

  • 将字符的使用频率作为权值构建哈夫曼树
  • 左分支标记为0,右分支标记为1
  • 从根到叶子的路径上标记序列即为该字符的编码
  • 哈夫曼编码是前缀编码(任何一个编码都不是其他编码的前缀)

🗣️ 大白话:使用频率高的字符用短编码,频率低的用长编码。这样总的编码长度最短——这就是数据压缩的基本原理。

📝 树的真题高频考点汇编

【WPL计算】 2021年真题:nn 个叶子权值 {10,12,16,21,30}\{10, 12, 16, 21, 30\},最小WPL?

构造哈夫曼树:10+12=2210+12=2216+21=3716+21=3722+30=5222+30=5237+52=8937+52=89?不对。实际最优:WPL=2×(16+21+30)+3×(10+12)=134+66=200WPL = 2\times(16+21+30) + 3\times(10+12) = 134+66 = 200答案200。

💡 WPL 计算也可用"内结点权值之和"法:每个内部结点权值 = 左右子树的权值之和,WPL = 所有内部结点的权值之和。

【哈夫曼编码平均长度】 2023年真题:频次 {3,4,5,6,8,10}\{3,4,5,6,8,10\},加权平均编码长度?

(3+4+5+6)×3+(8+10)×23+4+5+6+8+10=54+3636=2.5\frac{(3+4+5+6)\times 3 + (8+10)\times 2}{3+4+5+6+8+10} = \frac{54+36}{36} = 2.5

kk 叉哈夫曼树(多叉哈夫曼树)】 2013年真题考点:

⚠️ kk 叉哈夫曼树的构造要点

  1. 虚段补充判断:若 (n01)%(k1)0(n_0 - 1) \% (k-1) \neq 0,需补 (k1)(n01)%(k1)(k-1) - (n_0-1)\%(k-1) 个权值为0的虚叶结点
  2. 每次从当前所有结点中选取权值最小的 kk合并
  3. 第一次合并时,若需补虚段,先合并虚段+最小权值结点

:权值 {1,3,5,7,9}\{1, 3, 5, 7, 9\},构造三叉哈夫曼树。n0=5n_0=5(51)%(31)=0(5-1)\%(3-1)=0,无需补虚段。

  • 第1次合并:{1,3,5}\{1, 3, 5\}99',剩余 {7,9,9}\{7, 9, 9'\},WPL贡献 (1+3+5)×(1+3+5)\times 深度
  • 第2次合并:{7,9,9}\{7, 9, 9'\}2525
  • WPL = (1+3+5)×2+(7+9)×1=18+16=34(1+3+5)\times 2 + (7+9)\times 1 = 18 + 16 = 34

【2013年408真题】 在 BST 中删除某结点后再将其插入,是否能恢复原来的 BST?

答案:不一定能恢复。 若被删除的结点有两个子树,删除操作会用中序后继(或前驱)替代该结点,树的结构已经改变。重新插入原结点后,它会被插入为叶结点(BST插入总是在叶结点位置),树的形态与原来不同。

💡 只有被删结点是叶结点仅有一个子树时,删后重新插入才能恢复原树。

【BST的顺序存储验证】 2022年真题·应用题:用数组保存二叉树(data[]),空节点值为-1,判断是否为BST。

方法:中序遍历检查是否升序。数组中第 ii 个结点的左/右孩子分别在 2i+12i+12i+22i+2(下标从0开始)。用递归中序遍历即可实现。

【完全二叉树的数组编号】 考研高频考点:

  • 结点 ii 的父结点:(i1)/2\lfloor (i-1)/2 \rfloor(下标从0开始)或 i/2\lfloor i/2 \rfloor(下标从1开始)
  • 结点 ii 的左孩子:2i+12i+1(从0)或 2i2i(从1)
  • 结点 ii 的右孩子:2i+22i+2(从0)或 2i+12i+1(从1)

【森林中树的棵数的确定】 2021年真题:森林 FF 对应的二叉树 TT,已知 TT 的后序遍历和中序遍历,求 FF 中树的棵数。

关键公式:由二叉树的后序+中序可唯一确定二叉树结构。确定 TT 后,TT 的根的右子树链长度 +1+1 = 森林中树的棵数(根的右链对应森林中各棵树的根)。

【度为 kk 的正则树的叶结点数公式】 2016年真题考点:

设度为 kk 的正则树有 mm 个非叶结点(每个非叶结点的度恰好为 kk),则叶结点数为:

n0=(k1)×m+1n_0 = (k-1) \times m + 1

💡 利用 n=n0+mn = n_0 + mn1=k×mn - 1 = k \times m(总边数),联立即得。

【二叉树先序=中序 → 仅有右子女】 2017年真题考点:若一棵二叉树的先序遍历和中序遍历完全相同,则该树中任何结点都没有左孩子(退化为右斜树)。


5.7 并查集

并查集(Union-Find):一种用于管理元素分组的数据结构,支持两种操作:

  • Find(x):查找元素 xx 属于哪个集合(返回根结点)
  • Union(x, y):将 xxyy 所在的集合合并

用双亲表示法(数组)实现

c
int UFSets[SIZE];  // 初始化:每个元素的父结点为-1(自己是根)

// 查找x所属集合(返回根)
int Find(int UFSets[], int x) {
    while (UFSets[x] >= 0)
        x = UFSets[x];
    return x;
}

// 合并x和y所在的集合
void Union(int UFSets[], int Root1, int Root2) {
    UFSets[Root2] = Root1;  // 将Root2连到Root1下面
}
java
class UnionFind {
    int[] parent;  // 初始化每个元素的父结点为-1(自己是根)

    UnionFind(int size) {
        parent = new int[size];
        Arrays.fill(parent, -1);
    }

    // 查找x所属集合(返回根)
    int find(int x) {
        while (parent[x] >= 0)
            x = parent[x];
        return x;
    }

    // 合并x和y所在的集合
    void union(int root1, int root2) {
        parent[root2] = root1;
    }
}

优化

  1. 按秩合并(Union by Rank):小树合并到大树下,避免退化为链
  2. 路径压缩(Path Compression):查找时将路径上的结点直接连到根下
c
// 路径压缩的Find
int Find(int UFSets[], int x) {
    int root = x;
    while (UFSets[root] >= 0)
        root = UFSets[root];
    while (x != root) {
        int t = UFSets[x];
        UFSets[x] = root;  // 路径上所有结点直接指向根
        x = t;
    }
    return root;
}
java
// 路径压缩的Find
int findWithPathCompression(int x) {
    int root = x;
    while (parent[root] >= 0)
        root = parent[root];
    while (x != root) {
        int t = parent[x];
        parent[x] = root;  // 路径上所有结点直接指向根
        x = t;
    }
    return root;
}

优化后 Find 和 Union 的时间接近 O(1)O(1)(严格来说是 O(α(n))O(\alpha(n)),其中 α\alpha阿克曼函数的反函数,对任何实际可能的 nnα(n)4\alpha(n) \leq 4,可视为常数)。

💡 并查集优化效果对比

实现方式Find 时间Union 时间
无优化O(n)O(n)(最坏,退化为链)O(1)O(1)
仅按秩合并O(logn)O(\log n)O(1)O(1)
仅路径压缩均摊 O(α(n))O(\alpha(n))O(1)O(1)
按秩合并 + 路径压缩均摊 O(α(n))O(\alpha(n))O(1)O(1)

⚠️ 考研注意:并查集中 Union 操作的时间复杂度取决于 Find(需要先找到根才能合并),严格来说 Union 的时间 = 两次 Find 时间 + O(1)O(1)

📝 真题剖析

【2017年408真题】 对于一棵有 nn 个结点、度为4的树,若用孩子兄弟表示法转换为二叉树 TbT_b,则 TbT_b 中无右孩子的结点个数为()。

A. 1   B. n/2n/2C. n/4n/4D. 与树的形态有关

解析

在孩子兄弟表示法中:

  • 左指针指向"第一个孩子"
  • 右指针指向"下一个兄弟"

无右孩子的结点 = 没有"下一个兄弟"的结点。在原树中,每组兄弟中最后一个没有下一个兄弟。

树中有 nn 个结点,边数 = n1n - 1。每条边对应一个"孩子"关系。在兄弟链中,每组兄弟的第一个不通过"兄弟"关系连接,而是通过"孩子"关系。

设树中有 pp 个分支结点(有孩子的结点),那就有 pp 组兄弟。每组兄弟中最后一个结点无右孩子,加上根结点也无右孩子(根没有兄弟),所以无右孩子的结点 = 分支结点数 + 1。

但实际上更直接的分析:在原树中,每个结点要么有下一个兄弟(有右孩子),要么没有。没有下一个兄弟的结点 = 每组兄弟中的最后一个。树中共有 nn 个结点,其中 n1n-1 个结点有"上一个兄弟或是父亲的第一个孩子"关系。每组兄弟的最后一个结点就是"没有下一个兄弟"的。设分支结点数为 pp,则这样的结点有 pp 个(每个分支结点的最后一个孩子)加上根结点 = p+1p + 1。但这与树的具体形态有关吗?

实际上,nn 个结点的树转成的二叉树中,没有右孩子的结点恰好等于原树中每个结点的最后一个孩子的数量 + 根结点 = 分支结点数 + 1。而不同形态的树会有不同的分支结点数。

不过,本题答案为 A. 1 吗?不,仔细看——原题说的是"nn 个结点"的树。

重新分析:无右孩子 = 没有下一个兄弟的结点个数。每个非叶结点有若干孩子,最后一个孩子没有下一个兄弟。所以无右孩子结点数 = 非叶结点数(即分支结点数)+1+ 1(根也没有右兄弟)。但 n0+n1+n2+n3+n4=nn_0 + n_1 + n_2 + n_3 + n_4 = n 且边数 =n1=n1+2n2+3n3+4n4= n-1 = n_1 + 2n_2 + 3n_3 + 4n_4,这与具体形态有关。

答案选A(无右孩子的结点个数 = nn 减去有右兄弟的结点数 = n(n1n - (n-1- 叶子结点中非末尾的))...)

实际本题的正确答案考虑到无右孩子的结点 = 有"最后一个孩子"身份的结点 + 根结点。

本题答案与树的形态有关,答案是A不对。实际上应该是每个节点的最后一个孩子没有右兄弟,因此没有右孩子的节点个数=分支结点个数+1(根也没有右兄弟)。答案为 nn- 非最后孩子节点数。这与具体的树的形态有关。但正确答案应该是 A:1(不对)。让我们重新查看:实际上无右孩子指的是没有下一个兄弟。总结点数 nn,有兄弟关系的边数 = n1n - 1 - 分支结点数(每个分支结点贡献"孩子数-1"条兄弟边)。所以有右孩子的结点 = (di1)=\sum(d_i - 1) = 边数 - 分支结点数 =n1p= n - 1 - p,无右孩子 = n(n1p)=p+1n - (n-1-p) = p + 1。这取决于分支结点数 pp。所以答案选D

📌 第五章总结

核心知识点考研要求
树的定义、基本术语必须牢记
二叉树的性质(n0=n2+1n_0 = n_2 + 1 等)高频考点,每年必考
完全二叉树的性质(父子编号关系)必须掌握
二叉树的先中后序遍历(递归实现)重中之重
层序遍历(借助队列)需要掌握
由遍历序列构造二叉树大题常考
线索二叉树理解概念,掌握构造方法
树、森林与二叉树的转换高频考点
树/森林的遍历与对应二叉树遍历的关系必须掌握
哈夫曼树的构造与WPL计算常考
哈夫曼编码需要掌握
并查集(Find & Union 操作)近年热门考点