#LC590. N叉树的后序遍历
N叉树的后序遍历
你好!我是你的教练。今天我们要攻克的是关于“树”这种非线性数据结构的基础题目——N叉树的后序遍历。在NOI系列竞赛中,树的遍历是所有进阶算法(如树形DP、LCA、树链剖分)的基石。
一、 题目描述 (NOI 竞赛版)
题目名称:N 叉树的后序遍历 (n_ary_tree_postorder) 时间限制:1.0s 内存限制:256MB
【问题描述】 给定一棵 叉树。请你按照后序遍历的顺序返回节点值。 叉树的后序遍历定义为:在访问根节点之前,先依次从左到右对每一棵子树进行后序遍历。
【输入格式】 为了符合 NOI 规范,输入将分为两部分: 第一行包含一个整数 ,表示节点总数。 接下来的 行,每行描述一个节点的信息:首先是该节点的值 ,接着是一个整数 表示其子节点数量,最后是 个整数,代表其子节点在输入序列中的编号(从 0 开始)。 根节点固定为编号为 0 的节点。
【输出格式】 输出一行,包含 个整数,表示后序遍历的节点值序列,整数之间用空格隔开。
【示例 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
逐行详细说明:
6:总共有 6 个节点。1 3 1 2 3:编号为 0 的节点,值为1,有3个孩子,编号分别是1, 2, 3。3 2 4 5:编号为 1 的节点,值为3,有2个孩子,编号分别是4, 5。2 0:编号为 2 的节点,值为2,有0个孩子。4 0:编号为 3 的节点,值为4,有0个孩子。5 0:编号为 4 的节点,值为5,有0个孩子。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
数据逐行解析
14:总节点数为 14。1 4 1 2 3 4:编号 0,值 1,有 4 个孩子,编号分别是 1, 2, 3, 4。2 0:编号 1,值 2,无孩子。3 2 5 6:编号 2,值 3,有 2 个孩子,编号分别是 5, 6。4 1 7:编号 3,值 4,有 1 个孩子,编号是 7。5 2 8 9:编号 4,值 5,有 2 个孩子,编号分别是 8, 9。6 0:编号 5,值 6,无孩子。7 1 10:编号 6,值 7,有 1 个孩子,编号是 10。8 1 11:编号 7,值 8,有 1 个孩子,编号是 11。9 1 12:编号 8,值 9,有 1 个孩子,编号是 12。10 0:编号 9,值 10,无孩子。11 1 13:编号 10,值 11,有 1 个孩子,编号是 13。12 0:编号 11,值 12,无孩子。13 0:编号 12,值 13,无孩子。14 0:编号 13,值 14,无孩子。

【数据范围】
- 树的高度
- 每个节点的值在 之间
二、 预备知识点
- DFS (深度优先搜索):后序遍历是 DFS 的一种变形。
- 树的存储结构:理解如何使用
vector<int> adj[N]或邻接表存储 叉树。 - 栈 (Stack):用于将递归转化为迭代,防止在极端极端退化树(链状)中爆系统栈。
- 序列反转 (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. 复杂度分析思考
- 时间复杂度:每个节点必须且只能被访问一次,每个边也只被走过一次。所以是 。
- 空间复杂度:
- 递归法:消耗系统栈,深度等于树高 。
- 迭代法:手动维护一个栈。
- 优化建议:对于 ,递归足够安全;若 ,建议改用迭代。
四、 算法思路提示 (伪代码流程图)
在 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)
五、 关键读题技巧:哪里是解题钥匙?
- 关键词:后序 (Postorder)
- 看到这个词,你要立刻反应出:“记录值的操作必须在所有递归调用之后”。
- 关键词:N 叉 (N-ary)
- 这意味着节点的孩子不是
left/right,而是一个容器。在 C++ STL 中,通常用std::vector存储。
- 这意味着节点的孩子不是
- 理解示例中的 null:
- LeetCode 的这种
null并不是节点的值,而是层序遍历中的分层符。在 NOI 的输入格式中,通常会直接告诉你每个节点有多少个孩子(即 值),这比 LeetCode 的格式更易处理。
- LeetCode 的这种
- 注意顺序:
- 题目要求“从左到右”对子树进行遍历。在迭代法压栈时,如果你想先处理左边,那么在反转逻辑中,你需要先压左边的孩子(因为栈是后进先出)。
希望这些启发能帮你写出完美的满分代码!尝试用 C++14 实现一下吧,记得注意 的特判。