treenode结构必须用指针定义,否则传值会拷贝副本,导致遍历修改无效、nil判断失效、内存无限递归;指针保障了对原树节点的可修改性、地址唯一性和链式访问能力。

TreeNode 结构必须用指针定义,否则所有遍历操作都只是在副本上运行,根本改不动原树——这是最常被忽略、也最容易导致“代码跑通但结果为空”的硬伤。
为什么 preorder(root *TreeNode) 里必须传 *TreeNode 而不是 TreeNode
Go 中结构体是值类型,传值即拷贝。如果写成 preorder(root TreeNode),函数内对 root.Left 或 root.Right 的任何赋值,都不会影响调用方的原始节点。
- 递归遍历时,你依赖的是指针的“可修改性”:比如中序遍历中要收集节点值到切片,必须能通过
root访问真实内存地址 - 非递归实现(如用栈)时,压入的是节点地址,不是副本;弹出后访问
node.Val才有意义 - 一旦混用值类型,
nil判断会失效(空结构体不等于nil指针),root == nil永远为 false
inorder() 递归实现里,为什么不能把 visit(root) 放在第一行
放错位置就不是中序了——中序的本质是“左→根→右”,顺序错一个,输出序列就完全失序。尤其在 BST 场景下,inorder() 唯一价值就是输出升序,错位等于废掉整个结构优势。
- 前序:必须先
visit(root),再递归左右;后序:必须最后visit(root) - 递归体里三行顺序不能靠记忆,建议始终按“L-N-R”这种缩写写注释,写完立刻对照
- 常见错误:把
inorder(root.Left)和inorder(root.Right)写反,虽不报错,但结果是镜像树的中序,和预期相反
非递归 postorder() 为什么比前/中序难写
因为后序要求“左右子树都处理完才能访问根”,而栈是 LIFO,天然倾向先处理刚压入的右子节点——你得额外标记某个节点是否“已处理过子树”,否则会重复压栈或漏访问。
- 简单做法:用两个栈,第一个存节点,第二个存“是否已展开子树”的布尔值
- 更常用做法:用一个栈 + 一个
lastVisited *TreeNode变量,每次弹出前检查top.Right == lastVisited || top.Left == lastVisited - 别直接套用前序的“压右再压左”逻辑——那得到的是“根→右→左”,翻转后才是后序,但翻转本身要额外空间
层序遍历用 queue 时,为什么推荐用 []*TreeNode 而不是 container/list
container/list 是双向链表,接口啰嗦、取首元素要 Front().Value.(*TreeNode),且每次 Remove() 后还要手动 Next(),实际性能反而不如切片+索引移动。
- 标准写法是维护
queue []*TreeNode和两个游标l, r,或更简洁地用queue = queue[1:]截断头部 - 切片扩容成本可控:一次
append不会频繁 realloc,而list每次插入都要 new node - 调试时打印
queue内容直观;list打印出来是一堆地址,看不出当前层有哪些节点
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











