算法练习5

今日完成 4 道高频算法题,覆盖回溯、动态规划、二维网格 DFS 三个重要专题。

复制代码
回溯:选择 -> 递归 -> 撤销选择
动态规划:定义状态 -> 推导状态转移 -> 优化空间
网格 DFS:遍历格子 -> 发现连通块 -> 标记已访问

1. 全排列

题目:给定一个不包含重复元素的数组 nums,返回所有可能的全排列。

示例:

复制代码
nums = [1, 2, 3]

结果:
[
  [1, 2, 3],
  [1, 3, 2],
  [2, 1, 3],
  [2, 3, 1],
  [3, 1, 2],
  [3, 2, 1]
]

思路

全排列要求每个位置都从尚未使用过的元素中选择一个。

复制代码
第一个位置:可以选 1、2、3
第二个位置:从剩余元素中选
第三个位置:只能选择最后一个剩余元素

需要使用:

复制代码
path:当前正在构造的排列
used:记录每个元素是否已经被使用
result:保存所有完整排列

当路径长度等于数组长度时,说明得到一个完整排列:

复制代码
if (path.size() == nums.length) {
    result.add(new ArrayList<>(path));
    return;
}

Java 实现

复制代码
public List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    boolean[] used = new boolean[nums.length];

    backtrack(nums, used, new ArrayList<>(), result);

    return result;
}

private void backtrack(
        int[] nums,
        boolean[] used,
        List<Integer> path,
        List<List<Integer>> result) {

    if (path.size() == nums.length) {
        result.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, result);

        path.remove(path.size() - 1);
        used[i] = false;
    }
}

回溯核心模板

复制代码
做选择;
递归进入下一层;
撤销选择;

对应代码:

复制代码
used[i] = true;
path.add(nums[i]);

backtrack(nums, used, path, result);

path.remove(path.size() - 1);
used[i] = false;

其中:

复制代码
result.add(new ArrayList<>(path));

必须创建副本。因为 path 会在回溯时不断增删;若直接保存 path,所有结果都会引用同一个列表对象。

复杂度

复制代码
时间复杂度:O(n * n!)
空间复杂度:O(n)

排列总数为 n!,复制每个排列需要 O(n)


2. 组合总和

题目:给定无重复正整数数组 candidates 和目标值 target,找出所有和为 target 的不同组合。

要求:

复制代码
同一个数字可以重复选择。
组合元素顺序不同,视为同一个组合。

示例:

复制代码
candidates = [2, 3, 6, 7]
target = 7

结果:
[[2, 2, 3], [7]]

与全排列的区别

复制代码
全排列:
每个元素最多选择一次;
使用 used[] 标记;
[1, 2] 和 [2, 1] 是不同答案。

组合总和:
元素可以重复选择;
不使用 used[];
[2, 2, 3] 与 [3, 2, 2] 是同一个组合。

核心参数

复制代码
remaining:距离 target 还差多少。
start:当前层从哪个下标开始选择。
path:当前组合。
result:所有符合条件的组合。

结束条件与剪枝

复制代码
if (remaining == 0) {
    result.add(new ArrayList<>(path));
    return;
}

if (remaining < 0) {
    return;
}

含义:

复制代码
remaining == 0:当前组合和恰好等于 target。
remaining < 0:当前组合和超过 target,后续数字均为正数,不可能回到 target,可以停止。

Java 实现

复制代码
public List<List<Integer>> combinationSum(int[] candidates, int target) {
    List<List<Integer>> result = new ArrayList<>();

    backtrack(candidates, target, 0, new ArrayList<>(), result);

    return result;
}

private void backtrack(
        int[] candidates,
        int remaining,
        int start,
        List<Integer> path,
        List<List<Integer>> result) {

    if (remaining == 0) {
        result.add(new ArrayList<>(path));
        return;
    }

    if (remaining < 0) {
        return;
    }

    for (int i = start; i < candidates.length; i++) {
        path.add(candidates[i]);

        backtrack(
                candidates,
                remaining - candidates[i],
                i,
                path,
                result
        );

        path.remove(path.size() - 1);
    }
}

为什么递归传 i

复制代码
backtrack(candidates, remaining - candidates[i], i, path, result);

传入 i,代表下一层仍可以选择当前数字:

复制代码
选择 2 后,下一层仍可选择 2。

[2] -> [2, 2] -> [2, 2, 3]

如果传入 i + 1,当前数字只能选一次,题目就会变成另一类问题。

为什么不从 0 重新开始

若每层都从下标 0 开始,会产生顺序重复:

复制代码
[2, 2, 3]
[2, 3, 2]
[3, 2, 2]

通过 start 限制下标不倒退:

复制代码
只生成 [2, 2, 3];
不会生成 [3, 2, 2]。

