treenode必须含val、left、right三字段;递归遍历差异仅在append(node.val)位置:前序首行、中序次行、后序末行;易错点是漏判node==nil导致空指针panic。

TreeNode 结构定义必须包含 Val、Left、Right 三个字段,否则所有遍历函数都会因空指针或类型不匹配而 panic。
递归遍历的写法差异只在三行顺序上
前序、中序、后序递归实现本质是同一套骨架,区别仅在于 append(访问根)这行代码的位置:
- 前序:
res = append(res, node.Val)→dfs(node.Left)→dfs(node.Right) - 中序:
dfs(node.Left)→res = append(res, node.Val)→dfs(node.Right) - 后序:
dfs(node.Left)→dfs(node.Right)→res = append(res, node.Val)
容易踩的坑是把 if node == nil 判断漏掉,或写成 if node != nil 后没 return,导致空节点继续向下递归,最终触发 nil pointer dereference。
inorderTraversal 迭代实现要用显式栈+状态标记
中序迭代不是简单“压左、弹、压右”就能搞定。因为弹出节点后,你得知道该去它的右子树,还是该把它的值加入结果——这需要额外信息。
常见错误写法是只存节点指针,导致无法区分“刚入栈”和“已处理完左子树”。正确做法是用一个布尔标记表示是否已访问过左子树,或用两阶段压栈(先压 node,再压 node.Right 和 node.Left 并打标记)。
更稳妥的实操建议:
- 用
stack []interface{}存struct{ node *TreeNode; visited bool } - 遇到未访问节点:压入自身(
visited: false),再压入左子节点(visited: false) - 遇到已访问节点:
append值,再压入右子节点(visited: false)
层序遍历别硬套递归,queue 是唯一合理选择
所谓“递归实现层序遍历”,本质是按深度分层调用,每次 dfs(node, depth) 把值塞进 res[depth] 对应切片。但它有严重缺陷:
- 必须预估最大深度,或动态扩容
res,带来额外 slice copy 开销 - 无法提前终止(比如找第 k 个节点),因为所有层都得递归到底
- 不符合 BFS 直观语义,调试时难以对应实际层级
实操中一律用 queue []*TreeNode 配合 for + for 模式:
for len(queue) > 0 {
levelSize := len(queue)
for i := 0; i
<p>注意:不要用 <code>list.List</code> 替代切片模拟队列,Go 中切片 <code>append</code> + <code>queue[1:]</code> 在小规模下性能更好,且内存局部性更强。</p>
<h3>后序迭代最容易错在“右子树是否已处理”</h3>
<p>后序要求左右都访问完才处理根,但迭代中你无法回溯判断右子树是否已完成。典型错误是弹出节点后直接 <code>append</code>,结果变成“根→右→左”或乱序。</p>
<p>可靠解法只有两种:</p>
- 双栈法:第一栈按
root → right → left压,第二栈接收弹出顺序,最后翻转第二栈 - 单栈 + lastVisited 标记:记录上一个已处理节点,每次检查
node.Right == lastVisited或node.Right == nil,满足才处理当前节点
后者更省内存,但要注意 lastVisited 必须初始化为 nil,且更新时机只能在 append 后立即赋值,不能在压栈时更新。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











