N 叉树的后序遍历 图解题解
这道题到底在问什么
- 输入
- root = [1,null,3,2,4,null,5,6]
- 输出
- [5,6,3,2,4,1]
最优解:为什么这么做
一句话答案:LeetCode 590 N 叉树的后序遍历:每个节点先把孩子从左到右递归走完,最后才收自己,一趟递归 DFS 就能排出整条后序序列,时间 O(n)、空间 O(h)。
后序遍历要返回一串什么样的顺序
给一棵 N 叉树的根节点 root,把所有节点的值按后序排成一个列表返回。N 叉树就是每个节点的孩子不止两个,而是存在一个孩子列表里,从左到右排好。题面示例 root=[1,null,3,2,4,null,5,6],画出来是根 1 底下挂着 3、2、4 三个孩子,3 又带着 5、6,最后要返回 [5,6,3,2,4,1]。
先收根、再走孩子,序列会歪成什么样
不少人第一笔就把根 1 写进结果,再去处理孩子——这样收出来的是 [1,3,5,6,2,4],根跑到了最前头,正是前序而不是后序。后序的定义卡得很死:一个节点的值,必须等它名下所有孩子、连同孩子的孩子都进了结果,才轮得到它自己。收根的时机差一步,整串顺序就全歪了。
孩子全收完,才轮到收自己
把这条规矩摊开来看:站在任意一个节点上,先别急着收自己,而是照孩子列表从左到右,一个个钻下去递归。等某个孩子是叶子、底下再没有孩子了,它就能直接进结果;要等这个节点名下的孩子全部落进结果,才排得上收它自己的值。二叉树的后序是先左右两个孩子再收自己,N 叉树只是把『两个孩子』换成『一整个孩子列表』,规矩一模一样。
一个 dfs 在每个节点做的三件事
写成递归就是一个 dfs(node):先判空,node 为空直接返回,空树和走到底都靠它兜住;再用 for 循环,按顺序对 node.children 里的每个孩子递归调用 dfs;等这个 for 循环整个跑完、所有孩子都收完了,才把 node.val 追加进答案数组 ans。这三步的先后不能乱——append 自己这一步,永远排在 for 循环之后。
拿 [1,null,3,2,4,null,5,6] 从头收到尾
从根 1 进 dfs。1 的孩子是 3、2、4,先钻第一个孩子 3。3 的孩子是 5、6,又先钻 5:5 没有孩子,直接进结果,ans=[5];回到 3 收第二个孩子 6,6 也没孩子,ans=[5,6];3 的孩子收完,收 3 自己,ans=[5,6,3]。回到 1 的第二个孩子 2,没孩子,ans=[5,6,3,2];第三个孩子 4,没孩子,ans=[5,6,3,2,4];1 的三个孩子全收完,最后收根 1,ans=[5,6,3,2,4,1],和题面输出一致。
先把根收了,后序当场变前序
最爱出岔子的是收根的时机:把 append 自己写在 for 循环前头,根提前入了列,后序当场退化成前序。孩子顺序也不能乱,从右往左递归会让同层孩子的相对次序整个颠倒。还有空节点,忘了判空、遇到空树就直接越界报错。复杂度这边倒是干净:每个节点只被处理一回,时间 O(n);额外开销是递归栈,深度等于当前走到的层数,最多就是树高 h,空间 O(h),最坏树退化成一条链时 h=n。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3记住「先遍历孩子列表、最后收根」这一条,下面每一帧都在套它。
- 4后序序列还是空的开局后序序列为空。DFS 从根节点 50 出发,口诀是先把孩子走完再收自己,我们一路往下探。
- 5先处理 50 的孩子走到节点 50(紫色)。它的孩子列表是 30、80,按后序规矩,要先把这些孩子从左到右一个个递归走完,50 自己留到最后再收。
- 6先处理 30 的孩子走到节点 30(紫色)。它的孩子列表是 10、20,按后序规矩,要先把这些孩子从左到右一个个递归走完,30 自己留到最后再收。
- 7先处理 10 的孩子走到节点 10(紫色)。它的孩子列表是 15、25,按后序规矩,要先把这些孩子从左到右一个个递归走完,10 自己留到最后再收。
- 815 没有孩子走到节点 15(紫色)。它的孩子列表是空的,是个叶子,没有可往下走的孩子,马上就能收它。
- 915 入列,后序 +1叶子 15 没有孩子要等,直接把它追加进后序序列(变绿),目前序列是 15。
- 1025 没有孩子走到节点 25(紫色)。它的孩子列表是空的,是个叶子,没有可往下走的孩子,马上就能收它。
- 1125 入列,后序 +1叶子 25 没有孩子要等,直接把它追加进后序序列(变绿),目前序列是 15 25。
- 1210 入列,后序 +110 的孩子 15、25 都已经收完了,现在轮到 10 自己。把 10 追加进后序序列(变绿),目前序列是 15 25 10。
- 1320 没有孩子走到节点 20(紫色)。它的孩子列表是空的,是个叶子,没有可往下走的孩子,马上就能收它。
- 1420 入列,后序 +1叶子 20 没有孩子要等,直接把它追加进后序序列(变绿),目前序列是 15 25 10 20。
- 1530 入列,后序 +130 的孩子 10、20 都已经收完了,现在轮到 30 自己。把 30 追加进后序序列(变绿),目前序列是 15 25 10 20 30。
- 16先处理 80 的孩子走到节点 80(紫色)。它的孩子列表是 70、90,按后序规矩,要先把这些孩子从左到右一个个递归走完,80 自己留到最后再收。
- 17先处理 70 的孩子走到节点 70(紫色)。它的孩子列表是 75、78,按后序规矩,要先把这些孩子从左到右一个个递归走完,70 自己留到最后再收。
- 1875 没有孩子走到节点 75(紫色)。它的孩子列表是空的,是个叶子,没有可往下走的孩子,马上就能收它。
- 1975 入列,后序 +1叶子 75 没有孩子要等,直接把它追加进后序序列(变绿),目前序列是 15 25 10 20 30 75。
- 2078 没有孩子走到节点 78(紫色)。它的孩子列表是空的,是个叶子,没有可往下走的孩子,马上就能收它。
- 2178 入列,后序 +1叶子 78 没有孩子要等,直接把它追加进后序序列(变绿),目前序列是 15 25 10 20 30 75 78。
- 2270 入列,后序 +170 的孩子 75、78 都已经收完了,现在轮到 70 自己。把 70 追加进后序序列(变绿),目前序列是 15 25 10 20 30 75 78 70。
- 2390 没有孩子走到节点 90(紫色)。它的孩子列表是空的,是个叶子,没有可往下走的孩子,马上就能收它。
- 2490 入列,后序 +1叶子 90 没有孩子要等,直接把它追加进后序序列(变绿),目前序列是 15 25 10 20 30 75 78 70 90。
- 2580 入列,后序 +180 的孩子 70、90 都已经收完了,现在轮到 80 自己。把 80 追加进后序序列(变绿),目前序列是 15 25 10 20 30 75 78 70 90 80。
- 2650 入列,后序 +150 的孩子 30、80 都已经收完了,现在轮到 50 自己。把 50 追加进后序序列(变绿),目前序列是 15 25 10 20 30 75 78 70 90 80 50。
- 27答案 = [15, 25, 10, 20, 30, 75, 78, 70, 90, 80, 50]整棵树走完,所有节点都收进了后序序列:15 25 10 20 30 75 78 70 90 80 50。可以看到每个节点都在自己的孩子全部收完之后才入列,根 50 排在最后,这就是后序的特征。
⚠️ 容易写错的地方
✗ 错:把当前节点先收了再走孩子
✓ 对:后序必须孩子全收完才收自己
先收自己就变成了前序,顺序整个错
✗ 错:孩子顺序写反(从右到左)
✓ 对:按列表从左到右递归
后序里同层孩子的相对顺序由从左到右决定
✗ 错:忘了判空 / 空树没处理
✓ 对:node 为空直接返回
空树应返回空列表,不判空会越界或报错
完整代码(Python / C++ / Java)
Python
from typing import List
class Node:
def __init__(self, val=None, children=None):
self.val = val
self.children = children if children is not None else []
class Solution:
def postorder(self, root: 'Node') -> List[int]:
ans = []
def dfs(node):
if not node:
return
for child in node.children:
dfs(child)
ans.append(node.val)
dfs(root)
return ansC++
#include <algorithm>
#include <functional>
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) { val = _val; }
Node(int _val, vector<Node*> _children) { val = _val; children = _children; }
};
class Solution {
public:
vector<int> postorder(Node* root) {
vector<int> ans;
function<void(Node*)> dfs = [&](Node* node) {
if (!node) return;
for (Node* child : node->children) dfs(child);
ans.push_back(node->val);
};
dfs(root);
return ans;
}
};Java
import java.util.*;
class Node {
public int val;
public List<Node> children;
public Node() { children = new ArrayList<>(); }
public Node(int _val) { val = _val; children = new ArrayList<>(); }
public Node(int _val, List<Node> _children) { val = _val; children = _children; }
}
class Solution {
public List<Integer> postorder(Node root) {
ArrayList<Integer> ans = new ArrayList<>();
dfs(root, ans);
return ans;
}
private void dfs(Node node, List<Integer> ans) {
if (node == null) return;
for (Node child : node.children) dfs(child, ans);
ans.add(node.val);
}
}复杂度
时间
O(n)
每个节点恰好被访问一次:进入一次、收一次,n 个节点就是 O(n)
空间
O(h)
递归栈深度等于树高 h;最坏退化成一条链时 h = n,即 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 N 叉树的后序遍历 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
进阶要求用迭代法,怎么做?+
一个常用技巧是『改良前序再反转』。用一个栈,每次弹出一个节点就把它的值记进结果,同时把它的孩子按从左到右压栈——这样栈顶先出的反而是最右边的孩子。这么走出来的是『根在最前』的逆后序,最后把整个结果数组反转一次,就得到正确的后序。另一种更直白的写法是显式用栈模拟递归,额外记录每个节点的孩子处理到第几个了。
为什么递归空间是 O(h) 而不是 O(n)?+
递归栈在任何一个时刻,只保存『从根到当前这个节点』这一条路径上的调用帧,深度就等于当前节点所在的层数,最多不过是整棵树的高度 h。只有当树退化成一条链、每个节点都只挂着一个孩子时,h 才等于节点总数 n。所以平时记 O(h),最坏情形才写成 O(n)。
N 叉树的后序和二叉树的后序有什么区别?+
本质完全一样:都是孩子全部收完,才轮到收自己。区别只在孩子的数量——二叉树后序是先左孩子、再右孩子、最后自己,孩子写死是两个;N 叉树把这两个孩子换成一个长度不定的孩子列表,用 for 循环挨个递归就行。理解了其中一个,另一个照搬。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 N 叉树的后序遍历 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。