回溯算法相关面试手撕题目

119 阅读8分钟

下面是一些常见的回溯算法的题目,

1 二叉树的最大深度

两个思路: 思路 1 进行后序遍历 终止条件: 当 root​ 为空,说明已越过叶节点,因此返回深度 000 。

递推工作: 本质上是对树做后序遍历。

计算节点 root​ 的左子树的深度,即调用 maxDepth (root. Left)。

计算节点 root​ 的右子树的深度,即调用 maxDepth (root. Right)。

返回值: 返回此树的深度,即 max (maxDepth (root. Left), maxDepth (root. Right)) + 1。

思路 2 可以进行层序遍历

特例处理: 当 root​ 为空,直接返回深度 000 。

初始化: 队列 queue (加入根节点 root ),计数器 res = 0。

循环遍历: 当 queue 为空时跳出。

初始化一个空列表 tmp ,用于临时存储下一层节点。

遍历队列: 遍历 queue 中的各节点 node ,并将其左子节点和右子节点加入 tmp。

更新队列: 执行 queue = tmp ,将下一层节点赋值给 queue。

统计层数: 执行 res += 1 ,代表层数加 1。

返回值: 返回 res 即可。

image.png

class Solution {

    public int maxDepth(TreeNode root) {

        //dfs进行遍历,每到一层的 count+1,

       // if(root==null) return 0;

       // return Math.max(maxDepth(root.left),maxDepth(root.right))+1;

       if (root == null) return 0;

        List<TreeNode> queue = new LinkedList<>();

          List<TreeNode> tmp;

            int res = 0;

        queue.add(root);

         while (!queue.isEmpty()) {

            tmp=new LinkedList<>();//用于临时存储下一层节点。

            for(TreeNode node:queue){//将这一层左子节点和右子节点加入 tmp。

                 if (node.left != null)

                 tmp.add(node.left);

                if (node.right != null)

                 tmp.add(node.right);

  

            }

            queue=tmp;

            res++;

  

         }

         return res;

  
  

    }

}

2 路径总和

image.png

思路: 先序遍历: 按照 “根、左、右” 的顺序,遍历树的所有节点。

路径记录: 在先序遍历中,记录从根节点到当前节点的路径。当路径满足 (1) 根节点到叶节点形成的路径且 (2) 各节点值的和等于目标值 targetSum 时,将此路径加入结果列表。

  • 递推参数:root 和 cur 目标值
  • 终止条件:
/**

 * Def启后自动记r 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 targetSum) {

        recur(root,targetSum)

        return res;

  

    }

    void recur(TreeNode root,int tar){

        if(root==null) return ;

        ptah.add(root.val);

        tar=tar-root.val;

        if(tar==0&&root.left==null&&root.right==null){

            res.add(new LinkedList<Integer>(path));//这里需要使用的path的拷贝,因为path会变化的我们需要不好变动的

        }

  
  

        recur(root.left,tar);

        recur(root.right,tar);

        path.removeLast();//向上回溯前需要将当前节点从路径 path 中删除,即执行 path.pop(),这个的意思实际上是将数据

        //当前指针跳到自己的父节点进行剩下的操作

    }

}

3 全排列

image.png 思路: 1.终止条件:当 x len(nums)-1时,代表所有位已固定(最后一位只有1种情况),则将当前组合 nums 转化为数组并加入 res,并返回。 2.递推参数:当前固定位x 3.递推工作:将第×位元素与i E[x,len(nums)]元素分别交换,并进入下层递归。 a.固定元素:将元素nums [i]和nums [x]交换,即固定nums[i】为当前位元素。 b.开启下层递归:调用dfs(× 1),即开始固定第× 1个元素。 C.还原交换:将元素nums [i]和 nums [x]交换(还原之前的交换)。

class Solution {

  

    List<Integer>nums;

    List<List<Integer>> res;

    public List<List<Integer>> permute(int[] nums) {

        //对于长度wein的数组,排列方案一共有n的阶乘个

        //先固定第一位元素,然后第二位,一种到第n位

        //递推参数是固定位x每一次,调整后的列表会一直循环到最后一位。才跳到下一次,从而实现n阶乘次循环。

        this.res=new ArrayList<>();

        this.nums=new ArrayList<>();

        for(int num:nums){

            this.nums.add(num);

        }

        dfs(0);

        return res;

  

    }

  

    void swap(int a,int b){

        //哈希表元素进行交换

        int tmp=nums.get(a);

        nums.set(a,nums.get(b));

        nums.set(b,tmp);

    }

    void dfs(int x){

        if(x==nums.size()-1){

            res.add(new ArrayList<>(nums));// 添加具体的排列方案

            return;//退出

        }

        for(int i=x;i<nums.size();i++){

            swap(i,x);//交换将 nums[i]固定在x位置

            dfs(x+1);//  // 开启固定第 x + 1 位元素

            swap(i,x);//回复交换,这个的部分是回溯的一部分

        }

    }

}

4 全排列 2

image.png

这一题是上一题的改进,实际上实现了一个不重复元素的全排列,在上面的代码对于固定位置 x 进行交换的时候,如果相同的话直接跳出这次交换就可以 下面是其代码,其实就是 for 循环当中,如果是已经存在啦就跳过这次操作就可以啦

class Solution {

     List<Integer> nums;

    List<List<Integer>> res;

  

    void swap(int a,int b){

        int tmp= nums.get(a);

        nums.set(a,nums.get(b));

        nums.set(b,tmp);

    }

    void dfs(int x){

        if(x==nums.size()){

            res.add(new ArrayList<>(nums));

            return;

        }

        HashSet<Integer>set=new HashSet<>();

        for(int i=x;i<nums.size();i++){

            if(set.contains(nums.get(i)))

                continue;

                set.add(nums.get(i));

                swap(i,x);

                dfs(x+1);

                swap(i,x);

        }

    }

    public List<List<Integer>> permuteUnique(int[] nums) {

        this.res=new ArrayList<>();

        this.nums=new ArrayList<>();

        for(int num:nums){

            this.nums.add(num);

        }

        dfs(0);

        return res;

        }

}

5 组合总和

上一题是排列,这一题是组合,数字可以无限选取,其实还是 dfs 的思路

在开启搜索前,先将数组 nums 排序。在遍历所有选择时,当子集和超过 target 时直接结束循环,因为后边的元素更大,其子集和都一定会超过 target 。 省去元素和变量 total,通过在 target 上执行减法来统计元素和,当 target 等于 000 时记录解。

剪枝策略,给定输入数组1,2,…,n,设搜索过程中的选择序列为1,2…,,则该选择序列需要满足 i<<…i,不满足该条件的选择序列都会造成重复,应当剪枝。

思路: image.png

class Solution {

    public List<List<Integer>> combinationSum(int[] candidates, int target) {

        List<Integer>state=new ArrayList<>();//状态子集

        Arrays.sort(candidates);//先对数组进行排序子集和超过了target,直接结束循环

        int start=0;//起始点

        List<List<Integer>> res=new ArrayList<>();//结果列表

        backtrack(state,target,candidates, start,res);

        return res;

  
  

  

    }

    void backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {

        if(target==0){//子集和等于target的时候记录{

            res.add(new ArrayList<>(state));

            return;

        }

        // 遍历所有选择

         for (int i = start; i < choices.length; i++) {

        //for(int i= start;i<choices.length;i++){

            if(target-choices[i]<0){

                //当前组合不满足直接跳出

                break;

            }

            state.add(choices[i]);

            //下一轮旋转

            backtrack(state,target-choices[i],choices,i,res);

            //回退选择,上面的选择选完a选项了,我要开始选b选项了

            state.remove(state.size()-1);

  

        }

  

    }

}

6 总数组合 2

这一题是上一题的改进,还是去重每个数字只能使用一次,因为数组元素与已经排序了所以只要数据在进行处理的时候,如果这个数和下个数相同直接跳过。 image.png


class Solution {

    void backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {

        // 子集和等于 target 时,记录解

        if (target == 0) {

            res.add(new ArrayList<>(state));

            return;

        }

        // 遍历所有选择

        // 剪枝二:从 start 开始遍历,避免生成重复子集

        // 剪枝三:从 start 开始遍历,避免重复选择同一元素

        for (int i = start; i < choices.length; i++) {

            // 剪枝一:若子集和超过 target ,则直接结束循环

            // 这是因为数组已排序,后边元素更大,子集和一定超过 target

            if (target - choices[i] < 0) {

                break;

            }

            // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过

            if (i > start && choices[i] == choices[i - 1]) {

                continue;

            }

            // 尝试:做出选择,更新 target, start

            state.add(choices[i]);

            // 进行下一轮选择

            backtrack(state, target - choices[i], choices, i + 1, res);

            // 回退:撤销选择,恢复到之前的状态

            state.remove(state.size() - 1);

        }

    }

  

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {

        List<Integer> state = new ArrayList<>(); // 状态(子集)

        Arrays.sort(candidates); // 对 candidates 进行排序

        int start = 0; // 遍历起始点

        List<List<Integer>> res = new ArrayList<>(); // 结果列表(子集列表)

        backtrack(state, target, candidates, start, res);

        return res;

    }

}

7 单词搜索

image.png

算法解析: ·递归参数:当前元素在矩阵board中的行列索引i和j,当前目标字符在word中的索引k。 ·终止条件: 1.返回fals:(1)行或列索引越界或(2)当前矩阵元素与目标字符不同或(3)当前矩阵元素已访问过((3)可合并至(2))。 2.返回true:k=len(word)-1,即字符串word已全部匹配。 ·递推工作: 1.标示记当前矩阵元素:将board[i][j】修改为空字符·,代表此元素已访问过,防止之后搜索时重复访问。 2.搜索下一单元格:朝当前元素的上、下、左、右四个方向开启下层递归,使用或连接(代表只需找到一条可行路径就直接返回,不再做后续DFS),并记录结果至rs。 3.还原当前矩阵元素:将board[i][j1元素还原至初始值,即word[k]。 ·返回值:返回布尔量 res,代表是否搜索到旧标字符串。

class Solution {

    public boolean exist(char[][] board, String word) {

        char[] words = word.toCharArray();

        for(int i = 0; i < board.length; i++) {

            for(int j = 0; j < board[0].length; j++) {

                if (dfs(board, words, i, j, 0)) return true;

            }

        }

        return false;

    }

    boolean dfs(char[][] board, char[] word, int i, int j, int k) {

        if (i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]) return false;

        if (k == word.length - 1) return true;//如果长度足够说明已经全部访问了

        board[i][j] = '\0';//标记当前矩阵元素:将当前元素标记为空

        boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) ||

                      dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);

        board[i][j] = word[k];

        return res;

    }

}