算法题解(回溯篇)

回溯算法模板

在这里插入图片描述

77. 组合 - 中等 - 10.20

77. 组合 - 中等

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

在这里插入图片描述在这里插入图片描述


class Solution {
    //保存结果
    List<List<Integer>> result = new ArrayList<>();
    //路径
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combine(int n, int k) {
        combineHelper(n,k,1);
        return result;
    }

    /**
     * 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex
     * @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。
     */    
    private void combineHelper(int n, int k, int startIndex){
        //终止条件 路径长度等于k
        if(path.size() == k){
            result.add(new ArrayList<>(path)); //添加为一个结果
            return;
        }

        //遍历查找 
        //剪枝前:n,击败57%
        //剪枝后:n - (k-path.size()) + 1,击败99%
        for(int i = startIndex; i <= n - (k-path.size()) + 1; i++){
            path.add(i); //路径添加
            combineHelper(n,k,i+1); //回溯调用,从下一个节点开始 i+1
            path.removeLast(); //撤销已处理的节点
        }
    }
}

在这里插入图片描述

216. 组合总和 III - 中等 - 10.20

216. 组合总和 III - 中等

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

  • 所有数字都是正整数。
  • 解集不能包含重复的组合。

在这里插入图片描述


处理过程就是 path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。处理过程 和 回溯过程是一一对应的,处理有加,回溯就要有减!
在这里插入图片描述
剪枝操作

在这里插入图片描述

class Solution {
    //保存结果
    List<List<Integer>> result = new ArrayList<>();
    //路径
    LinkedList<Integer> path = new LinkedList<>(); 

    public List<List<Integer>> combinationSum3(int k, int n) {
        combineHelper(k,n,1,0);
        return result;
    }

    private void combineHelper(int k, int n, int startIndex, int sum){
        //剪枝
        if(sum > n){
            return;
        }

        //终止条件 路径长度等于k
        if(path.size() == k){
            if(sum == n) result.add(new ArrayList<>(path)); //添加为一个结果
            return;
        }

        //遍历查找 
        //剪枝前:9
        //剪枝后:9 - (k-path.size()) + 1
        for(int i = startIndex; i <= 9 - (k - path.size()) + 1; i++){
            path.add(i); //路径添加
            sum += i;
            combineHelper(k,n,i+1,sum); //回溯调用,从下一个节点开始 i+1
            path.removeLast(); //撤销已处理的节点
            sum -= i;
        }
    }   
}

在这里插入图片描述

17. 电话号码的字母组合 - 中等 - 10.21

17. 电话号码的字母组合 - 中等

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

在这里插入图片描述
在这里插入图片描述

class Solution {

    //数字到字符串的映射
    String[] str = {"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    //结果集
    List<String> res = new ArrayList<>();
    //路径 操作字符 
    StringBuilder sb = new StringBuilder();

    public List<String> letterCombinations(String digits) {
        //特殊处理
        if(digits == null || digits.length() == 0) return res;
        //调用回溯函数 
        backtrack(digits,0);
        return res;
    }

    //回溯函数
    void backtrack(String digits,int index){
        //退出条件  如果 输入字符的长度 等于 操作字符的长度 就把操作字符添加进去,然后返回
        if(digits.length() == sb.length()){
            res.add(sb.toString());
            return;
        }
        //获取当前数字对应的字符串
        String val = str[digits.charAt(index) - '2'];
        //遍历
        for(char v: val.toCharArray()){
            sb.append(v); //拼接字符
            backtrack(digits,index+1); //递归调用,index+1是 使用下一个数字对应的字符串          
            //操作完成后删除刚才用过的字母
            sb.deleteCharAt(sb.length()-1);
        }

    }
}

39. 组合总和 - 中等 - 10.22

39. 组合总和 - 中等

给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。

candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。

对于给定的输入,保证和为 target 的唯一组合数少于 150 个。

在这里插入图片描述在这里插入图片描述


方法一:减法(更快)

// 方法一:减法
class Solution {
    List<List<Integer>> res;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        //存放结果
        res = new ArrayList<>();
        //排序
        Arrays.sort(candidates);
        //回溯
        backtrack(candidates,target,new ArrayList<>(),0);

