Skip to content

回溯与DFS/BFS

回溯是一种暴力搜索的艺术,通过"选择-递归-回溯"的模式穷举所有可能解。DFS和BFS则是图和树的两种基本遍历方式,在岛屿问题、搜索问题中应用广泛。本模块涵盖全排列、子集、组合总和、岛屿数量等经典题型,掌握回溯模板和DFS/BFS的适用场景是关键。


Q1: 全排列 「🟡 中级」

题目描述:给定一个不含重复数字的数组 nums,返回其所有可能的全排列。你可以按任意顺序返回答案。

考察点:回溯算法、排列问题、used数组标记。

解题思路

  • 方法:回溯法,用used数组标记已选元素,递归深度等于数组长度时收集结果(时间O(n*n!),空间O(n))- 最优解

代码实现

java
public List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> res = new ArrayList<>();
    backtrack(nums, new boolean[nums.length], new ArrayList<>(), res);
    return res;
}

private void backtrack(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> res) {
    if (path.size() == nums.length) {
        res.add(new ArrayList<>(path));
        return;
    }
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) continue;
        used[i] = true;
        path.add(nums[i]);
        backtrack(nums, used, path, res);
        path.remove(path.size() - 1);
        used[i] = false;
    }
}

复杂度分析:时间复杂度 O(n*n!),共n!个排列,每个排列需要O(n)复制;空间复杂度 O(n),递归栈和used数组。

变体扩展

  • 变体1:全排列II(有重复元素)
  • 变体2:第k个排列
  • 变体3:下一个排列
  • 变体4:字母大小写全排列

Q2: 子集 「🟡 中级」

题目描述:给你一个整数数组 nums,数组中的元素互不相同。返回该数组所有可能的子集(幂集)。解集不能包含重复的子集。你可以按任意顺序返回解集。

考察点:回溯算法、子集问题、startIndex去重。

解题思路

  • 方法1:回溯法,每次递归都收集结果,用startIndex控制不重复选择(时间O(n*2ⁿ),空间O(n))- 最优解
  • 方法2:迭代法,逐个元素添加到已有子集中

代码实现

java
public List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> res = new ArrayList<>();
    backtrack(nums, 0, new ArrayList<>(), res);
    return res;
}

private void backtrack(int[] nums, int startIndex, List<Integer> path, List<List<Integer>> res) {
    res.add(new ArrayList<>(path)); // 每个节点都是一个子集
    for (int i = startIndex; i < nums.length; i++) {
        path.add(nums[i]);
        backtrack(nums, i + 1, path, res);
        path.remove(path.size() - 1);
    }
}

复杂度分析:时间复杂度 O(n*2ⁿ),共2ⁿ个子集,每个子集复制需要O(n);空间复杂度 O(n),递归栈深度。

变体扩展

  • 变体1:子集II(有重复元素)
  • 变体2:递增子序列
  • 变体3:组合
  • 变体4:所有可能的真子集

Q3: 组合总和 「🟡 中级」

题目描述:给你一个无重复元素的整数数组 candidates 和一个目标整数 target,找出 candidates 中可以使数字和为目标数 target 的所有不同组合,并以列表形式返回。你可以按任意顺序返回这些组合。candidates 中的同一个数字可以无限制重复被选取。

考察点:回溯算法、组合问题、剪枝优化。

解题思路

  • 方法:回溯法,可重复选所以startIndex从i开始(不是i+1),排序后可剪枝(时间O(n*2ⁿ),空间O(n))- 最优解

代码实现

java
public List<List<Integer>> combinationSum(int[] candidates, int target) {
    List<List<Integer>> res = new ArrayList<>();
    Arrays.sort(candidates); // 排序便于剪枝
    backtrack(candidates, target, 0, 0, new ArrayList<>(), res);
    return res;
}

