当前:LC257 · 二叉树的所有路径 · 首次出现于 Day 21 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC257 · Binary Tree Paths · 树

二叉树的所有路径:把已经走过的字往下传

每条根到叶的路是一串新字符串。Go 里字符串按值传递,左支拼完的那份带不走右支的现场。

DFS:递归参数携带「已走过的路径字符串」。到叶子节点时,把当前字符串(或值拼接)加入结果。每条分支路径独立,不必回溯——因为字符串按值传递,天然隔离。时间 O(n²),空间 O(h)。

时间 O(n²)空间 O(h)结论先行 · 全文约 6 节
导读

给定一棵二叉树的根,返回所有从根走到叶子的路径,格式写成 "1->2->5" 这种箭头串。叶子是左右都空的节点;只走到半路、还没碰到叶子,不能交。

主例树是 [1, 2, 3, null, 5]:根 1,左 2 再往右到 5,右孩子是 3。两条根到叶:1→2→5 和 1→3,答案 ["1->2->5", "1->3"]。LC113 还要路径和等于目标;这里不求和,只收集字符串。

把每条路都从根再走一遍能做对,但同一段前缀 1→2 会被重复拼接。本文要回答:为什么把「已经拼好的前缀」当参数往下传就够了,以及主例从左叶回到根再走右叶时,现场为什么不会脏。

缺口是前缀串,不是一棵要改的共享数组

第一直觉是先遍历,碰到叶子再从根把节点值抠出来拼。结果能对,但每条路都要回头找父亲,或者在外面另存一张「节点到父」的表。更糟的做法是共用一个切片往前 push:走到 5 时切片是 [1,2,5],若不 pop 就去走 3,3 会接到 5 后面,拼出幽灵路径。

缺的不是「有哪些叶子」,而是进入某个节点时,从根到它的父亲已经写好的那截字。当前节点只负责把自己的值接上去;若自己是叶子,这串字就是一条答案;若不是,把新串分别交给左右孩子。问题从「整棵树有哪些路」收成「带着前缀,继续往下走」。

递归参数 s 是「从根走到当前节点之前」的字符串。进入节点 n:若 s 为空,说明这是根,s' 就是 n 的值;否则 s' = s + "->" + n 的值。这一个拼接动作同时处理了根和中间节点,箭头不会出现在开头,也不会在叶子后再多画一个。

Go 的 string 赋值和 s += 都会得到一份新串,调用者手里的 s 不变。左孩子递归里拼出的 "1->2->5" 是新值,回到节点 1 时,参数 s 仍是 "1"。右孩子拿到的仍是 "1",再拼成 "1->3"。所以这套写法没有 push/pop——不是忘了回溯,是值传递把隔离做完了。

若改成共享切片 path, isolation 就没了:进入时 append,离开时必须 path = path[:len-1],否则兄弟分支会看见别人留下的尾。LC113 走的就是切片这条路。两种写法收集的是同一批路径,恢复现场的责任不同。

用手走主例。从根进入,s 空,拼出 "1",1 不是叶子。先走左孩子 2:传入 "1",拼成 "1->2"。2 还有右孩子 5:传入 "1->2",拼成 "1->2->5",5 左右皆空,收下第一条。演示场景前三帧就是这一条左路,path 从 [1] 长到 [1,2,5]。

从 5 返回 2,再返回 1。1 的参数仍是 "1"。再走右孩子 3:拼成 "1->3",3 是叶子,收下第二条。演示最后一帧两条都在。空树没有路径;单节点树只有一条、等于根的值,不会多出箭头。时间主要花在拼接和拷贝字符串上,最坏每条路径长度 O(n),所以是 O(n²),不是 O(n)。

按值传递字符串每次拼接都是新值,左支改不了右支的参数。不必为这题强行写 push/pop。若路径改成切片,离开节点时就要 pop,那是 LC113 的合同,不是这题字符串写法的合同。
路径字符串逐层拼接
1235
路径:1
"1"

Go:DFS 传字符串

solution.goGo
func binaryTreePaths(root *TreeNode) []string {
res := []string{}
var dfs func(*TreeNode, string)
dfs = func(n *TreeNode, s string) {
if n == nil { return }
if s == "" { s = strconv.Itoa(n.Val) } else { s += "->" + strconv.Itoa(n.Val) }
if n.Left == nil && n.Right == nil {
res = append(res, s)
return
}
dfs(n.Left, s)
dfs(n.Right, s)
}
dfs(root, "")
return res
}

1s 是调用者给的前缀。函数里再赋值或 +=,改的是本帧的 s,外面那份还在。

2根的 s 为空,只写入节点值;其后每次先加 "->" 再加值,避免 "->1->2"。

3左右都空才是叶子,此时整串入结果。有一个孩子就还要往下传,不能提前收。

4左右各传当前这份 s。主例回到 1 再走 3 时,传入的仍是 "1",不是 "1->2->5"。

总结

前缀字符串当参数往下传,叶子处收下。主例两条:1->2->5 与 1->3。

  • 字符串按值传递,分支天然隔离,这套写法不必 push/pop。
  • 叶子是唯一收集点。根单独成树时答案就是根的值。
  • 若路径改成切片,离开就要 pop,那是 LC113,不是本题字符串合同。
同族题目
LC112路径总和LC113路径总和 IILC144二叉树的前序遍历