        return res;   
    }

    //回溯
    private void backtrack(int[] candidates,int remains,List<Integer> path,int start){
        //如果做减法后剩下0,则把这条路径添加进来
        if(remains == 0){
            res.add(new ArrayList<>(path));
            return;
        }

        //遍历
        for(int i=start; i<candidates.length; i++){
            //如果剩余值小于0,就返回
            if(remains-candidates[i] < 0) return;
            //剪枝 即去掉相同数的路径 如[2,2,3,7] 去掉第二个2。
            if(i > 0 && candidates[i] == candidates[i-1]) continue;
            //添加元素到路径
            path.add(candidates[i]);
            //回溯
            backtrack(candidates, remains-candidates[i], path, i);
            //找到了一个解或者 remains < 0 了,将当前数字移除,然后继续尝试
            path.remove(path.size()-1);
        }
    }
}

方法二:加法

//方法二:加法
class Solution {
    List<List<Integer>> res;
    List<Integer> path;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        //存放结果
        res = new ArrayList<>();
        //存放路径
        path = new ArrayList<>();
        //排序
        Arrays.sort(candidates);
        //回溯
        backtrack(candidates,target,0,path,0);

        return res;   
    }

    //回溯
    private void backtrack(int[] candidates, int target, int sum, List<Integer> path, int start){
        //如果做加法后等于target,则把这条路径添加进来
        if(sum == target){
            res.add(new ArrayList<>(path));
            return;
        }

        //遍历
        for(int i=start; i<candidates.length; i++){
            //如果累加和大于target,就返回
            if(sum > target) return;
            //剪枝 即去掉相同数的路径 如[2,2,3,7] 去掉第二个2。
            if(i > 0 && candidates[i] == candidates[i-1]) continue;
            //添加元素到路径
            path.add(candidates[i]);
            //累加
            sum += candidates[i];
            //回溯
            backtrack(candidates, target, sum, path, i);
            //找到了一个解或者 remains < 0 了,将当前数字移除,然后继续尝试
            path.remove(path.size()-1);
            //上面加了,这里就要减回去
            sum -= candidates[i];
        }
    }
}

剑指 Offer 12. 矩阵中的路径

剑指 Offer 12. 矩阵中的路径
在这里插入图片描述

在这里插入图片描述


解析:

参考:矩阵中的路径( DFS + 剪枝 ,清晰图解)

在这里插入图片描述

class Solution {
    public boolean exist(char[][] board, String word) {
        //将word转换成字串数组
        char[] words = word.toCharArray();
        //遍历图
        for(int i=0; i<board.length; i++){
            for(int j=0; j<board[0].length; j++){
                //如果找到了,就返回true,否则继续找
                if(dfs(board,words,i,j,0)) return true;
            }
        }
        //遍历结束还没找到返回false
        return false;
    }


    //i,j是元素位置下标,k是传入字符串当前索引
    private boolean dfs(char[][] board, char[] words, int i, int j, int k){
        //判断传入参数的可行性 i 与图行数row比较,j与图列数col比较
        //如果board[i][j] == word[k],则表明当前找到了对应的数,就继续执行(标记找过,继续dfs 下上右左)
        if(i>=board.length || i<0 || j>=board[0].length || j<0 || board[i][j] != words[k]) return false;

        // 表示找完了,每个字符都找到了
        // 一开始k=0,而word.length肯定不是0,所以没找到,就执行dfs继续找。
        if(k == words.length-1) return true;

        // 访问过的标记空字符串,“ ”是空格 '\0'是空字符串,不一样的!
        // 比如当前为A,没有标记找过,且A是word中对应元素,则此时应该找A下一个元素,假设是B,在dfs(B)的时候还是-
        // ->要搜索B左边的元素(假设A在B左边),所以就是ABA(凭空多出一个A,A用了2次,不可以),如果标记为空字符串->
        // 就不会有这样的问题,因为他们值不相等AB != ABA。 
        board[i][j] = '\0';

        //顺序是 下上右左, 上面找到了对应索引的值所以k+1
        boolean res = dfs(board,words,i+1,j,k+1) || dfs(board,words,i-1,j,k+1) || 
                      dfs(board,words,i,j+1,k+1) || dfs(board,words,i,j-1,k+1);
        
        // 还原找过的元素,因为之后可能还会访问到(不同路径)
        board[i][j] = words[k];
        
        // 返回结果,如果false,则if(dfs(board, words, i, j, 0)) return true;不会执行,就会继续找
        return res;
    }
}

