Skip to content

图与贪心

图论和贪心算法是面试中较难的两类问题。图的核心是遍历(DFS/BFS)和拓扑排序;贪心的核心是每步选择局部最优从而得到全局最优。本模块涵盖课程表、腐烂的橘子、买卖股票、跳跃游戏等经典题目。掌握图的表示方法和贪心的适用场景是解题关键。


Q1: 课程表 「🟡 中级」

题目描述:你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1。在选修某些课程之前需要一些先修课程。先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi],表示如果要学习课程 ai 则必须先学习课程 bi。请你判断是否可能完成所有课程的学习?

考察点:拓扑排序、有向无环图(DAG)、BFS/DFS、入度表。

解题思路

  • 方法1:BFS拓扑排序(Kahn算法),入度为0的节点入队,逐个出队并减少邻居入度(时间O(V+E),空间O(V+E))- 最优解
  • 方法2:DFS,检测是否有环(时间O(V+E),空间O(V+E))

代码实现

java
public boolean canFinish(int numCourses, int[][] prerequisites) {
    // 构建邻接表和入度表
    List<List<Integer>> adj = new ArrayList<>();
    int[] inDegree = new int[numCourses];
    for (int i = 0; i < numCourses; i++) {
        adj.add(new ArrayList<>());
    }
    for (int[] pre : prerequisites) {
        adj.get(pre[1]).add(pre[0]); // pre[1] -> pre[0]
        inDegree[pre[0]]++;
    }
    // BFS
    Queue<Integer> queue = new LinkedList<>();
    for (int i = 0; i < numCourses; i++) {
        if (inDegree[i] == 0) queue.offer(i);
    }
    int count = 0;
    while (!queue.isEmpty()) {
        int curr = queue.poll();
        count++;
        for (int next : adj.get(curr)) {
            inDegree[next]--;
            if (inDegree[next] == 0) {
                queue.offer(next);
            }
        }
    }
    return count == numCourses;
}

复杂度分析:时间复杂度 O(V+E),V为课程数,E为先修关系数;空间复杂度 O(V+E),邻接表存储。

变体扩展

  • 变体1:课程表II(返回学习顺序)
  • 变体2:课程表III(按时间安排)
  • 变体3:课程表IV(查询是否为先修)
  • 变体4:找到最终的安全状态

Q2: 岛屿数量(图的BFS/DFS) 「🟡 中级」

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

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

解题思路

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

代码实现

java
// BFS实现
public int numIslands(char[][] grid) {
    if (grid == null || grid.length == 0) return 0;
    int count = 0;
    int m = grid.length, n = grid[0].length;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == '1') {
                bfs(grid, i, j, m, n);
                count++;
            }
        }
    }
    return count;
}

private void bfs(char[][] grid, int i, int j, int m, int n) {
    Queue<int[]> queue = new LinkedList<>();
    queue.offer(new int[]{i, j});
    grid[i][j] = '0';
    int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    while (!queue.isEmpty()) {
        int[] curr = queue.poll();
        for (int[] dir : dirs) {
            int x = curr[0] + dir[0];
            int y = curr[1] + dir[1];
            if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '1') {
                grid[x][y] = '0';
                queue.offer(new int[]{x, y});
            }
        }
    }
}

复杂度分析:时间复杂度 O(m*n),每个格子最多访问一次;空间复杂度 O(min(m,n)),队列最大不超过较短边。

变体扩展

  • 变体1:岛屿的最大面积
  • 变体2:腐烂的橘子
  • 变体3:被围绕的区域
  • 变体4:太平洋大西洋水流问题

Q3: 腐烂的橘子 「🟡 中级」

题目描述:在给定的 m x n 网格 grid 中,每个单元格可以有三个值之一:0 代表空单元格,1 代表新鲜橘子,2 代表腐烂的橘子。每分钟,任何与腐烂的橘子(在4个正方向上)相邻的新鲜橘子都会腐烂。返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。

考察点:多源BFS、层序遍历、最短时间计算。

解题思路

  • 方法:多源BFS,初始所有腐烂橘子同时开始扩散,按层序遍历统计分钟数(时间O(mn),空间O(mn))- 最优解

代码实现

