题目
Problem: 257. 二叉树的所有路径
给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。叶子节点 是指没有子节点的节点。
前知
递归与回溯
递归是一种自然而然地用来处理树形结构的方法,因为树的结构本身就是递归的。在二叉树中,递归通常用于遍历和搜索。可以参考之前我写过的深度优先遍历中的递归法
System.out.println(root.val); // 先访问根节点
preorderTraversal(root.left); // 再递归遍历左子树
preorderTraversal(root.right); // 最后递归遍历右子树
回溯是一种解决问题的算法技巧,通常用于在问题的解空间中搜索所有可能的解。回溯算法通过尝试一系列的选择,并在每个选择后进行递归探索,直到找到解决方案或者确定无解为止。如果在某个选择后发现无法继续前进,就会回溯到之前的状态,尝试其他的选择。
回溯和递归是可以同时存在的,回溯通常是通过递归来实现的。在回溯算法中,递归函数的调用扮演了重要的角色,它负责在每个选择后继续搜索解空间。当达到某个条件时(例如找到解决方案或确定无解),递归会返回上一层,并尝试其他选择,这就是回溯的过程。回溯通常用于解决更复杂的问题,例如在二叉树中搜索特定路径、查找最优解或者n皇后问题等
StringBuilder
StringBuilder 是 Java 中用于处理可变字符序列的类,它提供了一系列方法来进行字符串的拼接、插入、删除等操作。与 String 类不同,StringBuilder 的对象是可变的,即可以在不创建新的对象的情况下直接修改其内容,因此在处理大量字符串拼接时,使用 StringBuilder 可以提高性能。
以下是常用的一些方法:
创建一个新的StringBuilder对象
StringBuilder sb = new StringBuilder();
append():在字符串末尾添加字符、字符数组、字符串、任意类型的数据等。
insert():在指定位置插入字符、字符数组、字符串、任意类型的数据等。
delete():删除指定位置的字符或字符区间。
replace():替换指定位置的字符或字符区间。
reverse():反转字符串中的字符顺序。
length():返回当前字符串的长度。
toString():将 StringBuilder 对象转换为 String 类型的字符串
字符串三者比较String,StringBuffer ,StringBuilder 。
如果需要频繁进行字符串操作且在单线程环境下,推荐使用 StringBuilder;如果在多线程环境下,推荐使用 StringBuffer;如果字符串内容不需要修改,可以使用 String。
- 不可变性(Immutability):
String 是不可变的,一旦创建就无法修改其内容。每次对字符串进行操作(如拼接、替换等),都会产生一个新的字符串对象。
StringBuffer 和 StringBuilder 是可变的,可以在不创建新对象的情况下直接修改其内容。
- 线程安全性(Thread Safety):
String 是线程安全的,因为它是不可变的,不会受到多线程并发访问的影响。
StringBuffer 的方法是线程安全的,因为它的方法都是同步的,可以在多线程环境下安全使用。
StringBuilder 不是线程安全的,因为它的方法不是同步的,如果在多线程环境下使用,需要额外的同步措施来保证线程安全。
- 性能(Performance):
String 的不可变性使得它适合在不需要频繁修改字符串的情况下使用,但在大量字符串拼接的场景下,性能较差,因为每次拼接都会创建新的字符串对象。
StringBuffer 适用于在多线程环境下需要频繁进行字符串操作的场景,它的方法都是同步的,但性能相对较差。
StringBuilder 适用于在单线程环境下需要频繁进行字符串操作的场景,它的方法不是同步的,性能较 StringBuffer 更好。
- 用途(Usage):
String 适用于需要不可变性的场景,例如字符串常量、字符串连接操作等。
StringBuffer 适用于多线程环境下需要频繁进行字符串操作的场景。
StringBuilder 适用于单线程环境下需要频繁进行字符串操作的场景,且性能要求较高的场景。
题解
一、思路
要求从根节点到叶子的路径,所以要用到递归的方式进行前序遍历找到一条路径,然后进行回溯来回退一个路径再进入到另一个路径
此图片取自Carl老师代码随想录里的
二、解题方法
递归三部曲
1. 确定递归函数的参数和返回值
通过前序递归先找到左树的叶子结点,参数为结点node,路径paths,最终结果result,不需要返回值
private void traversal(TreeNode root, List<Integer> paths, List<String> res)
2. 确定终止条件
终止条件不能是等到当前结点没有cur == NULL
了才终止,要在到叶子结点,左右孩子都为空时就退出。因为这样才可以将路径添加到result数组里去,但是这样cur就没有判断是否为空,我们在下面第三步里判断。
终止处理的逻辑为用stringbuilder记录paths从根节点到叶子节点的数据并用“->”连接,将这个字符串添加到res里退出本次递归
if (root.left == null && root.right == null) {
// 输出
StringBuilder sb = new StringBuilder();// StringBuilder用来拼接字符串,速度更快
for (int i = 0; i < paths.size() - 1; i++) {
sb.append(paths.get(i)).append("->");
}
sb.append(paths.get(paths.size() - 1));// 记录最后一个节点
res.add(sb.toString());// 收集一个路径
return;
}
3. 确定单层递归的逻辑
因为前序遍历的顺序首先将中间节点加入路径里面,再递归左子树和右子树,上面第二步说了没有判断cur是否为空,于是在这里加上判断条件。由于递归和回溯是一一对应的,递归进了下一层的话,同样回溯也得有一个删除结点返回上一层的语句
paths.add(root.val);
if (root.left != null) { // 左
traversal(root.left, paths, res);
paths.remove(paths.size() - 1);// 回溯
}
if (root.right != null) { // 右
traversal(root.right, paths, res);
paths.remove(paths.size() - 1);// 回溯
}
三、Code
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
/**
* 递归法
*/
public List<String> binaryTreePaths(TreeNode root) {
List<String> res = new ArrayList<>();// 存最终的结果
if (root == null) {
return res;
}
List<Integer> paths = new ArrayList<>();// 作为结果中的路径
traversal(root, paths, res);
return res;
}
private void traversal(TreeNode root, List<Integer> paths, List<String> res) {
paths.add(root.val);// 前序遍历,中
// 遇到叶子结点
if (root.left == null && root.right == null) {
// 输出
StringBuilder sb = new StringBuilder();// StringBuilder用来拼接字符串,速度更快
for (int i = 0; i < paths.size() - 1; i++) {
sb.append(paths.get(i)).append("->");
}
sb.append(paths.get(paths.size() - 1));// 记录最后一个节点
res.add(sb.toString());// 收集一个路径
return;
}
// 递归和回溯是同时进行,所以要放在同一个花括号里
if (root.left != null) { // 左
traversal(root.left, paths, res);
paths.remove(paths.size() - 1);// 回溯
}
if (root.right != null) { // 右
traversal(root.right, paths, res);
paths.remove(paths.size() - 1);// 回溯
}
}
}
总结
以上就是针对这道题的刷题笔记,讲解了怎么用递归以及回溯求解二叉树的所有路径问题,用前序遍历遇到叶子结点时则回溯到上面去重新遍历,还用到了stringbuilder来拼接字符串