在这里插入图片描述

剑指 Offer 13. 机器人的运动范围

剑指 Offer 13. 机器人的运动范围
在这里插入图片描述


解析:DFS

参考:机器人的运动范围( 回溯算法,DFS / BFS ,清晰图解)

在这里插入图片描述

class Solution {
    public int movingCount(int m, int n, int k) {
        boolean[][] visited = new boolean[m][n]; //辅助数组
        return dfs(m,n,k,0,0,visited);
    }

    public int dfs(int m, int n, int k, int i, int j, boolean[][] visited){
        //下标越界 或 行坐标和列坐标的数位之和大于k 或 标志位为false
        if(i>=m || j>=n || k<getNumSum(i)+getNumSum(j) || visited[i][j]){
            return 0;
        }
        //可以到达的格子 设置为true
        visited[i][j] = true;
        //结果 = 1 + 向下可达的格子数 + 向右可达的格子数
        return 1 + dfs(m,n,k,i+1,j,visited) + dfs(m,n,k,i,j+1,visited);
    }

    //数的各个位数之和
    private int getNumSum(int a){
        int sum = a % 10;
        int tmp = a / 10;
        while(tmp > 0){
            sum += tmp % 10;
            tmp /= 10;
        }
        return sum;
    }
}

在这里插入图片描述

剑指 Offer 34. 二叉树中和为某一值的路径

剑指 Offer 34. 二叉树中和为某一值的路径

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

在这里插入图片描述
在这里插入图片描述


解析:回溯

在这里插入图片描述

/**
 * 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 {
    LinkedList<List<Integer>> res = new LinkedList<>(); //结果
    LinkedList<Integer> path = new LinkedList<>(); //路径

    public List<List<Integer>> pathSum(TreeNode root, int target) {
        recall(root,target);
        return res;
    }

    public void recall(TreeNode root, int target){
        if(root == null) return;
        path.add(root.val); //单条路径添加值
        target -= root.val; //做减法
        //如果做减法后结果为0 并且 当前为叶子节点,就添加路径
        if(target == 0 && root.left == null && root.right == null){
            res.add(new LinkedList<>(path)); //添加路径
        }
        //回溯调用,查找左右子树
        recall(root.left, target);
        recall(root.right, target);
        //撤销已处理的节点
        path.removeLast();
    }
}

在这里插入图片描述

剑指 Offer 36. 二叉搜索树与双向链表

剑指 Offer 36. 二叉搜索树与双向链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

为了让您更好地理解问题,以下面的二叉搜索树为例:

在这里插入图片描述
在这里插入图片描述


解析:

在这里插入图片描述
在这里插入图片描述

参考:二叉搜索树与双向链表(中序遍历,清晰图解)

/*
// Definition for a Node.
class Node {
    public int val;
    public Node left;
    public Node right;

    public Node() {}

    public Node(int _val) {
        val = _val;
    }

    public Node(int _val,Node _left,Node _right) {
        val = _val;
        left = _left;
        right = _right;
    }
};
*/
class Solution {
    Node pre, head; //pre为前一个节点,head为头节点
    public Node treeToDoublyList(Node root) {
        if(root == null) return null;
        dfs(root);
        //经过dfs处理之后,pre就有指向了,然后进行头尾相连
        head.left = pre;
        pre.right = head;
        //返回头节点
        return head;
    }

