Leetcode_DFS、BFS

目标和

1 题目描述

给你一个整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :

  • 例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

示例 2

输入:nums = [1], target = 1
输出:1

提示

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000

2 解题(Java)

树的后序遍历+记忆化搜索

class Solution {
    Map<String,Integer> cache = new HashMap<>();
    int target;
    int[] nums;
    public int findTargetSumWays(int[] nums, int target) {
        this.target = target;
        this.nums = nums;
        return dfs(0, nums[0]) + dfs(0, -nums[0]);
    }

    int dfs(int index, int sum) {
        String key = index + "_" + sum;
        if (cache.containsKey(key)) {
            return cache.get(key);
        }
        if (index == nums.length-1) {
            cache.put(key, sum == target ? 1 : 0);
            return cache.get(key);
        }
        int left = dfs(index + 1, sum + nums[index+1]);
        int right = dfs(index + 1, sum - nums[index+1]);
        cache.put(key, left + right);
        return cache.get(key);
    }
}

3 复杂性分析

在这里插入图片描述

路径总和 III

1 题目描述

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

示例 1

输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3

示例 2

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3

提示:

  • 二叉树的节点个数的范围是 [0,1000]
  • -109 <= Node.val <= 109
  • -1000 <= targetSum <= 1000

2 解题(Java)

前缀和+回溯法(前序遍历):

/**
 * 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;
 *     }
 * }
 */