java
public int orangesRotting(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    Queue<int[]> queue = new LinkedList<>();
    int fresh = 0;
    // 初始化:统计新鲜橘子数,腐烂橘子入队
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == 2) {
                queue.offer(new int[]{i, j});
            } else if (grid[i][j] == 1) {
                fresh++;
            }
        }
    }
    if (fresh == 0) return 0;
    int minutes = 0;
    int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    while (!queue.isEmpty()) {
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            int[] curr = queue.poll();
            for (int[] dir : dirs) {
                int x = curr[0] + dir[0];
                int y = curr[1] + dir[1];
                if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 1) {
                    grid[x][y] = 2;
                    fresh--;
                    queue.offer(new int[]{x, y});
                }
            }
        }
        minutes++;
    }
    return fresh == 0 ? minutes - 1 : -1;
}

复杂度分析:时间复杂度 O(mn),每个格子最多入队一次;空间复杂度 O(mn),最坏全是腐烂橘子。

变体扩展

  • 变体1:岛屿数量
  • 变体2:01矩阵(最近0的距离)
  • 变体3:墙与门
  • 变体4:二叉树的层序遍历

Q4: 买卖股票的最佳时机II 「🟡 中级」

题目描述:给你一个整数数组 prices,其中 prices[i] 表示某支股票第 i 天的价格。在每一天,你可能会决定购买和/或出售股票。你在任何时候最多只能持有一股股票。你也可以购买它,然后在同一天出售。返回你能获得的最大利润。

考察点:贪心算法、峰谷法、动态规划。

解题思路

  • 方法1:动态规划,每天有持有/不持有两种状态(时间O(n),空间O(n)可优化到O(1))
  • 方法2:贪心,只要后一天比前一天贵就赚差价(时间O(n),空间O(1))- 最优解

代码实现

java
public int maxProfit(int[] prices) {
    int maxProfit = 0;
    for (int i = 1; i < prices.length; i++) {
        if (prices[i] > prices[i - 1]) {
            maxProfit += prices[i] - prices[i - 1];
        }
    }
    return maxProfit;
}

复杂度分析:时间复杂度 O(n),遍历一次价格数组;空间复杂度 O(1)。

变体扩展

  • 变体1:买卖股票的最佳时机(只能买卖一次)
  • 变体2:买卖股票的最佳时机III(最多两次交易)
  • 变体3:买卖股票的最佳时机IV(最多k次交易)
  • 变体4:最佳买卖股票时机含冷冻期
  • 变体5:买卖股票的最佳时机含手续费

Q5: 跳跃游戏 「🟡 中级」

题目描述:给定一个非负整数数组 nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。

考察点:贪心算法、最远可达位置、动态规划。

解题思路

  • 方法1:动态规划(时间O(n²),空间O(n))
  • 方法2:贪心,维护最远可达位置(时间O(n),空间O(1))- 最优解

代码实现

java
public boolean canJump(int[] nums) {
    int maxReach = 0;
    for (int i = 0; i < nums.length; i++) {
        if (i > maxReach) return false; // 到不了当前位置
        maxReach = Math.max(maxReach, i + nums[i]);
        if (maxReach >= nums.length - 1) return true;
    }
    return true;
}

复杂度分析:时间复杂度 O(n),遍历一次数组;空间复杂度 O(1)。

变体扩展

  • 变体1:跳跃游戏II(最少跳跃次数)
  • 变体2:跳跃游戏III(是否可达0)
  • 变体3:跳跃游戏IV(同值可跳)
  • 变体4:跳跃游戏V(有高度限制)

Q6: 分发饼干 「🟢 初级」

题目描述:假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j]。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

考察点:贪心算法、排序、双指针。

解题思路

  • 方法:排序 + 贪心,用最小的饼干满足胃口最小的孩子(时间O(nlogn + mlogm),空间O(1)或O(logn)排序栈空间)- 最优解

代码实现

java
public int findContentChildren(int[] g, int[] s) {
    Arrays.sort(g);
    Arrays.sort(s);
    int i = 0, j = 0;
    int count = 0;
    while (i < g.length && j < s.length) {
        if (s[j] >= g[i]) {
            count++;
            i++;
            j++;
        } else {
            j++; // 饼干太小,换大饼干
        }
    }
    return count;
}

复杂度分析:时间复杂度 O(nlogn + mlogm),排序为主;空间复杂度 O(1)(忽略排序的栈空间)。

变体扩展

  • 变体1:柠檬水找零
  • 变体2:用最少数量的箭引爆气球
  • 变体3:无重叠区间
  • 变体4:划分字母区间