    public void dfs(Node cur){
        //递归结束条件
        if(cur == null) return;

        //左
        dfs(cur.left);

        //如果pre为空,就说明是第一个节点,头结点,然后用head保存头结点,用于之后的返回
        if(pre == null) head = cur;
        //如果pre不为空,那就说明是中间的节点。并且pre保存的是上一个节点,让上一个节点的右指针指向当前节点
        else pre.right = cur;  
        //再让当前节点的左指针指向父节点,也就连成了双向链表
        cur.left = pre;
        //保存当前节点,用于下层递归创建
        pre = cur;

        //右
        dfs(cur.right);
    }
}

在这里插入图片描述

剑指 Offer 54. 二叉搜索树的第k大节点

剑指 Offer 54. 二叉搜索树的第k大节点
在这里插入图片描述


解法一:先使用中序遍历存储元素,然后返回倒数第k大的节点值。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {

    public int kthLargest(TreeNode root, int k) {
        if(root == null) return 0;
        List<Integer> res = new ArrayList<>();
        inOrder(root,res);
        return res.get(res.size()-k); //获取第k大的节点值
    }

    //中序遍历
    public void inOrder(TreeNode root, List<Integer> res){
        if(root == null) return;
        inOrder(root.left,res);
        res.add(root.val);
        inOrder(root.right,res);
    }
}

在这里插入图片描述


优化后:遍历右根左,到倒数第k大值时就直接返回。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {

    private int res = 0, n = 0;

    public int kthLargest(TreeNode root, int k) {
        this.n = k;
        inOrder(root);
        return res;
    }

    public void inOrder(TreeNode root){
        //注意这里是先遍历右子树
        if(root.right != null && n > 0) inOrder(root.right);
        
        n--; //递减
        if(n == 0) { //找到倒数第k大的值
            res = root.val;
            return;
        }

        if(root.left != null && n > 0) inOrder(root.left);
        
    }
}

在这里插入图片描述

*剑指 Offer 55 - I. 二叉树的深度

剑指 Offer 55 - I. 二叉树的深度

在这里插入图片描述


解析:层序遍历

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null) return 0;
        Queue<TreeNode> que = new LinkedList<>();
        que.offer(root);
        int n = 0; //树的深度
        while(!que.isEmpty()){
            n++;
            int len = que.size();
            while(len > 0){
                TreeNode node = que.poll();
                if(node.left != null) que.offer(node.left);
                if(node.right != null) que.offer(node.right);
                len--;
            }          
        }
        return n;
    }
}

在这里插入图片描述
解法二:递归

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null) return 0;
        return Math.max(maxDepth(root.left),maxDepth(root.right)) + 1;
    }
}

在这里插入图片描述

剑指 Offer 55 - II. 平衡二叉树

剑指 Offer 55 - II. 平衡二叉树
在这里插入图片描述


解析:递归计算出左右子树的高度,然后判断是否相差大于1。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public boolean isBalanced(TreeNode root) {
        return dfs(root) >= 0;
    }

    public int dfs(TreeNode root){
        if(root == null) return 0;
        int leftHeight = dfs(root.left); //左子树高度
        int rightHeight = dfs(root.right); //右子树高度
        if(leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1){  //如果左右子树高度为-1,或者两者的差值大于1,就返回-1
            return -1;
        }else{
            return Math.max(leftHeight, rightHeight) + 1; //树的深度
        }
    }
}

在这里插入图片描述

剑指 Offer 64. 求1+2+…+n

剑指 Offer 64. 求1+2+…+n

在这里插入图片描述


解析:递归,短路&&

class Solution {
    public int sumNums(int n) {
        //如果n小于等于0,就不会执行&&后面的语句
        boolean flag = n > 0 && (n += sumNums(n - 1)) > 0;
        return n;
    }
}

在这里插入图片描述

剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

在这里插入图片描述
在这里插入图片描述


