该部分主要通过"N叉树遍历"这一递归中的经典问题,引出回溯并总结回溯模版,需要格外关注的是递归与回溯的关系。此外,回溯模版所能解决的问题也是十分明确的,例如:组合、分割、子集、排列、棋盘等,但是具体问题具体分析,回溯模版也会有灵活的调整。
1.N叉树的遍历
N叉树的遍历在递归模块已经深度剖析,不再过多赘述,直接上代码!
public static void treeDFS(TreeNode node){
//递归出口
if(node == null) return;
//局部枚举
System.out.println(node.val);
for (int i = 1; i <= node.length; i++) {
treeDFS("第i个子节点");
}
}
那么,回溯的模版又是什么样子,它和N叉树的遍历有什么关系呢?相信在给出回溯模版的时候,你已经可以发现N叉树遍历模版和回溯模版的差异了!
public static void backtracking(参数){
//递归出口
if(终止条件){
存放结果;
return;
}
//局部枚举
for (选择本层集合中元素(画成树,就是树节点孩子的大小)) {
处理节点;
backtracking();
回溯,撤销处理结果;
}
}
观察回溯模版和递归模版,我们可以发现回溯模版总共做了三件事情:递归、局部枚举和"放下前任"。关键需要搞清楚,这三个分别指的是什么,递归:递归模版和递归出口;局部枚举:以LeetCode77为例,从集合1,2,3,4中找出所有两个数的组合,枚举就是指第二个数字可以枚举2、3、4;"放下前任":path数组维护了当前所走的路径,因此在枚举当前元素后,以递归的形式返回前,应先将当前枚举的元素移除path数组(就好比开始新化学实验之前,需要清理仪器设备,不能让前面的实验杂质干扰影响正在进行的实验)。
理解清楚回溯模版后,我们再以二叉树的路径问题来巩固回溯模版!
2.回溯热身——再论二叉树路径问题
2.1输出二叉树的所有路径
题目见LeetCode257。
题目分析:为了使用回溯模版,需要搞清楚三个问题:第一,递归出口:当root为空退出dfs;第二,局部枚举:维护一个path数组,先将当前枚举的节点加入path数组,然后再枚举左右孩子节点,若当钱枚举的节点如果是叶子结点,将path数组加入ans数组中;第三,"处理前任":枚举完当前节点和左右孩子节点后,需要将当前节点移除出path数组。
厘清思路,直接上代码!
需要注意的是,在LeetCode中不要使用定义在类属性中的静态变量,即去掉static,不然会有脏数据,其原理为:静态变量在LeetCode类加载时只初始化一次,后面再次输入实例的时候就会出现脏数据!
public class MyBinaryTreePaths {
static List<String> ans = new ArrayList<>();
public static void main(String[] args) {
BinaryTree bTree = new BinaryTree();
bTree.root = bTree.buildBinaryTree();
List<String> result = binaryTreePaths(bTree.root);
System.out.println(result);
}
public static List<String> binaryTreePaths(TreeNode root) {
dfs(root, new ArrayList<>());
return ans;
}
public static void dfs(TreeNode p, ArrayList<Integer> path){
//递归出口
if(p == null) return;
//处理当前节点
path.add(p.val);
if(p.left == null && p.right == null) ans.add(getPathString(path));
//局部枚举
dfs(p.left, path);
dfs(p.right, path);
//放下前任
path.remove(path.size() - 1);
}
public static String getPathString(ArrayList<Integer> path){
StringBuilder buf = new StringBuilder();
buf.append(path.get(0));
for (int i = 1; i < path.size(); i++) {
buf.append("->");
buf.append(path.get(i));
}
return buf.toString();
}
}
2.2路径总和问题
题目见LeetCode113,描述为:输出树的路径和targetSum的路径。
题目分析:如果跟节点的值为val,那么只需要在左右子树中找路径和为targetSum-val的路径即可!因此,该问题就是一个递归问题,递归出口:root为空则结束递归。紧接着我们需要局部枚举:局部枚举思路与上一道题目大致相同,此外我们还需要维护一个不断更新的targetSum来记录当前需要在子树寻找路径和为多少的路径,当且仅当枚举到叶子结点且targetSum为0时,需要将path加入到ans中;"处理前任":策略与上一个题目相同,不再赘述,此时无需再次回溯targetSum,因为他是值传递且作为局部变量。
厘清思路,直接上代码!
public class HasPathSum {
static List<List<Integer>> ret = new LinkedList<List<Integer>>();
static Deque<Integer> path = new LinkedList<Integer>();
public static List<List<Integer>> pathSum(TreeNode root, int targetSum) {
myDfs(root, targetSum);
return ret;
}
public static void myDfs(TreeNode root, int targetSum) {
//递归出口
if(root == null) return;
//处理当前节点
targetSum -= root.val;
path.offer(root.val);
if(root.right == null && root.left == null && targetSum == 0){
ret.add(new ArrayList<Integer>(path));
}
//局部枚举
myDfs(root.left, targetSum);
myDfs(root.right, targetSum);
//放弃前任
path.pollLast();
}
}
OK,《算法通关村第十八关——回溯青铜挑战笔记》结束,喜欢的朋友三联加关注!关注鱼市带给你不一样的算法小感悟!(幻听)
再次,感谢鱼骨头教官的学习路线!鱼皮的宣传!小y的陪伴!ok,拜拜,第十八关第二幕见!