/**
 * 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 {
    //key为从根节点到当前节点的前缀和,value为此前缀和的数目
    Map<Integer, Integer> dic = new HashMap<>();
    int count;
    int sum;
    public int pathSum(TreeNode root, int sum) {
        this.sum = sum;
        //初始化:空节点,前缀和为0,个数为1
        dic.put(0, 1);
        recur(root, 0);
        return count;
    }

    private void recur(TreeNode node, int preSum) {
        if(node == null) return;
        preSum += node.val;
        count += dic.getOrDefault(preSum - sum, 0);
        dic.put(preSum, dic.getOrDefault(preSum, 0) + 1);
        recur(node.left, preSum);
        recur(node.right, preSum);
        //回溯到父节点前复原前缀和的数目
        dic.put(preSum, dic.get(preSum) - 1);
    }
}

3 复杂性分析

  • 时间复杂度:O(N),每个节点都遍历一次;
  • 空间复杂度:O(N),prefixMap占用O(N)空间;

除法求值*

1 题目描述

给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。

另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。

返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。

注意:输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。

示例 1

输入:equations = [[“a”,“b”],[“b”,“c”]], values = [2.0,3.0], queries =
[[“a”,“c”],[“b”,“a”],[“a”,“e”],[“a”,“a”],[“x”,“x”]]
输出:[6.00000,0.50000,-1.00000,1.00000,-1.00000]
解释:
条件:a / b = 2.0, b / c = 3.0
问题:a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?
结果:[6.0, 0.5, -1.0, 1.0, -1.0 ]

示例 2

输入:equations = [[“a”,“b”],[“b”,“c”],[“bc”,“cd”]], values = [1.5,2.5,5.0], queries = [[“a”,“c”],[“c”,“b”],[“bc”,“cd”],[“cd”,“bc”]]
输出:[3.75000,0.40000,5.00000,0.20000]

示例 3

输入:equations = [[“a”,“b”]], values = [0.5], queries = [[“a”,“b”],[“b”,“a”],[“a”,“c”],[“x”,“y”]]
输出:[0.50000,2.00000,-1.00000,-1.00000]

提示

  • 1 <= equations.length <= 20
  • equations[i].length == 2
  • 1 <= Ai.length, Bi.length <= 5
  • values.length == equations.length
  • 0.0 < values[i] <= 20.0
  • 1 <= queries.length <= 20
  • queries[i].length == 2
  • 1 <= Cj.length, Dj.length <= 5
  • Ai, Bi, Cj, Dj 由小写英文字母与数字组成

2 解题(Java)

图论:广度优先搜索

class Solution {
    public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
        // 1通过哈希表将每个不同的字符串映射成整数
        int var_count = 0;
        Map<String, Integer> dic = new HashMap<>();
        for (int i=0; i<equations.size(); i++) {
            if (!dic.containsKey(equations.get(i).get(0))) {
                dic.put(equations.get(i).get(0), var_count++);
            }
            if (!dic.containsKey(equations.get(i).get(1))) {
                dic.put(equations.get(i).get(1), var_count++);
            }
        }

        // 2构建图:对于每个点,存储其直接连接到的所有点及对应的权值(两点的比值)
        List<Pair>[] edges = new List[var_count];
        for (int i=0; i<var_count; i++) {
            edges[i] = new ArrayList<>();
        }
        for (int i=0; i<equations.size(); i++) {
            int x = dic.get(equations.get(i).get(0)), y = dic.get(equations.get(i).get(1));
            edges[x].add(new Pair(y, values[i]));
            edges[y].add(new Pair(x, 1.0/values[i]));
        }

        // 3对任何一个查询,从起点出发,通过广度优先搜索的方式,不断更新起点和当前点的路径长度,直到搜索到终点为止
        double[] res = new double[queries.size()];
        for (int i=0; i<queries.size(); i++) {
            List<String> query = queries.get(i);
            double result = -1.0;
            if (dic.containsKey(query.get(0)) && dic.containsKey(query.get(1))) {
                int x = dic.get(query.get(0)), y = dic.get(query.get(1));
                if (x == y) result = 1.0;
                else {
                    Deque<Integer> pointsQueue = new LinkedList<>();
                    pointsQueue.offer(x);
                    double[] ratios = new double[var_count];
                    Arrays.fill(ratios, -1.0);
                    ratios[x] = 1.0;

                    while (!pointsQueue.isEmpty() && ratios[y] < 0) {
                        int a = pointsQueue.poll();
                        for (Pair pair : edges[a]) {
                            int b = pair.index;
                            if (ratios[b] > 0) continue;
                            ratios[b] = ratios[a] * pair.value;
                            pointsQueue.offer(b);
                        }
                    }
                    result = ratios[y];
                }
            }
            res[i] = result;
        }
        return res;
    }
    class Pair {
        int index;
        double value;
        Pair(int index, double value) {
            this.index = index;
            this.value = value;
        }
    }
}

3 复杂性分析

  • 时间复杂度:O(ML+Q⋅(L+M)),其中 M 为边的数量,Q 为询问的数量,L 为字符串的平均长度。构建图时,需要处理 M条边,每条边都涉及到平均长度为L的字符串比较;处理查询时,每次查询首先要进行一次 O(L) 的比较,然后至多遍历 O(M) 条边;
  • 空间复杂度:O(NL+M),其中 N 为点的数量,M 为边的数量,L 为字符串的平均长度。为了将每个字符串映射到整数,需要开辟空间为O(NL) 的哈希表;随后,需要花费 O(M) 的空间存储每条边的权重;处理查询时,还需要 O(N)的空间维护访问队列。最终,总的复杂度为 O(NL+M+N)=O(NL+M);

删除无效的括号

1 题目描述

给你一个由若干括号和字母组成的字符串 s ,删除最小数量的无效括号,使得输入的字符串有效。

返回所有可能的结果。答案可以按 任意顺序 返回。

示例 1:

输入:s = “()())()”
输出:[“(())()”,“()()()”]

示例 2:

输入:s = “(a)())()”
输出:[“(a())()”,“(a)()()”]

示例 3:

输入:s = “)(”
输出:[“”]

提示:

  • 1 <= s.length <= 25
  • s 由小写英文字母以及括号 ‘(’ 和 ‘)’ 组成
  • s 中至多含 20 个括号

2 解题(Java)

class Solution {
    private int len;
    private char[] charArray;
    private Set<String> res = new HashSet<>();
    StringBuilder ans = new StringBuilder();
    public List<String> removeInvalidParentheses(String s) {
        this.len = s.length();
        this.charArray = s.toCharArray();
        
        // 计算多余的左右括号,左右括号都可能有多余
        int leftRemove = 0;
        int rightRemove = 0;
        for (int i = 0; i < len; i++) {
            if (charArray[i] == '(') {
                leftRemove++;
            } else if (charArray[i] == ')') {
                if (leftRemove == 0) {
                    rightRemove++;
                }
                if (leftRemove > 0) {
                    leftRemove--;
                }
            }
        }

        //回溯算法,尝试每一种可能的删除操作
        dfs(0, 0, 0, leftRemove, rightRemove);
        return new ArrayList<>(res);
    }

    /**
     * @param index       当前遍历到的下标
     * @param leftCount   已经遍历到的左括号的个数
     * @param rightCount  已经遍历到的右括号的个数
     * @param leftRemove  应该删除的左括号的个数
     * @param rightRemove 应该删除的右括号的个数
     */
    private void dfs(int index, int leftCount, int rightCount, int leftRemove, int rightRemove) {
        if (index == len) {
            if (leftRemove == 0 && rightRemove == 0) {
                res.add(ans.toString());
            }
            return;
        }

        char cur = charArray[index];
        // 可能的操作1:删除当前遍历到的字符
        if (cur == '(' && leftRemove > 0) {
            // 由于leftRemove > 0,并且当前遇到的是左括号,因此可以尝试删除当前遇到的左括号
            dfs(index + 1, leftCount, rightCount, leftRemove - 1, rightRemove);
        }
        if (cur == ')' && rightRemove > 0) {
            // 由于rightRemove > 0,并且当前遇到的是右括号,因此可以尝试删除当前遇到的右括号
            dfs(index + 1, leftCount, rightCount, leftRemove, rightRemove - 1);
        }

        // 可能的操作 2:保留当前遍历到的字符
        ans.append(cur);
        if (cur != '(' && cur != ')') {
            // 如果不是括号,继续深度优先遍历
            dfs(index + 1, leftCount, rightCount, leftRemove, rightRemove);
        } else if (cur == '(') {
            // 考虑左括号
            dfs(index + 1, leftCount + 1, rightCount, leftRemove, rightRemove);
        } else if (cur == ')' && rightCount < leftCount) {
            // 考虑右括号
            dfs(index + 1, leftCount, rightCount + 1, leftRemove, rightRemove);
        }
        ans.deleteCharAt(ans.length() - 1);
    }
}