private void backtrack(int[] candidates, int target, int startIndex, int sum, List<Integer> path, List<List<Integer>> res) {
    if (sum == target) {
        res.add(new ArrayList<>(path));
        return;
    }
    for (int i = startIndex; i < candidates.length; i++) {
        if (sum + candidates[i] > target) break; // 剪枝
        path.add(candidates[i]);
        backtrack(candidates, target, i, sum + candidates[i], path, res); // i不变,可重复选
        path.remove(path.size() - 1);
    }
}

复杂度分析:时间复杂度 O(n*2ⁿ),最坏情况所有组合都要尝试;空间复杂度 O(target/min),递归深度最多为target/min(candidates)。

变体扩展

  • 变体1:组合总和II(每个数字只能用一次,有重复)
  • 变体2:组合总和III(k个数相加等于n)
  • 变体3:组合总和IV(求排列数,DP解法)
  • 变体4:零钱兑换II(求组合数,DP解法)

Q4: 括号生成 「🟡 中级」

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

考察点:回溯算法、括号有效性、剪枝条件。

解题思路

  • 方法:回溯法,分别追踪左右括号数量,保证左括号>=右括号(时间O(4ⁿ/√n)卡特兰数,空间O(n))- 最优解

代码实现

java
public List<String> generateParenthesis(int n) {
    List<String> res = new ArrayList<>();
    backtrack(n, n, new StringBuilder(), res);
    return res;
}

private void backtrack(int left, int right, StringBuilder path, List<String> res) {
    if (left == 0 && right == 0) {
        res.add(path.toString());
        return;
    }
    // 左括号还有剩余,可以选左括号
    if (left > 0) {
        path.append('(');
        backtrack(left - 1, right, path, res);
        path.deleteCharAt(path.length() - 1);
    }
    // 右括号多于左括号剩余时,可以选右括号(保证有效性)
    if (right > left) {
        path.append(')');
        backtrack(left, right - 1, path, res);
        path.deleteCharAt(path.length() - 1);
    }
}

复杂度分析:时间复杂度 O(4ⁿ/√n),卡特兰数第n项;空间复杂度 O(n),递归深度。

变体扩展

  • 变体1:有效的括号
  • 变体2:最长有效括号
  • 变体3:删除无效的括号
  • 变体4:有效括号的嵌套深度

Q5: 岛屿数量 「🟡 中级」

题目描述:给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

考察点:DFS深度优先搜索、BFS广度优先搜索、网格遍历。

解题思路

  • 方法1:DFS,遍历每个格子,遇到陆地则DFS标记所有相连陆地(时间O(mn),空间O(mn)最坏)- 最优解
  • 方法2:BFS,使用队列
  • 方法3:并查集

代码实现

java
public int numIslands(char[][] grid) {
    int count = 0;
    for (int i = 0; i < grid.length; i++) {
        for (int j = 0; j < grid[0].length; j++) {
            if (grid[i][j] == '1') {
                dfs(grid, i, j);
                count++;
            }
        }
    }
    return count;
}

private void dfs(char[][] grid, int i, int j) {
    if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] != '1') {
        return;
    }
    grid[i][j] = '0'; // 标记为已访问
    dfs(grid, i + 1, j);
    dfs(grid, i - 1, j);
    dfs(grid, i, j + 1);
    dfs(grid, i, j - 1);
}

复杂度分析:时间复杂度 O(mn),每个格子最多访问一次;空间复杂度 O(mn),最坏全是陆地,递归栈深度。

变体扩展

  • 变体1:岛屿的最大面积
  • 变体2:岛屿的周长
  • 变体3:岛屿数量II(动态添加)
  • 变体4:最大人工岛

Q6: 单词搜索 「🟡 中级」

题目描述:给定一个 m x n 二维字符网格 board 和一个字符串单词 word。如果 word 存在于网格中,返回 true;否则,返回 false。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

考察点:DFS回溯、二维网格搜索、剪枝优化。

解题思路

  • 方法:DFS回溯,从每个匹配的起点开始搜索,用标记数组或原地修改记录访问过的位置(时间O(mn3^L),L为单词长度,空间O(L))- 最优解

代码实现

