目录
257.二叉树的所有路径
题目
给你一个二叉树的根节点 root
,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [1,2,3,null,5] 输出:["1->2->5","1->3"]
代码(前序递归回溯)
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> result = new ArrayList<>(); //存放路径合集
List<Integer> path = new ArrayList<>(); //存放单条路径
if(root == null){
return result;
}
traverse(root,path,result);
return result;
}
//前序递归遍历二叉树
public void traverse(TreeNode cur,List<Integer> path,List<String> result){
//处理中间节点,这里中间节点的处理必须写到终止条件前面(只有一个节点)
path.add(cur.val);
//终止条件,遍历到叶子节点
if(cur.left == null && cur.right == null){
//先处理list的path为格式化的String
StringBuilder sb = new StringBuilder();
//遍历path的每个节点值
for(int i=0; i < path.size();i++){
sb.append(path.get(i)); //加入节点值
if(i != path.size() - 1){
sb.append("->"); //不是最后一个节点,加入->
}
}
//再把String添加到result中
result.add(sb.toString());
//终止
return;
}
//单层逻辑,继续处理左右孩子
if(cur.left != null){
//递归:当前节点有左孩子,递归左孩子的路径
traverse(cur.left,path,result);
//回溯:由于path存储的是单条路径,递归左孩子后需要把左孩子从path中去掉
path.remove(path.size() - 1);
}
if(cur.right != null){
//递归:当前节点有右孩子,递归右孩子的路径
traverse(cur.right,path,result);
//回溯:由于path存储的是单条路径,递归右孩子后需要把右孩子从path中去掉
path.remove(path.size() - 1);
}
}
}
代码(前序迭代)
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> result = new ArrayList<>(); //存放路径合集
Stack<TreeNode> stacknode = new Stack<>(); //存放前序遍历的节点
//stackpath的设置就是在原先前序遍历记录节点的基础上,把从根节点到当前遍历节点的路径的存储下来
Stack<String> stackpath = new Stack<>(); //存放前序遍历的路径
if(root == null){
return result;
}
//两个栈是一一对应的,node栈存储的是前序的节点,path栈存储的是每个节点对应的从根节点到它本身的路径
//根节点入队
stacknode.push(root);
//根节点路径入队
stackpath.push(root.val+""); //+""是为了把int值转为String
while(!stacknode.isEmpty()){
TreeNode cur = stacknode.pop(); //中间节点cur出队
String path = stackpath.pop(); //中间节点的路径出队,path对应的是根节点到cur的路径
//如果cur是叶子节点,path正好是到cur叶子节点的一条路径
if(cur.left == null && cur.right == null){
result.add(path); //把根节点到cur(叶子结点)的路径入队
}
//处理右孩子
if(cur.right != null){
stacknode.push(cur.right); //右孩子节点入队
//path是根节点到cur节点的路径
//再加上cur.right.val,就是根节点到cur.right的路径
stackpath.push(path + "->" + cur.right.val); //右孩子路径入队
}
//处理左孩子
if(cur.left != null){
stacknode.push(cur.left); //左孩子节点入队
stackpath.push(path + "->" + cur.left.val); //左孩子路径入队
}
}
return result;
}
}
代码(层序迭代)
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
Queue<TreeNode> node = new ArrayDeque<>();
Queue<String> path = new ArrayDeque<>();
List<String> res = new ArrayList<>();
if(root == null){
return res;
}
node.offer(root);
path.offer(root.val+"");
while(!node.isEmpty()){
int len = node.size();
while(len-- > 0){
TreeNode cur = node.poll();
String curpath = path.poll();
if(cur.left == null && cur.right == null){
res.add(curpath);
}
if(cur.left != null){
node.offer(cur.left);
StringBuilder sb = new StringBuilder(curpath);
sb.append("->");
sb.append(cur.left.val);
path.offer(sb.toString());
}
if(cur.right != null){
node.offer(cur.right);
StringBuilder sb = new StringBuilder(curpath);
sb.append("->");
sb.append(cur.right.val);
path.offer(sb.toString());
}
}
}
return res;
}
}
总结
1.前序递归回溯
前序递归回溯的逻辑是,用path存储单条路径,用result存储所有路径。
先处理中间节点,加入到path中。
终止条件是遇到叶子节点,就把path转为对应的String并添加到result中。
单层逻辑是,分别递归回溯处理每个左右孩子。如果左孩子非空,说明路径需要往左孩子走,traverse左孩子,获取左孩子中的路径,由于path中存储的是单条路径,traverse左孩子后,相当于左孩子中所有的路径都已经存到result中,而当前的path中有左孩子的值,需要去掉它,保证path中存储的是中间节点值,然后才能继续递归。如果右孩子非空,原理也是一样的。
2.前序迭代
前序迭代的逻辑是,用一个node栈存储前序遍历的节点,用一个path栈存储前序遍历的节点对应的路径(根节点到当前遍历节点),这两个栈是一一对应的。当某个节点cur出栈时,其对应的path也要出栈。然后判断cur是否是叶子节点,如果是,就把对应的path添加的result路径集合中。如果不是叶子节点,就要继续遍历,先处理右孩子,当右孩子非空,先把右孩子添加到node栈中,同时,把path+右孩子值的路径添加到path栈中。再处理左孩子,逻辑同上。
用node和path栈同时前序遍历,可以简单理解为,我们一边遍历节点保存node栈,一边把从根节点到该节点的路径也记录在path栈。这样,当我们要出栈处理节点时,同时可以出栈获取该节点的路径。如果该路径是一个到叶子的路径,就能直接添加到result中。如果该路径不是到叶子的路径,就继续遍历他的孩子,同时把该路径+孩子的路径添加到path栈。
核心逻辑相当于一边前序遍历node,一边前序存储path。然后当我们按原先前序遍历的逻辑处理node时,加上判断是不是叶子节点,如果是,就把出栈的path存到result中。而看似难理解的stackpath.push(path + "->" + cur.right.val)和stackpath.push(path + "->" + cur.left.val)语句,其实就是在遍历时,同时把当前遍历节点的路径也存到栈里。方便我们在处理node时,同时拿到根节点到node的路径罢了。
第一次接触回溯,有点难理解,不过可以画个图举例,然后也能理解个差不多。今天的题目难一点,就做一道题吧。