课程表

1 题目描述

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例 1

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

提示

  • 1 <= numCourses <= 105
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • prerequisites[i] 中的所有课程对 互不相同

2 解题(Java)

2.1 解题思路

拓扑排序

  1. 借助一个标志列表 flags,用于判断每个节点 i (课程)的状态:
    1. 未被 DFS 访问:i == 0;
    2. 当前节点完成DFS 访问:i == -1;
    3. 已被当前节点启动的 DFS 访问:i == 1。
  2. 对 numCourses 个节点依次执行 DFS,判断每个节点起步 DFS 是否存在环,若存在环直接返回 False。
  3. DFS 流程:
    1. 终止条件:
      • 当 flag[i] == -1,说明当前访问节点已完成 DFS 访问,无需再重复搜索,直接返回 True;
      • 当 flag[i] == 1,说明在本轮 DFS 搜索中节点 i 被第 2 次访问,即 课程安排图有环 ,直接返回 False;
    2. 将当前访问节点 i 对应 flag[i] 置 1,即标记其被本轮 DFS 访问过;
    3. 递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回 False;
    4. 当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 −1 并返回 True。
  4. 若整个图 DFS 结束并未发现环,返回 True。

2.2 代码

class Solution {
    List<List<Integer>> adjacency = new ArrayList<>();
    int[] flags;
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        for (int i=0; i<numCourses; i++) {
            adjacency.add(new ArrayList<>());
        }
        flags = new int[numCourses];
        for (int[] cp : prerequisites) {
            adjacency.get(cp[1]).add(cp[0]);
        }
        for (int i=0; i<numCourses; i++) {
            if (!dfs(i)) return false;
        }
        return true;
    }
    private boolean dfs(int i) {
        if (flags[i] == 1) return false;
        if (flags[i] == -1) return true;
        flags[i] = 1;
        for (Integer j : adjacency.get(i)) {
            if (!dfs(j)) return false;
        }
        flags[i] = -1;
        return true;
    }
}

3 复杂性分析

  • 时间复杂度 O(N+M): 遍历一个图需要访问所有节点和所有临边,N 和 M 分别为节点数量和临边数量;
  • 空间复杂度 O(N+M): 为建立邻接表所需额外空间,adjacency 长度为 N ,并存储 M 条临边的数据;

岛屿数量

1 题目描述

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1

示例 2

输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

提示

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 300
  • grid[i][j] 的值为 ‘0’ 或 ‘1’

2 解题(Java)

class Solution {
    boolean[][] marked;
    char[][] grid;
    int res = 0;
    int m, n;
    public int numIslands(char[][] grid) {
        this.grid = grid;
        m = grid.length;
        n = grid[0].length;
        marked = new boolean[m][n];
        for (int i=0; i<m; i++) {
            for (int j=0; j<n; j++) {
                if (grid[i][j] == '1' && marked[i][j] == false) {
                    res++;
                    dfs(i, j);
                }
            }
        }
        return res;
    }
    void dfs(int i, int j) {
        marked[i][j] = true;
        if (i+1 < m && grid[i+1][j] == '1' && marked[i+1][j] == false) dfs(i+1, j);
        if (i-1 >= 0 && grid[i-1][j] == '1' && marked[i-1][j] == false) dfs(i-1, j);
        if (j+1 < n && grid[i][j+1] == '1' && marked[i][j+1] == false) dfs(i, j+1);
        if (j-1 >= 0 && grid[i][j-1] == '1' && marked[i][j-1] == false) dfs(i, j-1);
    }
}

3 复杂性分析

  • 时间复杂度O(MN)
  • 空间复杂度O(MN):在最坏情况下,整个网格均为陆地,深度优先搜索的深度达到MN;

