用*treenode是因为go值传递无法修改原树结构,且结构体不能直接包含自身类型;中序递归需先判nil防panic,迭代版用显式栈避免爆栈,指针操作必须严谨处理nil边界。

为什么用 *TreeNode 而不是 TreeNode?
Go 中函数参数是值传递,如果传入 TreeNode 结构体,递归时修改的是副本,无法影响原始树结构(比如插入、删除、重连子节点)。遍历虽不改结构,但统一用指针能避免后续扩展时踩坑——尤其当你要实现 Insert 或 Remove 时,*TreeNode 是必须的。
常见错误:定义节点时写 type TreeNode struct { Val int; Left TreeNode; Right TreeNode },导致无限嵌套编译失败。Go 不允许结构体直接包含自身类型,必须用指针:Left *TreeNode。
中序遍历递归实现的关键写法
标准中序(左→根→右)用递归最直观,但要注意 nil 检查位置和递归终止条件:
func inorderTraversal(root *TreeNode) []int {
if root == nil {
return []int{}
}
var res []int
res = append(res, inorderTraversal(root.Left)...)
res = append(res, root.Val)
res = append(res, inorderTraversal(root.Right)...)
return res
}
这里容易忽略的点:
-
root == nil必须在开头判断,否则访问root.Left会 panic - 不能写成
if root != nil { ... }然后把return []int{}放在末尾——那样空树返回的是nil切片,和非空分支返回的非 nil 切片行为不一致,可能引发隐性 bug -
append(..., ......)中的...不可省略,否则会把整个切片当一个元素追加
避免栈溢出:迭代版中序用显式栈
深度很大的树(比如退化为链表)递归容易爆栈。迭代版用 []*TreeNode 模拟调用栈,核心是「一路压左,遇到 nil 就弹、记录、转右」:
func inorderTraversalIterative(root *TreeNode) []int {
var res []int
stack := []*TreeNode{}
curr := root
for curr != nil || len(stack) > 0 {
for curr != nil {
stack = append(stack, curr)
curr = curr.Left
}
curr = stack[len(stack)-1]
stack = stack[:len(stack)-1]
res = append(res, curr.Val)
curr = curr.Right
}
return res
}
关键细节:
- 外层循环条件是
curr != nil || len(stack) > 0,漏掉curr != nil会导致右子树为空时提前退出 - 弹栈后必须立刻赋值
curr = curr.Right,否则会重复处理同一节点 - Go 切片截断
stack[:len(stack)-1]是 O(1),比stack = stack[0:len(stack)-1]更安全(后者在底层数组被复用时可能保留旧值)
指针遍历中容易被忽略的 nil 场景
实际项目里,*TreeNode 变量本身可能是 nil,也可能指向一个 Val 有效但 Left/Right 为 nil 的节点。这两者语义不同,但代码里常混为一谈:
- 传入
nil表示空树,应直接返回空结果,不 panic -
root.Left == nil是合法状态,表示该节点无左子树,不是错误 - 若误写
if root.Left != nil { traverse(root.Left) }而漏掉else分支,逻辑就残缺了(中序必须处理左空但根存在的情况) - 测试时务必覆盖三种 case:
nil、单节点、左右都空的节点
真正麻烦的从来不是写对一次遍历,而是保证所有指针操作在 nil 边界上不越界、不漏判、不误判。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











