尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

二叉树后序遍历全解析:从递归到迭代,串联深度、BST与线索化

发布时间:2026/9/25 19:43:25

资讯中心
01
ARTICLE

二叉树后序遍历全解析:从递归到迭代,串联深度、BST与线索化

二叉树后序遍历全解析:从递归到迭代,串联深度、BST与线索化
之前给自己定的刷题计划走到第 14 天这一题是二叉树后序遍历。原以为遍历这种题十分钟就能拿下结果被一个运行时错误绊住调试完反而把递归、迭代、线索化这些知识点全部串起来了。如果你也经常在写二叉树程序时报“RecursionError”或者看到“NoneType object has no attribute left”这种报错一头雾水又或者刚准备开始刷树相关的题这篇文章应该能帮你少踩几个坑。我会从“为什么后序不好写”讲起把递归和迭代各自的实现方法拆开再顺手把大家常搜的二叉树深度、搜索二叉树、线索二叉树、顺序存储、判断满二叉树这些知识点一起串起来当作一整块复习。这篇文章不会停留在“背下三行代码”而是尽量告诉你每步背后的原因。1. 为什么后序遍历是三种遍历里最反直觉的1.1 三种遍历的访问时机差异很多教程一开始就给你三句话“前序根左右中序左根右后序左右根。”背起来容易落到代码里就分不清了。其实三者真正的区别是根节点的输出时机而不是节点“谁先被走到”。一个二叉树节点在遍历过程中会被“看到”三次第一次是刚走到它的时候第二次是从左子树回来的时候第三次是从右子树回来的时候。前序遍历在第一次看到节点时就输出所以是“根左右”中序遍历在从左子树回来、还没去右子树的时候输出所以是“左根右”后序遍历在从右子树回来、准备返回给父节点的时候才输出所以是“左右根”。后序遍历最难理解的地方就在这里前序和中序本质上都是“中途做一件事”而后序是“所有子问题都处理完再做正事”。新手很容易把三个函数调用顺序写错最常见的就是把res.append(node.val)放在两个递归之前结果输出变成了前序还看不出哪里不对。1.2 为什么后序的递归顺序不能靠硬背如果你试图靠背口诀来写代码很快就会遇到一个问题前序代码长这样def dfs(node): if not node: return print(node.val) dfs(node.left) dfs(node.right)后序代码长这样def dfs(node): if not node: return dfs(node.left) dfs(node.right) print(node.val)看起来只是把print的位置挪到了最后但递归调用的执行过程完全不同。前序里打印发生在递归进入左右子树之前所以一棵树有多少个节点就相当于先访问根再一层层往下钻后序里打印发生在左右子树都返回之后所以要等左子树全部输出完、右子树全部输出完当前节点才能输出。这种差异在只有两层的小树上还不明显一旦树变成三层以上人脑就很难直接“看到”输出顺序必须借助栈去模拟。这也是“二叉树前中后序遍历的常见问题”里大家问得最多的点为什么递归看懂了让我写却写得不对。答案很简单因为你没有真正理解“递归返回”这个动作。1.3 后序到底适合解决什么问题正因为要先把左右子树都跑完才处理根节点后序特别适合“由下往上归并信息”的问题。比如计算二叉树的最大深度必须知道左子树深度和右子树深度才能算出当前节点深度比如删除整棵树必须先递归删除孩子节点再删除当前节点防止孩子指针悬空再比如判断满二叉树也要先确认左右孩子都存在才轮到判断爷爷辈。这些场景全部是后序思维所以后序遍历不只是面试题它本身就是很多树形算法的基础框架。理解了这一点之后再去看那些零散的二叉树题你会发现它们很多时候共用同一套后序递归骨架。2. 递归版本后序遍历的标准答案与易错点2.1 最小可用实现用最常规的递归写法大概是这个样子class TreeNode: def __init__(self, val0, leftNone, rightNone): self.val val self.left left self.right right def postorder_traversal(root: TreeNode) - list[int]: result [] def dfs(node): if node is None: return dfs(node.left) dfs(node.right) result.append(node.val) dfs(root) return result这里最关键的是dfs(node.left)、dfs(node.right)、result.append(node.val)这三行的顺序。前两行负责把左右子树彻底跑完第三行才会把当前节点输出。如果题目要求返回列表就用一个外部列表收集如果题目只要求打印直接把print(node.val)放到两个递归之后也可以。2.2 手动展开一棵小树的递归过程为了不让“递归就是自己调用自己”变成一句空话我们可以手动展开一棵小树。假设树的结构是1 / \ 2 3后序输出应该是[2, 3, 1]。递归过程是这样的进入dfs(1)不为空继续。调用dfs(1.left)也就是dfs(2)。在dfs(2)里先调用dfs(2.left)是空节点直接返回。再调用dfs(2.right)也是空节点直接返回。最后打印2dfs(2)返回。回到dfs(1)继续调用dfs(1.right)也就是dfs(3)。在dfs(3)里左空、右空最后打印3返回。回到dfs(1)最后打印1。整个过程里节点 1 一直等到左右子树都结束后才输出。这就是“后序”这个名称的来源根节点排在最后。2.3 为什么很多人栽在“返回值”上我见过不少同学在实现后序时写了一个类似这样的函数def dfs(node): if node is None: return [] return dfs(node.left) dfs(node.right) [node.val]这个写法其实也能通过但结果列表会在每一层被合并回传空间开销会更大。而且如果递归函数没有按照预期返回列表马上会出现TypeError。比如有人为了省事把出口写成return而不是return []那么在递归回溯时dfs(node.left)拿到的就是None后续拼接立刻报错。还有一种更隐蔽的错误把result定义在递归函数内部但每次递归调用都重新初始化result []最后返回的永远是空列表。这种问题不会报错但结果是错的排查起来更费时间。我的建议是把所有对外部结果的修改集中在一处用嵌套函数闭包引入代码可读性最高也不容易写错。2.4 复杂度不是背出来的后序遍历每个节点都会被访问一次时间肯定是 O(n)。空间上递归栈的最大深度等于树的高度对平衡二叉树来说是 O(log n)对退化成一棵单链的树来说是 O(n)。这个 O(n) 的空间是递归调用本身消耗的不是因为存了结果列表。很多人只盯着结果列表的 O(n)忘了递归栈也有空间到后面学迭代法时才会突然明白迭代法省掉的其实是递归栈。3. 写二叉树程序时为什么总是报运行时错误3.1 运行时错误的几个典型长相刷题平台里常见的运行时错误不只是程序崩溃还包括无限循环导致超时、答案顺序错乱等。我整理了一张对照表报错类型常见原因典型场景RecursionError递归没有正确收敛树太深或递归出口写错AttributeError: NoneType object has no attribute left访问了空节点的子节点没判断node is None就去取left/rightTypeError返回值类型不一致递归里时而返回列表时而返回None输出顺序变成了前序顺序写错但逻辑完全合法递归里append位置放到了两个递归前迭代版死循环或超时栈没有正确弹出迭代遍历时把节点重复压入栈这些错误在二叉树题里极其高频因为树的递归天然依赖“空节点”这个出口一旦某个节点是None你还在访问它的属性必然崩。3.2 一套有效的排查链路遇到运行时错误不要急着打日志。先把树的规模缩小到最小可复现用例。比如报AttributeError就先构造一棵只有根节点的树然后手动追踪代码走到哪里会访问None。举个实际例子如果我把递归入口写成def dfs(node): if node is None: return dfs(node.left) dfs(node.right) print(node.val)这段代码在空树上不会有问题因为函数入口已经处理了None。但如果你把dfs(node.left)写成dfs(node.left.left)或者没有判空直接进入递归空树立刻就会报NoneType。所以第一板斧永远是明确处理空节点。第一板斧是判空第二板斧是打印递归路径。在递归函数里插入一行print(fvisit {node.val})用小树跑一遍能很清楚地看到顺序到底是前序还是后序。第三板斧是准备几棵固定的测试树空树、单节点、两层满树、三层左斜树。每次提交之前先在本地环境把这几棵固定树跑一遍别直接丢给在线判题系统。很多“二叉树程序总是报运行时错误”的问题其实并不是平台怪而是你少测了边界输入。3.3 递归深度限制的坑很多人不知道Python 默认递归深度大约是 1000而在线评测系统输入的树可能是上千节点的链式结构。这时候递归后序遍历会直接触发RecursionError但这不是你的逻辑错是语言本身的限制。解决办法有两个一是把递归改成迭代二是如果你确实想保留递归写法可以对单链树单独处理。不过在“每日一题”这类场景里我更推荐直接把迭代版练熟因为树特别大的时候递归版很可能在实际工程里也不适用。3.4 区分“运行时错误”和“答案错误”这里还想多说一句很多人在平台上看到红色判题结果统一管它叫“报错”但你是“运行时错误”还是“答案错误”排查方向完全不一样。运行时错误说明程序没有跑完或者中途崩了重点查空指针、递归深度、数组越界答案错误说明代码能跑但输出和预期不一致重点查遍历顺序、列表收集逻辑。我见过有人把“答案错误”当成运行时错误花一个小时去查空指针最后发现只是append放错了位置。判题信息里的英文单词还是值得看一眼的Runtime Error、Compile Error、Wrong Answer是完全不同的赛道。4. 迭代实现不用递归也能输出“左右根”4.1 一个栈加 lastVisited 的经典写法后序的迭代没有前序、中序那么直接因为当你从栈里看到一个节点时你无法立刻决定是否应该打印它如果左子树刚访问完还需要继续访问右子树如果右子树也访问完了才可以打印并返回。所以引入一个last_visited变量记录最近一次被打印的节点def postorder_traversal_iter(root: TreeNode) - list[int]: result [] stack [] cur root last_visited None while stack or cur: while cur: stack.append(cur) cur cur.left peek stack[-1] if peek.right is None or peek.right is last_visited: result.append(peek.val) last_visited stack.pop() else: cur peek.right return result这个版本用last_visited判断“是不是刚从右子树回来”。如果满足条件说明右子树为空或者右子树的根刚好是被打印过的节点此时左右子树都处理完了栈顶节点可以输出了否则就跳到右子树继续。关键点在于每次访问完右子树后会再次回到父节点此时如果不用last_visited记录程序一定会重复进入右子树造成死循环。这也是“程序没报错但就是跑不完”的典型原因在判题系统里会表现为超时。4.2 双栈加反转的取巧版理论上所有递归都能用栈模拟但还有一种更取巧的思路前序遍历是“根左右”而我们要的“左右根”正好是“根右左”的反转。于是可以先做一次“根右左”的遍历再把结果反转。def postorder_traversal_two_stacks(root: TreeNode) - list[int]: if root is None: return [] stack1 [root] stack2 [] while stack1: node stack1.pop() stack2.append(node.val) if node.left: stack1.append(node.left) if node.right: stack1.append(node.right) return stack2[::-1]为什么这里要先压左再压右因为栈是后进先出先压左再压右弹出顺序就是先右后左得到的序列是“根、右、左”反转之后刚好是“左、右、根”。这个版本代码最短也比较容易背。缺点是要额外开一个结果数组做反转空间是 O(n)刷题时完全够用。4.3 两种迭代法的取舍角度单栈 lastVisited双栈反转空间消耗一个栈两个栈/一个结果数组思路难度偏难偏简单对“后序本质”的把握更强更取巧面试喜好加分不易踩坑如果面试官先问递归再追问迭代建议说清楚单栈版本的“右子树正在被访问”这个状态管理问题。如果你只记得双栈解法也可以接受但需要能解释清楚“为什么反转前序的右左版本就能得到后序”。我个人建议把last_visited版本理解透因为它不依赖反转更能体现后序遍历的“由下往上归并”本质。将来你写表达式求值、求二叉树高度这类代码时思路可以直接迁移。5. 后序遍历能串起的周边知识深度、BST、线索化与顺序存储5.1 二叉树深度典型的后序思想很多树题看起来和后序遍历无关但实现上根本离不开它。求二叉树最大深度就是一个例子def max_depth(root: TreeNode) - int: if root is None: return 0 left_depth max_depth(root.left) right_depth max_depth(root.right) return max(left_depth, right_depth) 1两个递归调用先分别算出左右子树深度最后再加一这就是非常标准的后序结构。所以你要是能把后序遍历的思想吃透“二叉树的深度”这类题就只剩套公式了。5.2 判断搜索二叉树中序检查递增后序检查区间关于“搜索二叉树”网上问得最多的问题是“如何判断一棵树是不是 BST”。最常见的做法是中序遍历看结果是否严格递增因为 BST 的中序遍历天然是有序的。后序也可以判断在后序递归里左子树的最大值必须小于根节点右子树的最小值必须大于根节点。可以给递归函数传入一个允许区间(min_val, max_val)然后递归校验。如果只是刷题我建议先用中序递增来判 BST代码更短不容易错。后序方案更适合需要同时合并多个子问题的场景比如“给一棵树判断它是不是每个节点的左右子树都满足大小关系并且节点数量相同”这类变体合并子结果时需要同时返回“当前子树最小/最大值”和“是否满足条件”。5.3 线索二叉树让遍历不再依赖递归和栈线索二叉树是每年校招都喜欢问的一个进阶概念。它的核心是把二叉树的空指针利用起来左空指针指向前驱右空指针指向后继这样在遍历时有部分节点不需要递归或栈就能直接走下一步。中序线索二叉树最经典因为中序节点的前驱和后继比较明朗。后序线索二叉树相对复杂因为要找后序节点的后继通常需要知道父节点而普通二叉树节点不保存父指针单靠线索不一定够。应对“线索二叉树”这个热搜词我的建议是先彻底搞懂中序线索能说出线索化的时间复杂度是 O(n)再知道后序线索化为什么难就可以了。如果面试问到实现不要硬背代码先画出树把每个空指针标成前驱或后继代码自然能推出来。5.4 顺序存储下的二叉树遍历二叉树顺序存储的核心是用数组保存树假设根节点在index0那么任意索引i的节点左孩子是2*i1右孩子是2*i2父节点是(i-1)//2。这个规律只对完全二叉树特别友好如果是稀疏树数组里会有大量空位所以一般会用null或None做占位。顺序存储的后序遍历逻辑和链式存储一样只是用索引替代指针。可以这样实现def postorder_from_array(arr, i0): if i len(arr) or arr[i] is None: return [] result [] result postorder_from_array(arr, 2 * i 1) result postorder_from_array(arr, 2 * i 2) result.append(arr[i]) return result这个写法能帮助你理解树的结构信息其实完全可以靠父子下标关系承载。这也是为什么很多题目里给一个带null的层序数组就能重建二叉树顺序存储和链式存储之间是可以互相转换的。5.5 判断满二叉树后序是天然时机满二叉树是指每一个非叶节点都有左右两个孩子简单说就是“没有节点只有一个孩子”。判断思路很简单遍历时如果发现某个节点有且仅有一个孩子那它就不是满二叉树。写成后序结构的好处是你可以先判断左右子树是否各自满足“满”的条件再回来检查当前节点。实现里可以给递归函数返回一个(is_full, child_count)之类的元组避免重复遍历。这样一题多问相当于把后序遍历和树的形状判断一起复习了。6. 后序遍历在真实工程里的样子6.1 表达式树求值编译器在解析数学表达式时经常把表达式建成一棵二叉树叶子节点是数字内部节点是运算符。要计算这棵树的值就必须先算左子表达式的值再算右子表达式的值最后对根节点做运算。这正好就是后序遍历。比如表达式(12)*3可以建成这样一棵树* / \ 3 / \ 1 2后序遍历先得到1 2 3 *也就是后缀表达式。遍历过程中遇到运算符就取前两个结果运算非常自然。所以后序遍历不只是在刷题里出现它直接对应逆波兰表达式的计算流程。6.2 析构与资源清理在需要手动管理内存的编程语言里删除一棵树必须用后序先删除左右孩子再删除根节点。如果顺序反了先删除根节点你就丢失了指向左右孩子的指针后续再也找不到它们内存就泄漏了。这种“先孩子后父亲”的顺序也体现在文件系统的递归删除目录、构建系统的任务调度中。依赖关系被抽象成树以后你想先编译子模块再编译依赖它的父模块后序就是唯一合理的顺序。6.3 从后序序列能反推什么常见考题里还有一种变形给定中序和后序遍历序列要求重建二叉树。思路是后序序列的最后一个元素一定是根节点然后在中序序列中找到根的位置左边是左子树右边是右子树再递归处理。这里后序的存在价值就是“告诉你根节点在哪一次分割里”。如果只给后序和前序没有中序普通二叉树不一定能唯一重建这也是搜索热词里大家经常讨论的点。7. 每日一题坚持到第 14 天我总结下来的三个实测建议7.1 先画一棵“丑树”再写代码我刷二叉树题最大的体会是动手敲代码前先在草稿纸上画一棵三层的不规则树手动写出它的前序、中序、后序遍历结果再拿着这组结果去验证代码。很多“看起来对但输出不对”的问题用一支笔就能定位。尤其是后序画图以后你会非常直观地感受到“根节点的输出被推迟到最末”。7.2 递归三问快速避坑每次写递归函数先问自己三个问题第一行处理空节点了吗返回值类型和上一层一致吗递归调用的先后顺序符合哪种遍历这三个问题能在十秒内暴露至少一半的运行时错误。代码写完后用三棵固定小树过一遍空树、单节点、两层满树、三层左斜树。这四个例子基本覆盖了 90% 的基础边界错误也正好覆盖了判断是否为满二叉树那类题的测试思路。7.3 把相邻考点绑定在一起复习只刷一道后序遍历记忆是散的。把二叉树深度、判断 BST、判断满二叉树、顺序存储、线索二叉树这些问题放在同一个周期里刷你会发现它们共享同一套后序递归骨架。以后遇到“删除二叉树”“二叉树的最近公共祖先”“给表达式树求值”这类题也能一眼看出来该从哪个方向下手。我自己刷这一题的过程也从“背一个遍历模板”变成了“理解一类树形问题的通用处理方式”。如果之后继续做每日一题系列我会继续用这种“单题切入、四周扩散”的方式整理更多题目。至少现在这个第 14 题已经帮我打通了二叉树遍历里最容易出问题的一环。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。