机器人的运动范围

1 题目描述

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

示例 1:

输入:m = 2, n = 3, k = 1
输出:3

示例 2:

输入:m = 3, n = 1, k = 0
输出:1

2 解题(Java)

广度优先搜索BFS:

class Solution {
    public int movingCount(int m, int n, int k) {
        if (m <= 0 || n <= 0 || k < 0) return 0;
        Deque<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{0, 0});
        boolean[][] vis = new boolean[m][n];
        vis[0][0] = true;
        int ans = 1;
        while (!queue.isEmpty()) {
            int[] cell = queue.poll();
            //向下移动
            int x = cell[0] + 1, y = cell[1];
            if (x < m && !vis[x][y] && get(x) + get(y) <= k) {
                queue.offer(new int[]{x, y});
                vis[x][y] = true;
                ans++;
            }
            //向右移动
            x = cell[0]; y = cell[1] + 1;
            if (y < n && !vis[x][y] && get(x) + get(y) <= k) {
                queue.offer(new int[]{x, y});
                vis[x][y] = true;
                ans++;
            }
        }
        return ans;
    }
    public int get(int n) {
        int res = 0;
        while (n != 0) {
            res += n % 10;
            n /= 10;
        }
        return res;
    }
}

3 复杂性分析

  • 时间复杂度O(MN):其中 M 为矩阵的行数,N 为矩阵的列数,最差情况下,机器人遍历矩阵所有单元格,此时时间复杂度为O(MN) ;
  • 空间复杂度O(MN):需要一个大小为 O(MN) 的标记结构来标记每个格子是否已经走过;

子集

1 题目描述

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2

输入:nums = [0]
输出:[[],[0]]

提示

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

2 解题(Java)

2.1 解法1(回溯)

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> tmp = new ArrayList<>();
    int[] nums;
    public List<List<Integer>> subsets(int[] nums) {
        this.nums = nums;
        backTrack(0);
        return res;
    }
    void backTrack(int x) {
        res.add(new ArrayList(tmp));
        for (int i = x; i < nums.length; i++) {
            tmp.add(nums[i]);
            backTrack(i+1);
            tmp.remove(tmp.size() - 1);
        }
    }
}
复杂性分析

时间复杂度O(n * 2 ^ n):一共 2 ^ n 个状态,每个状态使用 O(n) 的时间复制到答案列表中;
空间复杂度O(n):临时列表 temp 的空间代价是 O(n),递归时栈空间的代价为 O(n);

2.2 解法2(位运算)

解题思路

  1. 记原序列中元素的总数为 n。原序列中的每个数字 a_i的状态可能有两种,即「在子集中」和「不在子集中」;
  2. 我们用 1 表示「在子集中」,0 表示「不在子集中」,那么每一个子集可以对应一个长度为 n 的 0/1 序列,第 i 位表示 a_i 是否在子集中。例如,n = 3,a = {5,2,9} 时:

在这里插入图片描述

  1. 可以发现 0/1 序列对应的二进制数正好从 0 到 2 ^ n - 1 。因此可以枚举mask∈[0, 2 ^ n - 1],mask 的二进制表示是一个 0/1 序列,我们可以按照这个 0/1 序列在原集合当中取数。

代码

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> temp = new ArrayList<>();
        int n = nums.length;
        for (int mask = 0; mask < (1 << n); mask++) {
            temp.clear();
            for (int i=0; i<n; i++) {
                if ((mask & (1 << i)) != 0) {
                    temp.add(nums[i]);
                }
            }
            res.add(new ArrayList(temp));
        }
        return res;
    }
}
复杂性分析

时间复杂度O(n * 2 ^ n):一共 2 ^ n 个状态,每个状态需要 O(n) 的时间来构造子集;
空间复杂度O(n):临时列表 temp 的空间代价是 O(n);

全排列

1 题目描述

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

2 解题(Java)

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> ans = new ArrayList<>();
    int[] nums;
    public List<List<Integer>> permute(int[] nums) {
        this.nums = nums;
        for (int num : nums) {
            ans.add(num);
        }
        backTrack(0);
        return res;
    }
    public void backTrack(int x) {
        if (x == nums.length) {
            res.add(new ArrayList<>(ans));
        } else {
            for (int i = x; i < nums.length; i++) {
                Collections.swap(ans, x, i);
                backTrack(x + 1);
                Collections.swap(ans, x, i);
            }
        }
    }
}

3 复杂性分析

  • 时间复杂度O(N*N!):N为数组的长度,时间复杂度取决于方案数,方案数为O(N!),每一个答案使用 O(N) 的时间复制到答案列表中,因此时间复杂度为O(N*N!);
  • 空间复杂度O(N):空间复杂度取决于栈的深度,因此为O(N);

