#LC590. N叉树的后序遍历

N叉树的后序遍历

你好!我是你的教练。今天我们要攻克的是关于“树”这种非线性数据结构的基础题目——N叉树的后序遍历。在NOI系列竞赛中,树的遍历是所有进阶算法(如树形DP、LCA、树链剖分)的基石。


一、 题目描述 (NOI 竞赛版)

题目名称:N 叉树的后序遍历 (n_ary_tree_postorder) 时间限制:1.0s 内存限制:256MB

【问题描述】 给定一棵 NN 叉树。请你按照后序遍历的顺序返回节点值。 NN 叉树的后序遍历定义为:在访问根节点之前,先依次从左到右对每一棵子树进行后序遍历。

【输入格式】 为了符合 NOI 规范,输入将分为两部分: 第一行包含一个整数 nn,表示节点总数。 接下来的 nn 行,每行描述一个节点的信息:首先是该节点的 vv,接着是一个整数 kk 表示其子节点数量,最后是 kk 个整数,代表其子节点在输入序列中的编号(从 0 开始)。 根节点固定为编号为 0 的节点。

【输出格式】 输出一行,包含 nn 个整数,表示后序遍历的节点值序列,整数之间用空格隔开。

【示例 1】

输入

6
1 3 1 2 3
3 2 4 5
2 0
4 0
5 0
6 0

输出

5,6,3,2,4,1

逐行详细说明:

  1. 6:总共有 6 个节点。
  2. 1 3 1 2 3:编号为 0 的节点,值为 1,有 3 个孩子,编号分别是 1, 2, 3
  3. 3 2 4 5:编号为 1 的节点,值为 3,有 2 个孩子,编号分别是 4, 5
  4. 2 0:编号为 2 的节点,值为 2,有 0 个孩子。
  5. 4 0:编号为 3 的节点,值为 4,有 0 个孩子。
  6. 5 0:编号为 4 的节点,值为 5,有 0 个孩子。
  7. 6 0:编号为 5 的节点,值为 6,有 0 个孩子。

提示:在 NOI 考场上,这种格式非常利于使用邻接表(例如 vector<int> adj[MAXN])直接读入。你可以通过 adj[0].push_back(1); adj[0].push_back(2); ... 快速建图。

【示例 2】

输入

14
1 4 1 2 3 4
2 0
3 2 5 6
4 1 7
5 2 8 9
6 0
7 1 10
8 1 11
9 1 12
10 0
11 1 13
12 0
13 0
14 0

输出

2,6,14,11,7,3,12,8,4,13,9,10,5,1

数据逐行解析

  1. 14:总节点数为 14。
  2. 1 4 1 2 3 4:编号 0,值 1,有 4 个孩子,编号分别是 1, 2, 3, 4。
  3. 2 0:编号 1,值 2,无孩子。
  4. 3 2 5 6:编号 2,值 3,有 2 个孩子,编号分别是 5, 6。
  5. 4 1 7:编号 3,值 4,有 1 个孩子,编号是 7。
  6. 5 2 8 9:编号 4,值 5,有 2 个孩子,编号分别是 8, 9。
  7. 6 0:编号 5,值 6,无孩子。
  8. 7 1 10:编号 6,值 7,有 1 个孩子,编号是 10。
  9. 8 1 11:编号 7,值 8,有 1 个孩子,编号是 11。
  10. 9 1 12:编号 8,值 9,有 1 个孩子,编号是 12。
  11. 10 0:编号 9,值 10,无孩子。
  12. 11 1 13:编号 10,值 11,有 1 个孩子,编号是 13。
  13. 12 0:编号 11,值 12,无孩子。
  14. 13 0:编号 12,值 13,无孩子。
  15. 14 0:编号 13,值 14,无孩子。

【数据范围】

  • 0n1040 \le n \le 10^4
  • 树的高度 H1000H \le 1000
  • 每个节点的值在 [0,106][0, 10^6] 之间