解析:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null) return null;
        if(p.val < root.val && q.val < root.val){ //往左搜索
            return lowestCommonAncestor(root.left, p, q);
        }
        if(p.val > root.val && q.val > root.val){ //往右搜索
            return lowestCommonAncestor(root.right, p, q);
        }  
        return root;  //如果出现p,q在一左一右,就直接返回当前的根节点    
    }
}

在这里插入图片描述

剑指 Offer 68 - II. 二叉树的最近公共祖先

剑指 Offer 68 - II. 二叉树的最近公共祖先
在这里插入图片描述
在这里插入图片描述


解析:

三种情况
1、p q 一个在左子树 一个在右子树,那么当前节点即是最近公共祖先。
2、p q 都在左子树
3、p q 都在右子树

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null) return null;
        //如果当前节点等于p或q,返回当前节点
        if(root == p || root == q) return root;
        //在左子树找
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        //在右子树找
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        // p q 一个在左,一个在右
        if(left != null && right != null) return root;
        // p q 都在左子树
        if(left != null) return left;
        // p q 都在右子树
        if(right != null) return right;

        return null;
    }
}

在这里插入图片描述

剑指 Offer 37. 序列化二叉树

剑指 Offer 37. 序列化二叉树

在这里插入图片描述
在这里插入图片描述


解析:

参考:序列化二叉树(层序遍历 BFS ,清晰图解)

在这里插入图片描述

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if(root == null) return "[]";
        StringBuilder res = new StringBuilder("[");  //左大括号
        Queue<TreeNode> que = new LinkedList<>();
        que.add(root);
        while(!que.isEmpty()){
            TreeNode node = que.poll();
            if(node != null){ //如果节点不为空
                res.append(node.val + ","); //添加给节点值
                que.add(node.left); //添加左孩子
                que.add(node.right);  //添加右孩子
            }else{
                res.append("null,"); //节点为空,添加空值
            }
        }
        res.deleteCharAt(res.length()-1); //删除逗号
        res.append("]"); //右大括号
        return res.toString();
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if(data.equals("[]")) return null;
        String[] vals = data.substring(1, data.length()-1).split(","); //根据","分割字符串
        TreeNode root = new TreeNode(Integer.parseInt(vals[0])); //创建根节点
        Queue<TreeNode> que = new LinkedList<>();
        que.add(root);
        int i=1; //下标
        while(!que.isEmpty()){
            TreeNode node = que.poll();
            if(!vals[i].equals("null")){
                node.left = new TreeNode(Integer.parseInt(vals[i])); //构建左孩子
                que.add(node.left); //添加左孩子
            }
            i++; //移动
            if(!vals[i].equals("null")){
                node.right  = new TreeNode(Integer.parseInt(vals[i])); //构建右孩子
                que.add(node.right); //添加右孩子
            }
            i++; //移动
        }
        return root;
    }
}

// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));

在这里插入图片描述

剑指 Offer 38. 字符串的排列

剑指 Offer 38. 字符串的排列

在这里插入图片描述


解析:回溯

class Solution {
    List<String> res;
    StringBuilder path;
    boolean[] visited;
    public String[] permutation(String s) {
        //空值处理
        if(s.equals("")) return new String[]{};
        //初始化
        this.res = new ArrayList<>();
        this.path = new StringBuilder();
        this.visited = new boolean[s.length()];
        //字符串转为字符数组
        char[] ch = s.toCharArray();
        //排序,方便处理重复的字符
        Arrays.sort(ch);
        backtrack(ch,0);
        return res.toArray(new String[0]);

    }

    public void backtrack(char[] ch, int depth){
        if(depth == ch.length){
            res.add(path.toString());
            return;
        }
        //遍历字符数组
        for(int i=0; i<ch.length; i++){
            if(visited[i]) continue; //跳过
            if(i > 0 && ch[i-1] == ch[i] && !visited[i-1]) continue; //重复字串的处理
            path.append(ch[i]); //添加字符
            visited[i] = true; //标记已访问
            backtrack(ch, depth+1); //回溯,深度+1
            visited[i] = false; //重置访问
            path.deleteCharAt(path.length()-1); //删除刚添加的字符,重新尝试
        }
    }
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值