网站开发数据库有关合同,弹幕网站开发难么,做带后台的网站,网站文章优化流程方案给你一个二叉树的根节点 root #xff0c;按 任意顺序 #xff0c;返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。 示例 1#xff1a; 输入#xff1a;root [1,2,3,null,5]
输出#xff1a;[1-2-5,1-3]示例 …
给你一个二叉树的根节点 root 按 任意顺序 返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。 示例 1 输入root [1,2,3,null,5]
输出[1-2-5,1-3]示例 2
输入root [1]
输出[1]思路
这道题目要求从根节点到叶子的路径所以需要前序遍历这样才方便让父节点指向孩子节点找到对应的路径。
在这道题目中将第一次涉及到回溯因为我们要把路径记录下来需要回溯来回退一个路径再进入另一个路径。
前序遍历以及回溯的过程如图 我们先使用递归的方式来做前序遍历。要知道递归和回溯就是一家的本题也需要回溯。
#递归
递归函数参数以及返回值
要传入根节点记录每一条路径的path和存放结果集的result这里递归不需要返回值代码如下
void traversal(TreeNode* cur, vectorint path, vectorstring result)确定递归终止条件
在写递归的时候都习惯了这么写
if (cur NULL) {终止处理逻辑
}但是本题的终止条件这样写会很麻烦因为本题要找到叶子节点就开始结束的处理逻辑了把路径放进result里。
那么什么时候算是找到了叶子节点 是当 cur不为空其左右孩子都为空的时候就找到叶子节点。
所以本题的终止条件是
if (cur-left NULL cur-right NULL) {终止处理逻辑
}为什么没有判断cur是否为空呢因为下面的逻辑可以控制空节点不入循环。
再来看一下终止处理的逻辑。
这里使用vector 结构path来记录路径所以要把vector 结构的path转为string格式再把这个string 放进 result里。
那么为什么使用了vector 结构来记录路径呢 因为在下面处理单层递归逻辑的时候要做回溯使用vector方便来做回溯。
可能有的同学问了我看有些人的代码也没有回溯啊。
其实是有回溯的只不过隐藏在函数调用时的参数赋值里下文我还会提到。
这里我们先使用vector结构的path容器来记录路径那么终止处理逻辑如下
if (cur-left NULL cur-right NULL) { // 遇到叶子节点string sPath;for (int i 0; i path.size() - 1; i) { // 将path里记录的路径转为string格式sPath to_string(path[i]);sPath -;}sPath to_string(path[path.size() - 1]); // 记录最后一个节点叶子节点result.push_back(sPath); // 收集一个路径return;
}确定单层递归逻辑
因为是前序遍历需要先处理中间节点中间节点就是我们要记录路径上的节点先放进path中。
path.push_back(cur-val);
然后是递归和回溯的过程上面说过没有判断cur是否为空那么在这里递归的时候如果为空就不进行下一层递归了。
所以递归前要加上判断语句下面要递归的节点是否为空如下
if (cur-left) {traversal(cur-left, path, result);
}
if (cur-right) {traversal(cur-right, path, result);
}此时还没完递归完要做回溯啊因为path 不能一直加入节点它还要删节点然后才能加入新的节点。
那么回溯要怎么回溯呢一些同学会这么写如下
if (cur-left) {traversal(cur-left, path, result);
}
if (cur-right) {traversal(cur-right, path, result);
}
path.pop_back();这个回溯就有很大的问题我们知道回溯和递归是一一对应的有一个递归就要有一个回溯这么写的话相当于把递归和回溯拆开了 一个在花括号里一个在花括号外。
所以回溯要和递归永远在一起世界上最遥远的距离是你在花括号里而我在花括号外
那么代码应该这么写
if (cur-left) {traversal(cur-left, path, result);path.pop_back(); // 回溯
}
if (cur-right) {traversal(cur-right, path, result);path.pop_back(); // 回溯
}那么本题整体代码如下
// 版本一
class Solution {
private:void traversal(TreeNode* cur, vectorint path, vectorstring result) {path.push_back(cur-val); // 中中为什么写在这里因为最后一个节点也要加入到path中 // 这才到了叶子节点if (cur-left NULL cur-right NULL) {string sPath;for (int i 0; i path.size() - 1; i) {sPath to_string(path[i]);sPath -;}sPath to_string(path[path.size() - 1]);result.push_back(sPath);return;}if (cur-left) { // 左 traversal(cur-left, path, result);path.pop_back(); // 回溯}if (cur-right) { // 右traversal(cur-right, path, result);path.pop_back(); // 回溯}}public:vectorstring binaryTreePaths(TreeNode* root) {vectorstring result;vectorint path;if (root NULL) return result;traversal(root, path, result);return result;}
};如上的C代码充分体现了回溯。
那么如上代码可以精简成如下代码
class Solution {
private:void traversal(TreeNode* cur, string path, vectorstring result) {path to_string(cur-val); // 中if (cur-left NULL cur-right NULL) {result.push_back(path);return;}if (cur-left) traversal(cur-left, path -, result); // 左if (cur-right) traversal(cur-right, path -, result); // 右}public:vectorstring binaryTreePaths(TreeNode* root) {vectorstring result;string path;if (root NULL) return result;traversal(root, path, result);return result;}
};如上代码精简了不少也隐藏了不少东西。
注意在函数定义的时候void traversal(TreeNode* cur, string path, vectorstring result) 定义的是string path每次都是复制赋值不用使用引用否则就无法做到回溯的效果。这里涉及到C语法知识
那么在如上代码中貌似没有看到回溯的逻辑其实不然回溯就隐藏在traversal(cur-left, path -, result);中的 path -。 每次函数调用完path依然是没有加上- 的这就是回溯了。
为了把这份精简代码的回溯过程展现出来大家可以试一试把
if (cur-left) traversal(cur-left, path -, result); // 左 回溯就隐藏在这里改成如下代码
path -;
traversal(cur-left, path, result); // 左即
if (cur-left) {path -;traversal(cur-left, path, result); // 左
}
if (cur-right) {path -;traversal(cur-right, path, result); // 右
}此时就没有回溯了这个代码就是通过不了的了。
如果想把回溯加上就要 在上面代码的基础上加上回溯就可以AC了。
if (cur-left) {path -;traversal(cur-left, path, result); // 左path.pop_back(); // 回溯 path.pop_back(); // 回溯 -
}
if (cur-right) {path -;traversal(cur-right, path, result); // 右path.pop_back(); // 回溯 path.pop_back(); // 回溯 -
}整体代码如下
//版本二
class Solution {
private:void traversal(TreeNode* cur, string path, vectorstring result) {path to_string(cur-val); // 中中为什么写在这里因为最后一个节点也要加入到path中if (cur-left NULL cur-right NULL) {result.push_back(path);return;}if (cur-left) {path -;traversal(cur-left, path, result); // 左path.pop_back(); // 回溯 path.pop_back(); // 回溯 -}if (cur-right) {path -;traversal(cur-right, path, result); // 右path.pop_back(); // 回溯path.pop_back(); // 回溯 -}}public:vectorstring binaryTreePaths(TreeNode* root) {vectorstring result;string path;if (root NULL) return result;traversal(root, path, result);return result;}
}; 大家应该可以感受出来如果把 path -作为函数参数就是可以的因为并没有改变path的数值执行完递归函数之后path依然是之前的数值相当于回溯了
综合以上第二种递归的代码虽然精简但把很多重要的点隐藏在了代码细节里第一种递归写法虽然代码多一些但是把每一个逻辑处理都完整的展现出来了。
#拓展
这里讲解本题解的写法逻辑以及一些更具体的细节下面的讲解中涉及到C语法特性如果不是C的录友就可以不看了避免越看越晕。
如果是C的录友建议本题独立刷过两遍再看下面的讲解同样避免越看越晕造成不必要的负担。
在第二版本的代码中其实仅仅是回溯了 - 部分调用两次pop_back一个pop 一次pop-大家应该疑惑那么 path to_string(cur-val); 这一步为什么没有回溯呢 一条路径能持续加节点 不做回溯吗
其实关键还在于 参数使用的是 string path这里并没有加上引用 即本层递归中path 该节点数值但该层递归结束上一层path的数值并不会受到任何影响。 如图所示 节点4 的path在遍历到节点3path3遍历节点3的递归结束之后返回节点4回溯的过程path并不会把3加上。
所以这是参数中不带引用不做地址拷贝只做内容拷贝的效果。这里涉及到C引用方面的知识
在第一个版本中函数参数我就使用了引用即 vectorint path 这是会拷贝地址的所以 本层递归逻辑如果有path.push_back(cur-val); 就一定要有对应的 path.pop_back()
那有同学可能想为什么不去定义一个 string path 这样的函数参数呢然后也可能在递归函数中展现回溯的过程但关键在于path to_string(cur-val); 每次是加上一个数字这个数字如果是个位数那好说就调用一次path.pop_back()但如果是 十位数百位数千位数呢 百位数就要调用三次path.pop_back()才能实现对应的回溯操作这样代码实现就太冗余了。
所以第一个代码版本中我才使用 vector 类型的path这样方便给大家演示代码中回溯的操作。 vector类型的path不管 每次 路径收集的数字是几位数总之一定是int所以就一次 pop_back就可以。
#迭代法
至于非递归的方式我们可以依然可以使用前序遍历的迭代方式来模拟遍历路径的过程对该迭代方式不了解的同学可以看文章二叉树听说递归能做的栈也能做 (opens new window)和二叉树前中后序迭代方式统一写法 (opens new window)。
这里除了模拟递归需要一个栈同时还需要一个栈来存放对应的遍历路径。
C代码如下
class Solution {
public:vectorstring binaryTreePaths(TreeNode* root) {stackTreeNode* treeSt;// 保存树的遍历节点stackstring pathSt; // 保存遍历路径的节点vectorstring result; // 保存最终路径集合if (root NULL) return result;treeSt.push(root);pathSt.push(to_string(root-val));while (!treeSt.empty()) {TreeNode* node treeSt.top(); treeSt.pop(); // 取出节点 中string path pathSt.top();pathSt.pop(); // 取出该节点对应的路径if (node-left NULL node-right NULL) { // 遇到叶子节点result.push_back(path);}if (node-right) { // 右treeSt.push(node-right);pathSt.push(path - to_string(node-right-val));}if (node-left) { // 左treeSt.push(node-left);pathSt.push(path - to_string(node-left-val));}}return result;}
};当然使用java的同学可以直接定义一个成员变量为object的栈StackObject stack new Stack();这样就不用定义两个栈了都放到一个栈里就可以了。
#总结
本文我们开始初步涉及到了回溯很多同学过了这道题目可能都不知道自己其实使用了回溯回溯和递归都是相伴相生的。
我在第一版递归代码中把递归与回溯的细节都充分的展现了出来大家可以自己感受一下。
第二版递归代码对于初学者其实非常不友好代码看上去简单但是隐藏细节于无形。
最后我依然给出了迭代法。
对于本题充分了解递归与回溯的过程之后有精力的同学可以再去实现迭代法。