3. 爬楼梯

题目:需要爬到第 n 阶,每次可以走 1 阶或 2 阶,求不同走法数量。

示例:

复制代码
n = 2

[1 + 1]
[2]

结果:2

n = 3

[1 + 1 + 1]
[1 + 2]
[2 + 1]

结果:3

状态转移

到达第 n 阶时,最后一步只有两种来源:

复制代码
从第 n - 1 阶走 1 步;
从第 n - 2 阶走 2 步。

因此:

复制代码
dp[n] = dp[n - 1] + dp[n - 2]

基础状态:

复制代码
dp[1] = 1
dp[2] = 2

常数空间优化

每次只依赖前两个状态,不需要完整 dp 数组:

复制代码
public int climbStairs(int n) {
    if (n <= 2) {
        return n;
    }

    int previousPrevious = 1;
    int previous = 2;

    for (int step = 3; step <= n; step++) {
        int current = previousPrevious + previous;

        previousPrevious = previous;
        previous = current;
    }

    return previous;
}

变量含义:

复制代码
previousPrevious:到第 i - 2 阶的方法数。
previous:到第 i - 1 阶的方法数。
current:到第 i 阶的方法数。

例如 n = 5

复制代码
第 1 阶:1
第 2 阶:2
第 3 阶:3
第 4 阶:5
第 5 阶:8

复杂度

复制代码
时间复杂度:O(n)
空间复杂度:O(1)

本题本质是斐波那契数列变形:

复制代码
dp[i] = dp[i - 1] + dp[i - 2]

4. 岛屿数量

题目:给定由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿数量。

规则:

复制代码
上下左右相邻的陆地属于同一座岛。
对角线相邻不连通。

示例:

复制代码
grid = [
  ['1', '1', '0', '0', '0'],
  ['1', '1', '0', '0', '0'],
  ['0', '0', '1', '0', '0'],
  ['0', '0', '0', '1', '1']
]

结果:3

网格转图

二维网格可以看作图:

复制代码
每个 '1' 是一个节点;
上下左右相邻的 '1' 之间存在边;
一片相连的陆地就是一个连通块,也就是一座岛。

DFS 思路

复制代码
双重循环扫描每一个格子:

遇到 '0':跳过。
遇到 '1':
    发现一座新岛,count 加 1;
    从当前位置开始 DFS;
    将该岛所有相连的 '1' 全部改为 '0'。

'1' 改成 '0' 的作用:

复制代码
标记该陆地已经访问;
避免同一个岛被重复统计;
避免 DFS 在相邻格子间反复递归。

Java 实现

复制代码
public int numIslands(char[][] grid) {
    int count = 0;

    for (int row = 0; row < grid.length; row++) {
        for (int col = 0; col < grid[0].length; col++) {
            if (grid[row][col] == '1') {
                dfs(grid, row, col);
                count++;
            }
        }
    }

    return count;
}

private void dfs(char[][] grid, int row, int col) {
    if (row < 0 || row >= grid.length
            || col < 0 || col >= grid[0].length
            || grid[row][col] == '0') {
        return;
    }

    grid[row][col] = '0';

    dfs(grid, row - 1, col);
    dfs(grid, row + 1, col);
    dfs(grid, row, col - 1);
    dfs(grid, row, col + 1);
}

DFS 四个方向

复制代码
上:row - 1, col
下:row + 1, col
左:row, col - 1
右:row, col + 1

复杂度

复制代码
时间复杂度:O(m * n)
空间复杂度:O(m * n)

其中:

复制代码
m:网格行数
n:网格列数

每个格子最多被访问一次。最坏情况下网格全为陆地,递归调用栈可能达到 O(m * n)

相关推荐
问商十三载1 小时前
RAG 检索召回越多越好?2026 信噪比模型深度解析
人工智能·算法·机器学习
额恩661 小时前
ResearchPilot 第三阶段总结报告
python·算法
Hi李耶1 小时前
【LeetCode】9-回文数
算法·leetcode·职场和发展
红叶舞2 小时前
基于线段树的数据结构
数据结构·python·算法
断点之下2 小时前
从“理扑克牌”到“分组优化”——直接插入排序与希尔排序详解
数据结构·算法·排序算法
起个破名想半天了2 小时前
算法与数据结构之DFS深度优先遍历
数据结构·算法·深度优先
李伟_Li慢慢2 小时前
凸包算法
人工智能·算法
徐礼昭|商派软件市场负责人2 小时前
AI Agent 蜂群协作与经验基因化:从单体执行到自组织系统的工程路径
大数据·人工智能·算法·智能体·多agent
来者皆善3 小时前
【位RAW图像处理五】任意位深位图像的中值模糊快速实现及其应用。
图像处理·人工智能·算法