LeetCode 257. 二叉树的所有路径
题目描述

题意分析
输入一棵二叉树的根节点,输出所有「从根节点到叶子节点」的路径,每条路径写成
1->2->5这样的字符串,返回顺序不限。题面有几个必须抠准的字眼。「叶子节点」指左右孩子都为空的节点,只有一个孩子的节点不算叶子,所以路径不能在半路截断;「从根开始」意味着每条路径都以根节点值打头,不存在从中间起步的路径;分隔符
->只出现在相邻两个节点值之间,路径首尾都不带。节点数范围是 $[1, 100]$,即树非空,但节点值可能是负数,字符串里出现-3这样的片段是正常的,不能用字符长度去猜节点个数。边界情况:只有一个节点时,这个节点既是根又是叶,答案是单元素列表
["1"],此时一个->都不该出现;某个节点只有右孩子时,不能因为左孩子为空就把它当叶子记录,必须继续往右走。答案条数等于叶子数量,每条路径长度是该叶子的深度,因此输出规模本身可以达到 $O(n \cdot h)$ 字符。
解法:DFS 回溯构造路径
核心思路
根到叶路径天然适合深度优先搜索:递归进入节点时把节点值加入当前路径,只有到达叶子时才把完整路径加入答案,随后回退到父节点继续搜索另一棵子树。
Java 用
StringBuilder、Go 用[]byte维护共享缓冲区,避免每层复制整条路径。递归契约是:进入dfs(node)时,缓冲区保存根到node父节点的路径;离开时必须恢复到完全相同的状态。进入后先记录旧长度,再追加本节点,函数结束前截回旧长度。正确性可按递归契约说明:DFS 会访问每个节点;只有左右孩子都为空时才记录,因此不会产生半截路径;到达叶子时缓冲区恰好包含从根到该叶子的所有节点。叶子处复制快照,回滚则保证兄弟分支互不污染。
解题步骤
- 初始化答案列表和空路径缓冲区,从根节点开始 DFS。
- 进入节点时记录缓冲区旧长度;路径非空则先追加
->,再追加当前值。- 若当前节点是叶子,将
path.toString()作为快照加入答案。- 否则继续递归左右孩子,保证所有根到叶路径都会被枚举。
- 返回父节点前把缓冲区恢复到旧长度,清除本分支留下的内容。
对
[1,2,3,null,5],DFS 先得到1->2->5;回退到根路径1后再进入右子树,得到1->3。若不回滚,第二条路径会错误地带上左分支内容。
代码实现
import java.util.*;
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> res = new ArrayList<>();
dfs(root, new StringBuilder(), res);
return res;
}
private void dfs(TreeNode node, StringBuilder path, List<String> res) {
if (node == null) {
return;
}
int length = path.length();
if (length > 0) {
path.append("->");
}
path.append(node.val);
if (node.left == null && node.right == null) {
res.add(path.toString());
} else {
dfs(node.left, path, res);
dfs(node.right, path, res);
}
path.setLength(length);
}
}
import "strconv"
func binaryTreePaths(root *TreeNode) []string {
res := make([]string, 0)
path := make([]byte, 0)
var dfs func(*TreeNode)
dfs = func(node *TreeNode) {
if node == nil {
return
}
length := len(path)
if length > 0 {
path = append(path, '-', '>')
}
path = strconv.AppendInt(path, int64(node.Val), 10)
if node.Left == nil && node.Right == nil {
res = append(res, string(path))
} else {
dfs(node.Left)
dfs(node.Right)
}
path = path[:length]
}
dfs(root)
return res
}
复杂度分析
- 时间复杂度:$O(n + S)$,其中 $n$ 是节点数,$S$ 是所有输出路径的字符总数;最坏可写成 $O(n \cdot h)$。遍历每个节点一次,叶子处复制当前路径。
- 空间复杂度:$O(h)$,不计答案;递归栈和共享路径缓冲区的长度都与树高 $h$ 成正比。
关键点总结
- 递归契约要同时包含两件事:进入时路径代表父节点,离开时恢复原状。
- 只有左右孩子都为空才是叶子,只有一个孩子的节点不能提前记录。
- 共享可变缓冲区时,需要在叶子处生成快照、在返回前回滚。
- 分隔符只在已有祖先路径时追加,避免路径开头或结尾出现多余的
->。
易错点总结
- 忘记回滚缓冲区会污染兄弟分支,例如第二条路径可能变成
1->2->5->3。- 在追加之后才记录旧长度,回滚时无法删除当前节点;存档必须发生在本层修改之前。
- 用
left == null || right == null判断叶子,会把只有一个孩子的节点误判为叶子;必须两边都为空。- 叶子处分支若提前
return,必须先回滚;当前实现统一在函数末尾回滚,避免遗漏。- 无条件追加
->会让单节点树输出->1,应只在路径非空时追加分隔符。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 112. 路径总和 | 简单 | 只判存在、可提前返回 |
| 113. 路径总和 II | 中等 | 列表快照拷贝 |
| 129. 求根节点到叶节点数字之和 | 中等 | 路径数字按位累加 |
| 404. 左叶子之和 | 简单 | 由父节点识别左叶子 |
| 1022. 从根到叶的二进制数之和 | 简单 | 位运算维护路径值 |
| LCR 049. 求根节点到叶节点数字之和 | 中等 | 同型题、数值版路径 |
| 剑指 Offer 34. 二叉树中和为某一值的路径 | 中等 | 目标和剪枝加回溯 |