二、 预备知识点

  1. DFS (深度优先搜索):后序遍历是 DFS 的一种变形。
  2. 树的存储结构:理解如何使用 vector<int> adj[N] 或邻接表存储 NN 叉树。
  3. 栈 (Stack):用于将递归转化为迭代,防止在极端极端退化树(链状)中爆系统栈。
  4. 序列反转 (Reverse):掌握迭代法中“根-右-左”转“左-右-根”的技巧。

三、 启发式教学:草稿纸上的推理过程

请拿出草稿纸,我们以 示例 1 为例进行推理:

1. 图形化拆解

根据 [1,null,3,2,4,null,5,6],我们在纸上画出这棵树:

      (1)
    /  |  \
  (3) (2) (4)
  / \
(5) (6)

2. 模拟后序遍历逻辑

后序遍历的核心思想是:“扫完孩子再扫自己”

  • Step 1: 访问节点 1,发现它有孩子 3, 2, 4。按顺序先去处理 3。
  • Step 2: 访问节点 3,发现它有孩子 5, 6。按顺序先去处理 5。
  • Step 3: 节点 5 没有孩子,此时可以“收割”它。记录: 5
  • Step 4: 返回 3,处理下一个孩子 6。节点 6 没孩子。记录: 6
  • Step 5: 节点 3 的孩子都处理完了,现在可以收割 3。记录: 3
  • Step 6: 返回 1,处理下一个孩子 2。节点 2 没孩子。记录: 2
  • ...以此类推。

3. 复杂度分析思考

  • 时间复杂度:每个节点必须且只能被访问一次,每个边也只被走过一次。所以是 O(N)O(N)
  • 空间复杂度
    • 递归法:消耗系统栈,深度等于树高 HH
    • 迭代法:手动维护一个栈。
    • 优化建议:对于 N=104N=10^4,递归足够安全;若 N>105N > 10^5,建议改用迭代。

四、 算法思路提示 (伪代码流程图)

在 NOI 考试中,你可以选择递归或迭代。迭代法在处理极深树时更稳健。

方案 A:递归 DFS 流程

graph TD
    A(开始 DFS_node) --> B{node 是否为空}
    B -- 是 --> C(返回)
    B -- 否 --> D(循环遍历 node 的子节点集合)
    D --> E(递归调用 DFS_child)
    E --> D
    D -- 所有子节点遍历完 --> F(将 node 的值存入结果数组)
    F --> G(结束)

方案 B:迭代法 (逆向思维)

提示:后序遍历是“左-右-根”,它的翻转是“根-右-左”。

graph TD
    Start(开始) --> Init(初始化一个栈 S 和 结果数组 Res)
    Init --> PushRoot(将根节点压入 S)
    PushRoot --> While{S 是否为空}
    While -- 否 --> Pop(弹出栈顶节点 curr)
    Pop --> Save(将 curr 的值加入 Res)
    Save --> ChildLoop(将 curr 的所有子节点从左到右压入 S)
    ChildLoop --> While
    While -- 是 --> Reverse(翻转 Res 数组)
    Reverse --> End(输出 Res)

五、 关键读题技巧:哪里是解题钥匙?

  1. 关键词:后序 (Postorder)
    • 看到这个词,你要立刻反应出:“记录值的操作必须在所有递归调用之后”
  2. 关键词:N 叉 (N-ary)
    • 这意味着节点的孩子不是 left/right,而是一个容器。在 C++ STL 中,通常用 std::vector 存储。
  3. 理解示例中的 null
    • LeetCode 的这种 null 并不是节点的值,而是层序遍历中的分层符。在 NOI 的输入格式中,通常会直接告诉你每个节点有多少个孩子(即 kk 值),这比 LeetCode 的格式更易处理。
  4. 注意顺序
    • 题目要求“从左到右”对子树进行遍历。在迭代法压栈时,如果你想先处理左边,那么在反转逻辑中,你需要先压左边的孩子(因为栈是后进先出)。

希望这些启发能帮你写出完美的满分代码!尝试用 C++14 实现一下吧,记得注意 N=0N=0 的特判。