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 并打标记)。
立即学习“go语言免费学习笔记(深入)”;
更稳妥的实操建议:
- 用
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 < levelSize; i++ {
node := queue[0]
queue = queue[1:]
res = append(res, node.Val)
if node.Left != nil { queue = append(queue, node.Left) }
if node.Right != nil { queue = append(queue, node.Right) }
}
}
注意:不要用 list.List 替代切片模拟队列,Go 中切片 append + queue[1:] 在小规模下性能更好,且内存局部性更强。
后序迭代最容易错在“右子树是否已处理”
后序要求左右都访问完才处理根,但迭代中你无法回溯判断右子树是否已完成。典型错误是弹出节点后直接 append,结果变成“根→右→左”或乱序。
可靠解法只有两种:
- 双栈法:第一栈按
root → right → left压,第二栈接收弹出顺序,最后翻转第二栈 - 单栈 + lastVisited 标记:记录上一个已处理节点,每次检查
node.Right == lastVisited或node.Right == nil,满足才处理当前节点
后者更省内存,但要注意 lastVisited 必须初始化为 nil,且更新时机只能在 append 后立即赋值,不能在压栈时更新。


