java
public boolean exist(char[][] board, String word) {
    for (int i = 0; i < board.length; i++) {
        for (int j = 0; j < board[0].length; j++) {
            if (dfs(board, word, i, j, 0)) {
                return true;
            }
        }
    }
    return false;
}

private boolean dfs(char[][] board, String word, int i, int j, int index) {
    if (index == word.length()) return true;
    if (i < 0 || i >= board.length || j < 0 || j >= board[0].length 
        || board[i][j] != word.charAt(index)) {
        return false;
    }
    char temp = board[i][j];
    board[i][j] = '#'; // 标记已访问
    boolean found = dfs(board, word, i + 1, j, index + 1)
                 || dfs(board, word, i - 1, j, index + 1)
                 || dfs(board, word, i, j + 1, index + 1)
                 || dfs(board, word, i, j - 1, index + 1);
    board[i][j] = temp; // 回溯
    return found;
}

复杂度分析:时间复杂度 O(mn3^L),每个起点有3个方向可选(排除来路);空间复杂度 O(L),递归深度为单词长度。

变体扩展

  • 变体1:单词搜索II(Trie树优化)
  • 变体2:岛屿数量
  • 变体3:矩阵中的路径
  • 变体4:黄金矿工

Q7: N皇后 「🔴 高级」

题目描述:按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数 n,返回所有不同的 n 皇后问题的解决方案。

考察点:回溯算法、经典难题、约束条件判断。

解题思路

  • 方法:回溯法,逐行放置皇后,用三个集合/数组标记列、主对角线、副对角线的占用情况(时间O(n!),空间O(n))- 最优解

代码实现

java
public List<List<String>> solveNQueens(int n) {
    List<List<String>> res = new ArrayList<>();
    char[][] board = new char[n][n];
    for (char[] row : board) Arrays.fill(row, '.');
    backtrack(n, 0, board, res, new boolean[n], new boolean[2 * n - 1], new boolean[2 * n - 1]);
    return res;
}

private void backtrack(int n, int row, char[][] board, List<List<String>> res,
                       boolean[] colUsed, boolean[] diag1Used, boolean[] diag2Used) {
    if (row == n) {
        List<String> list = new ArrayList<>();
        for (char[] r : board) list.add(new String(r));
        res.add(list);
        return;
    }
    for (int col = 0; col < n; col++) {
        int diag1 = row - col + n - 1; // 主对角线:row-col为常数
        int diag2 = row + col;         // 副对角线:row+col为常数
        if (colUsed[col] || diag1Used[diag1] || diag2Used[diag2]) continue;
        board[row][col] = 'Q';
        colUsed[col] = true;
        diag1Used[diag1] = true;
        diag2Used[diag2] = true;
        backtrack(n, row + 1, board, res, colUsed, diag1Used, diag2Used);
        board[row][col] = '.';
        colUsed[col] = false;
        diag1Used[diag1] = false;
        diag2Used[diag2] = false;
    }
}

复杂度分析:时间复杂度 O(n!),第1行n种选择,第2行最多n-2种,以此类推;空间复杂度 O(n),递归栈和标记数组。

变体扩展

  • 变体1:N皇后II(求解的数量)
  • 变体2:解数独
  • 变体3:有效的数独

Q8: 二叉树的右视图 「🟡 中级」

题目描述:给定一个二叉树的根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

考察点:BFS层序遍历、DFS右优先遍历。

解题思路

  • 方法1:BFS层序遍历,每层最后一个节点就是右视图(时间O(n),空间O(n))- 最优解
  • 方法2:DFS,根→右→左的顺序,每层第一个访问的节点加入结果

代码实现

java
// BFS层序遍历
public List<Integer> rightSideView(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if (root == null) return res;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            if (i == size - 1) res.add(node.val); // 每层最后一个
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
    }
    return res;
}

复杂度分析:时间复杂度 O(n),每个节点入队出队一次;空间复杂度 O(n),队列最多存最后一层节点。

变体扩展

  • 变体1:二叉树的左视图
  • 变体2:二叉树的层序遍历
  • 变体3:二叉树的俯视图
  • 变体4:填充每个节点的下一个右侧节点指针