组合总和

1 题目描述

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

candidates 中的数字可以无限制重复被选取。

说明

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

示例 1

输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]

示例 2

输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]

提示

  • 1 <= candidates.length <= 30
  • 1 <= candidates[i] <= 200
  • candidate中的每个元素都是独一无二的
  • 1 <= target <= 500

2 解题(Java)

解题思路

  1. 对于这类寻找所有可行解的问题,一般用搜索回溯的算法来解决;
  2. 定义递归函数dfs(target,index),表示当前在candidates数组的第index位,还剩target要组合,已经组合的列表为ans,递归的终止条件为target<=0或index==candidates.length;
  3. 在dfs函数中,我们可以选择使用第index个数,即执行dfs(target - candidates[index], index);也可以选择跳过不用第index个数,即执行dfs(target, index + 1);
  4. 将搜索过程用一棵树来表达,每次搜索都会延伸出两个分支,直到递归的终止条件,这样就能不遗漏且不重复地找到所有可行解;

在这里插入图片描述

代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> ans = new ArrayList<>();
    int[] candidates;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        this.candidates = candidates;
        dfs(target, 0);
        return res;
    }

    public void dfs(int target, int index) {
        if (target < 0 || index >= candidates.length) {
            return;
        }
        if (target == 0) {
            res.add(new ArrayList<>(ans));
            return;
        }
        // 选择当前下标对应的数
        ans.add(candidates[index]);
        dfs(target - candidates[index], index);
        ans.remove(ans.size() - 1);
        // 跳过当前下标对应的数
        dfs(target, index + 1);
    }
}

3 复杂性分析

  • 时间复杂度O(N * 2 ^ N):O(N * 2 ^ N)是一个比较松的上界,但实际运行中,由于使用target <= 0进行了剪枝,因此实际运行情况往往远远小于这个上界;
  • 空间复杂度O(target):空间复杂度取决于递归的深度,最差情况下需要递归target层;

括号生成

1 题目描述

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1

输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]

示例 2

输入:n = 1
输出:[“()”]

提示

1 <= n <= 8

2 解题(Java)

  1. 通过回溯法遍历各种组合;
  2. 剪枝(减少递归次数同时避免了判断有效括号):如果左括号数量不大于n,才可以放一个左括号;如果右括号数量小于左括号数量,才可以放一个右括号;
class Solution {
    List<String> res = new ArrayList<>();
    StringBuilder ans = new StringBuilder();
    int max;
    public List<String> generateParenthesis(int n) {
        max = n;
        backtrack(0, 0);
        return res;
    }

    public void backtrack(int open, int close) {
        if (ans.length() == max * 2) {
            res.add(ans.toString());
            return;
        }
        if (open < max) {
            ans.append('(');
            backtrack(open + 1, close);
            ans.deleteCharAt(ans.length() - 1);
        }
        if (close < open) {
            ans.append(')');
            backtrack(open, close + 1);
            ans.deleteCharAt(ans.length() - 1);
        }
    }
}

3 复杂性分析

复杂度依赖于最终形成多少个答案,官方解释由(4 ^ n)/(n ^ (3/2))渐进界定。

