一.题目

二.思路讲解
2.1 选择遍历方式
本题要求返回从根节点到所有叶子节点 的路径,因此我们需要采用前序遍历 ,这样才能在访问节点时从根开始逐步向下构建路径。前序遍历的顺序是根-左-右,正好符合路径的生成顺序。
2.2 路径的构建与存储
在遍历过程中,我们需要一个数据结构来记录当前路径。通常使用一个字符串 path 来存储从根到当前节点的路径表示。对于普通节点,我们应将节点值转换为字符串,并加上 "->" 作为分隔;而对于叶子节点 ,则只需加上节点值即可,然后将完整路径存入结果数组 ret 中。这里,path 和 ret 可以是全局变量或通过参数传递。
2.3 回溯与参数传递的权衡
如果采用全局变量 path,那么在递归返回时,需要手动恢复现场 (即删除刚刚添加的节点部分),否则路径会错误累积。例如,处理完一个叶子节点后,必须将最后添加的节点值及箭头弹出,以便后续路径使用。这种回溯操作容易遗漏,导致错误。
如果将 path 作为递归函数的参数 传递,则每次递归调用都会创建一个新的字符串副本,无需手动恢复现场,因为参数是值传递 ,不会影响上一层的 path。这种方式更安全。
2.4 递归终止条件与剪枝
为了简化问题,我们将叶子节点 作为递归的最简单情况 。当遇到叶子节点时,直接构建完整路径并存入结果,然后返回,不再继续递归。这样,我们就不需要递归到空节点,从而剪枝 掉空节点的判断,使代码更简洁高效。具体来说,在递归函数中,首先处理当前节点,如果当前节点是叶子节点,则记录路径并返回;否则,分别递归左子树和右子树(如果存在)。这样,我们通过提前终止避免了不必要的空节点递归。
三.代码演示
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution
{
public:
vector<string> ret;
vector<string> binaryTreePaths(TreeNode* root)
{
string path = "";
dfs(root,path);
return ret;
}
void dfs(TreeNode* root,string path)
{
//1.是叶子节点
if(root->left == nullptr && root->right == nullptr)
{
path += to_string(root->val);
ret.push_back(path);
return;
}
//2.不是叶子节点
path += to_string(root->val);
path += "->";
if(root -> left != nullptr)
dfs(root->left,path);
if(root -> right != nullptr)
dfs(root->right,path);
}
};
四.代码演示
一、递归函数设计
我们定义一个递归函数 dfs(TreeNode* root, string path) ,它的作用是:以当前节点 root 为起点,从根到该节点的路径已经记录在 path 中,继续向下遍历,并将所有从根到叶子节点的完整路径存入结果数组 ret。这里 path 采用值传递,这样每一层递归都有自己的副本,无需手动回溯。
二、递归终止条件(叶子节点处理)
本题将叶子节点 作为递归的最简单情况。当当前节点的左右子节点均为空时,说明到达叶子节点。此时,我们将当前节点的值追加到 path 中,形成完整路径,然后将其加入结果数组 ret,并返回。这样,我们无需递归到空节点 ,避免了不必要的递归调用,实现了剪枝。
三、递归步骤分解(非叶子节点处理)
对于非叶子节点,我们需要继续向下遍历左右子树。步骤如下:
-
构建当前路径 :将当前节点的值转换为字符串,并加上
"->"分隔符,追加到path末尾。 -
递归左子树 :如果左孩子存在,调用
dfs(root->left, path),传入更新后的路径。 -
递归右子树 :如果右孩子存在,调用
dfs(root->right, path)。
注意,由于 path 是值传递,左右子树的递归调用使用的是不同的副本,因此不会相互干扰,也无需手动恢复现场。
四、参数传递与回溯
这里采用 path 作为函数参数(值传递),而不是全局变量。这样做的好处是:
-
每次递归调用都会创建新的字符串副本,自动隔离不同分支的路径。
-
避免了手动回溯(如
pop_back)的麻烦,降低了出错概率。 -
虽然会产生一定的拷贝开销,但路径长度有限,完全可以接受。
如果使用全局变量,则需要在每次递归返回后手动删除刚刚添加的部分,这容易遗漏,而参数传递则天然解决了这个问题。
五、关键细节
-
叶子节点的判断 :直接检查
root->left == nullptr && root->right == nullptr,因为题目保证每个节点要么有孩子要么没有。 -
路径构建 :对于非叶子节点,需要添加
"->";对于叶子节点,只添加节点值,不加箭头。 -
空节点剪枝:在递归前先判断孩子是否存在,避免了调用空节点,进一步优化。
-
结果数组 :
ret作为成员变量,在递归过程中不断添加完整路径。