时间复杂度O((4 ^ N)/(N ^ (1/2)):在回溯过程中,每个答案需要O(N)的时间形成;
空间复杂度O(N):除答案数组外,空间复杂度取决于递归栈的深度,递归2N层;

打印从1到最大的n位数

1 题目描述

输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。

示例 1:

输入: n = 1
输出: 1,2,3,4,5,6,7,8,9

说明

n 为正整数

2 解题(Java)

2.1 解题思路

本题的主要考点是大数越界情况下的打印。需要解决以下三个问题:

  1. 表示大数的变量类型:无论是 short / int / long … 任意变量类型,数字的取值范围都是有限的。因此,大数的表示应用 String 类型;
  2. 生成数字的字符串集:使用 int 类型时,每轮可通过 +1 生成下个数字,而此方法无法应用至 String 类型。并且, String 类型的数字的进位操作效率较低,例如 “9999” 至 “10000” 需要从个位到千位循环判断,进位 4 次。观察可知,生成的列表实际上是 n 位 0 - 9 的 全排列 ,因此可避开进位操作,通过递归生成数字的 String 列表;
  3. 递归生成全排列:基于分治算法的思想,先固定高位,向低位递归,当个位已被固定时,添加数字的字符串。例如当 n=2 时(数字范围 1 - 99 ),固定十位为 0 - 9 ,按顺序依次开启递归,固定个位 0 - 9 ,终止递归并添加数字字符串;

在这里插入图片描述
根据以上方法,可初步编写全排列代码:

class Solution {
    StringBuilder res;
    int n;
    char[] num, loop = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
    public String printNumbers(int n) {
        this.n = n;
        res = new StringBuilder(); // 数字字符串集
        num = new char[n]; // 定义长度为 n 的字符列表
        dfs(0); // 开启全排列递归
        res.deleteCharAt(res.length() - 1); // 删除最后多余的逗号
        return res.toString(); // 转化为字符串并返回
    }
    void dfs(int digit) {
        if(digit == n) { // 终止条件:已固定完所有位
            res.append(String.valueOf(num) + ","); // 拼接 num 并添加至 res 尾部,使用逗号隔开
        }
        for(char i : loop) { // 遍历 ‘0‘ - ’9‘
            num[digit] = i; // 固定第 x 位为 i
            dfs(digit + 1); // 开启固定第 x + 1 位
        }
    }
}

在此方法下,各数字字符串被逗号隔开,共同组成长字符串。返回的数字集字符串如下所示:

输入:n = 1
输出:"0,1,2,3,4,5,6,7,8,9"

输入:n = 2
输出:"00,01,02,...,10,11,12,...,97,98,99"

输入:n = 3
输出:"000,001,002,...,100,101,102,...,997,998,999"

观察可知,当前的生成方法仍有以下问题:

  1. 诸如 00,01,02,⋯ 应显示为 0,1,2,⋯ ,即应删除高位多余的 0 ;
  2. 此方法从 0 开始生成,而题目要求 列表从 1 开始 ;

以上两个问题的解决方法如下:

1 删除高位多余的 0

  1. 字符串左边界定义: 声明变量 start 规定字符串的左边界,以保证添加的数字字符串 num[start:] 中无高位多余的 0 。例如当n=2时,1−9 时 start = 1,10−99 时 start=0 ;
  2. 左边界 start 变化规律: 观察可知,当输出数字的所有位都是 9 时,则下个数字需要向更高位进 1 ,此时左边界 start需要减 1 (即高位多余的 0 减少一个)。例如当 n=3 (数字范围 1−999 )时,左边界 start 需要减 1 的情况有: “009” 进位至 “010” , “099” 进位至 “100” 。设数字各位中 9 的数量为 count_9 ,所有位都为 9 的判断条件可用以下公式表示:n - start == count_9;
  3. 统计 nine 的方法:固定第 x 位时,当 i = 9 则执行 count_9 = count_9 + 1 ,并在回溯前恢复 count_9 = count_9 - 1;

2 列表从 1 开始

在以上方法的基础上,添加数字字符串前判断其是否为 “0” ,若为 “0” 则直接跳过。

2.3 代码

class Solution {
    StringBuilder res;
    int n, start, count_9 = 0;
    char[] num, loop = {'0','1','2','3','4','5','6','7','8','9'};
    public String printNumbers(int n) {
        this.n = n;
        res = new StringBuilder();
        num = new char[n];
        start = n - 1;
        dfs(0);
        res.deleteCharAt(res.length() - 1);
        return res.toString();
    }
    void dfs(int digit) {
        if (digit == n) {
            String s = String.valueOf(num).substring(start);
            if(!s.equals("0")) res.append(s + ",");
            if (n - start == count_9) start--;
            return;
        }
        for (char i : loop) {
            if (i == '9') count_9++;
            num[digit] = i;
            dfs(digit + 1);
        }
        count_9--;
    }
}

3 复杂性分析

  • 时间复杂度 O(10 ^ n): 递归生成的排列的数量为 10^n;
  • 空间复杂度 O(10 ^ n): 结果列表 res 的长度为 10 ^ n - 1,各数字字符串的长度区间为 1, 2, …, n,因此占用 O(10^n)大小的额外空间;

矩阵中的路径

1 题目描述

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。

[[“a”,“b”,“c”,“e”],
[“s”,“f”,“c”,“s”],
[“a”,“d”,“e”,“e”]]

但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。

示例 1:

输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true

示例 2:

输入:board = [[“a”,“b”],[“c”,“d”]], word = “abcd”
输出:false

2 解题(Java)

2.1 解题思路

典型的矩阵搜索问题,可使用 深度优先搜索(DFS)+ 剪枝 来解决。

  • 深度优先搜索: 可以理解为暴力法遍历矩阵中所有字符串可能性。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推;
  • 剪枝: 在搜索中,遇到这条路不可能和目标字符串匹配成功的情况(例如:此矩阵元素和目标字符不同或此元素已被访问),则应立即返回,称之为可行性剪枝 ;

在这里插入图片描述
DFS 解析:

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

2.2 Java代码

class Solution {
    char[][] board;
    char[] words;
    public boolean exist(char[][] board, String word) {
        this.board = board;
        this.words = word.toCharArray();
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[0].length; j++) {
                if (dfs(i, j, 0)) return true;
            }
        }
        return false;
    }
    public boolean dfs(int i, int j, int k) {
        if (i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != words[k]) {
            return false;
        }
        if (k == words.length - 1) return true;
        board[i][j] = '\0';
        boolean res = dfs(i + 1, j, k + 1) || dfs(i - 1, j, k + 1) || dfs(i, j + 1, k + 1) || dfs(i, j - 1, k + 1);
        board[i][j] = words[k];
        return res;
    }
}

3 复杂性分析

M,N 分别为矩阵行列大小,K 为字符串 word 长度。

  • 时间复杂度O(3 ^ K * MN): 最差情况下,需要遍历矩阵中长度为 K 字符串的所有方案,时间复杂度为 O(3^K);矩阵中共有 MN 个起点,时间复杂度为 O(MN) :

    • 方案数计算: 设字符串长度为 K ,搜索中每个字符有上、下、左、右四个方向可以选择,舍弃回头(上个字符)的方向,剩下 3 种选择,因此方案数的复杂度为 O(3^K)。
  • 空间复杂度 O(K) : 搜索过程中的递归深度不超过 K ,因此系统因函数调用累计使用的栈空间占用 O(K)(因为函数返回后,系统调用的栈空间会释放)。

字符串的排列

1 题目描述

输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:

输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]

限制:

1 <= s 的长度 <= 8

2 解题(Java)

2.1 解题思路

  1. 排列方案数量: 对于一个长度为 n 的字符串(假设字符互不重复),其排列共有 n×(n−1)×(n−2)…×2×1 种方案;
  2. 排列方案的生成方法: 根据字符串排列的特点,考虑深度优先搜索所有排列方案。即通过字符交换,先固定第 1 位字符( n 种情况)、再固定第 2 位字符( n-1 种情况)、… 、最后固定第 n 位字符( 1 种情况);

在这里插入图片描述
3. 重复方案与剪枝: 当字符串存在重复字符时,排列方案中也存在重复方案。为排除重复方案,需在固定某位字符时,保证 “每种字符只在此位固定一次” ,即遇到重复字符时不交换,直接跳过。从 DFS 角度看,此操作称为 “剪枝” ;

在这里插入图片描述

2.2 递归解析

  1. 终止条件: 当 x = len© - 1 时,代表所有位已固定(最后一位只有 1 种情况),则将当前组合 c 转化为字符串并加入 res,并返回;
  2. 递推参数: 当前固定位 x ;
  3. 递推工作: 初始化一个 Set ,用于排除重复的字符;将第 x 位字符与i∈[x,len©] 字符分别交换,并进入下层递归:
    • 剪枝: 若 c[i] 在 Set​ 中,代表其是重复字符,因此“剪枝”;
    • 将 c[i] 加入 Set​ ,以便之后遇到重复字符时剪枝;
    • 固定字符: 将字符 c[i] 和 c[x] 交换,即固定 c[i] 为当前位字符;
    • 开启下层递归: 调用 dfs(x + 1) ,即开始固定第 x + 1 个字符;
    • 还原交换: 将字符 c[i] 和 c[x] 交换(还原之前的交换,如果不还原会丢失方案);

2.3 代码

class Solution {
    List<String> res = new LinkedList<>();
    char[] c;
    public String[] permutation(String s) {
        c = s.toCharArray();
        dfs(0);
        return res.toArray(new String[]{});
    }
    void dfs(int x) {
        if(x == c.length - 1) {
            res.add(String.valueOf(c)); // 添加排列方案
            return;
        }
        Set<Character> set = new HashSet<>();
        for(int i = x; i < c.length; i++) {
            if(set.contains(c[i])) continue; // 重复,因此剪枝
            set.add(c[i]);
            swap(i, x); // 交换,将 c[i] 固定在第 x 位 
            dfs(x + 1); // 开启固定第 x + 1 位字符
            swap(i, x); // 恢复交换,回溯到上一步
        }
    }
    void swap(int a, int b) {
        char tmp = c[a];
        c[a] = c[b];
        c[b] = tmp;
    }
}

3 复杂性分析

  • 时间复杂度 O(N!): N 为字符串 s 的长度;时间复杂度和字符串排列的方案数成线性关系,方案数为N×(N−1)×(N−2)…×2×1 ,因此复杂度为 O(N!)。
  • 空间复杂度 O(N ^ 2) : 全排列的递归深度为 N ,系统累计使用栈空间大小为 O(N) ;递归中辅助Set累计存储的字符数量最多为 N + (N-1) + … + 2 + 1 = (N+1)N/2,即占用 O(N ^ 2 ) 的额外空间。

数字字符串转化成IP地址

1 题目描述

现在有一个只包含数字的字符串,将该字符串转化成IP地址的形式,返回所有可能的情况。

例如:

给出的字符串为"25525522135",返回[“255.255.22.135”, “255.255.221.35”]. (顺序没有关系)

示例1

输入

“25525522135”

返回值

[“255.255.22.135”,“255.255.221.35”]

2 解题(Java)

import java.util.*;

public class Solution {
    ArrayList<String> res = new ArrayList<>();
    public ArrayList<String> restoreIpAddresses(String s) {
        backTrack(s, 0, 3);
        return res;
    }
    // start:本次插入的起始位置
    // cnt:剩余可插入'.'的次数
    public void backTrack(String s, int start, int cnt) {
        if (cnt == 0) {
            String[] strs = s.split("\\.");
            for (String str : strs) {
                if (str.length() > 1 && str.charAt(0) == '0') return; // 排除有前导0的情况
                if (Integer.parseInt(str) > 255) return; // 排除>255的情况
            }
            res.add(s);
            return;
        }
        int n = s.length();
        if (start + 1 < n) backTrack(s.substring(0, start+1) + "." + s.substring(start+1), start+2, cnt - 1); //插入到start+1位置,下一次从start+2位置开始
        if (start + 2 < n) backTrack(s.substring(0, start+2) + "." + s.substring(start+2), start+3, cnt - 1); //插入到start+2位置,下一次从start+3位置开始
        if (start + 3 < n) backTrack(s.substring(0, start+3) + "." + s.substring(start+3), start+4, cnt - 1); //插入到start+3位置,下一次从start+4位置开始
    }
}

3 复杂性分析

  • 时间复杂度O(N(N-1)*(N-2))*:N为字符串的长度;时间复杂度取决于方案数,经过剪枝,方案数小于*(N-1)*(N-2);
  • 空间复杂度O(1):空间复杂度取决于递归深度;

电话号码的字母组合

1 题目描述

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

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

在这里插入图片描述
示例:

输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

说明:

尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

2 解题(Java)

2.1 解题思路

回溯算法:

  1. 用一个哈希表存储每个数字对应的所有字母;
  2. 回溯过程维护一个StringBuilder ans,表示字母组合;
  3. ans初始为空,每次取电话号码的一位数字,然后从哈希表中获得该数字对应的所有字母,并将其中一个加入ans中;
  4. 继续处理电话号码后一位数字,直到处理完电话号码的所有数字,即得到一个完整的字母组合,并将其加入结果res中;
  5. 回退,遍历其余的字母组合,依次加入res中,返回res即可;

注:

回溯算法主要用于寻找所有的可行解,如果发现一个解不可行,再回溯寻找其它解。而在本题中,由于每个数字对应的每个字母都可能进入字母组合,因此不存在不可行的解,相当于穷举所有解。

2.2 代码

class Solution {
    List<String> res = new ArrayList<>();
    StringBuilder ans = new StringBuilder();
    Map<Character, String> dic = new HashMap<Character, String>() {{
        put('2', "abc");
        put('3', "def");
        put('4', "ghi");
        put('5', "jkl");
        put('6', "mno");
        put('7', "pqrs");
        put('8', "tuv");
        put('9', "wxyz");
    }};
    String digits;
    public List<String> letterCombinations(String digits) {
        this.digits = digits;
        if (digits.length() == 0) return res;
        backTrack(0);
        return res;
    }
    public void backTrack(int index) {
        if (index == digits.length()) {
            res.add(ans.toString());
        } else {
            String letters = dic.get(digits.charAt(index));
            for (int i=0; i<letters.length(); i++) {
                ans.append(letters.charAt(i));
                backTrack(index+1);
                ans.deleteCharAt(index);
            }
        }
    }
}

3 复杂性分析

  • 时间复杂度O(3 ^ M * 4 ^ N):其中 M 是输入中对应3个字母的数字个数(包括数字 2、3、4、5、6、8),N 是输入中对应4个字母的数字个数(包括数字 7、9),当输入包含M个对应3个字母的数字和N个对应4个字母的数字时,不同的字母组合共有3 ^ M * 4 ^ N种,需要遍历每一种字母组合;
  • 空间复杂度O(M + N):其中M是输入中对应3个字母的数字个数,N是输入中对应4个字母的数字个数,M+N是输入数字的总个数。除了返回值以外,空间复杂度主要取决于哈希表以及回溯过程中的递归调用层数,哈希表的大小与输入无关,可以看成常数,递归调用层数最大为M+N;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hellosc01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值