【笔面试算法学习专栏】图算法入门专题:岛屿数量与课程表

1. 引言与图算法基础

图论是计算机科学中研究图(Graph)这一离散结构的数学分支,广泛应用于网络分析、路径规划、社交网络、任务调度等领域。

1.1 图的基本概念

图的定义 :图 G = ( V , E ) G = (V, E) G=(V,E) 由顶点集 V V V 和边集 E E E 组成,其中每条边 e ∈ E e \in E e∈E 连接两个顶点 u , v ∈ V u, v \in V u,v∈V。

图的分类

  • 无向图:边没有方向,表示双向关系(如社交网络中的好友关系)
  • 有向图:边有方向,表示单向关系(如网页跳转、任务依赖)
  • 加权图:边带有权重,表示距离、成本等量化关系
  • 非加权图:边没有权重,仅表示连接关系

特殊图结构

  • 树(Tree):无环的连通无向图,边数 = 顶点数 - 1
  • 有向无环图(DAG):没有回路的有向图,可用于拓扑排序

1.2 图的表示方法

邻接矩阵 :使用 n × n n \times n n×n 的二维数组表示图,matrix[i][j] = 1 表示顶点 i i i 到 j j j 有边。优点:快速查询任意两点间是否有边;缺点:空间复杂度 O ( n 2 ) O(n^2) O(n2),不适合稀疏图。

邻接表:使用数组或字典存储每个顶点的邻居列表。优点:空间效率高,适合稀疏图;缺点:查询两顶点是否有边需要遍历邻居列表。

python 复制代码
# 邻接表表示的图(无向图)
graph = {
    0: [1, 2],
    1: [0, 3],
    2: [0, 3],
    3: [1, 2]
}

1.3 图的遍历算法

深度优先搜索(DFS):沿着一条路径深入到底,直到无法前进时回溯,采用栈(递归)实现。应用场景:连通分量检测、拓扑排序、环检测。

广度优先搜索(BFS):按层次扩展,先访问距离起点最近的顶点,采用队列实现。应用场景:无权图最短路径、多源扩展。

时间复杂度 :邻接表表示下均为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣),邻接矩阵表示下为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。

1.4 图算法面试核心考点

  1. 连通分量计数:统计图中连通区域的数量(如岛屿数量问题)
  2. 环检测:判断有向图或无向图中是否存在环(如课程表问题)
  3. 拓扑排序:对有向无环图的顶点进行线性排序
  4. 最短路径:寻找两点间权重最小的路径
  5. 最小生成树:寻找连接所有顶点的最小权重子图

本文聚焦前两个核心考点,通过力扣hot100中的两道经典题目------岛屿数量(200)和课程表(207),系统讲解图算法的基础实现与优化技巧。

2. 题目一:岛屿数量深度解析

2.1 问题描述

题目200. 岛屿数量

给定一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿的数量。岛屿被水包围,并且通过水平或垂直方向相邻的陆地连接而成。

示例

复制代码
输入: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.2 问题本质与建模

核心洞察 :将网格视为图,每个陆地单元格('1')是图中的一个节点,上下左右四个方向的相邻陆地之间有一条边。问题转化为统计无向图中连通分量的数量

网格图的特点

  • 顶点数:陆地单元格的数量
  • 边数:相邻陆地关系的数量(最多为每个陆地与4个邻居的连接)
  • 图结构隐含在网格坐标中,无需显式构建邻接表

2.3 解法一:DFS递归实现(沉岛法)

算法思想 :遍历网格,当遇到未访问的陆地时,岛屿计数加1,并通过DFS递归将该岛屿的所有陆地标记为已访问(原地修改为 '0')。

实现步骤

  1. 处理边界条件:网格为空则返回0
  2. 遍历每个网格单元格 (i, j)
  3. grid[i][j] == '1'
    • 岛屿计数 count += 1
    • 调用 dfs(i, j) 淹没整个岛屿
  4. 返回 count

DFS递归函数设计

python 复制代码
def dfs(grid, i, j):
    # 边界检查:越界或遇到水/已访问陆地
    if i < 0 or i >= len(grid) or j < 0 or j >= len(grid[0]) or grid[i][j] == '0':
        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)  # 右

关键优化

  • 原地修改 :直接修改原网格,避免使用额外的 visited 数组
  • 边界检查顺序:先检查坐标越界,再访问网格值,防止数组下标越界
  • 方向处理:仅考虑上下左右四个方向,忽略对角线

复杂度分析

  • 时间复杂度: O ( m × n ) O(m \times n) O(m×n),每个单元格最多访问一次
  • 空间复杂度: O ( m × n ) O(m \times n) O(m×n),最坏情况递归栈深度达网格大小(全为陆地时)

面试优势:代码简洁直观,是最经典的解法,适用于大多数面试场景。

2.4 解法二:BFS队列实现

算法思想:用队列替代递归,实现层次遍历,同样采用沉岛策略。

实现步骤

  1. 初始化队列,将起点入队时立即标记为已访问
  2. 当队列非空时:
    • 出队当前节点
    • 遍历四个方向的邻居
    • 若邻居是未访问的陆地,则入队并标记
  3. 重复步骤2直到队列为空

关键细节

  • 入队即标记:必须在入队时立即标记为已访问,否则会导致同一节点多次入队,造成超时
  • 队列存储坐标 :使用双端队列或普通队列存储 (i, j) 元组

BFS实现代码

python 复制代码
from collections import deque

def bfs(grid, i, j):
    queue = deque()
    queue.append((i, j))
    grid[i][j] = '0'  # 入队时立即标记
    
    directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    
    while queue:
        x, y = queue.popleft()
        
        for dx, dy in directions:
            nx, ny = x + dx, y + dy
            
            if 0 <= nx < len(grid) and 0 <= ny < len(grid[0]) and grid[nx][ny] == '1':
                queue.append((nx, ny))
                grid[nx][ny] = '0'  # 入队时立即标记

复杂度分析

  • 时间复杂度: O ( m × n ) O(m \times n) O(m×n),每个单元格最多入队一次
  • 空间复杂度: O ( min ⁡ ( m , n ) ) O(\min(m, n)) O(min(m,n)),队列中最多存储一行或一列的长度(长条形陆地)

适用场景

  • 超大网格:避免递归栈溢出风险
  • 需要层次信息:如计算岛屿的最大面积
  • 工程友好:非递归实现更稳定

2.5 解法三:并查集(Union-Find)优化

算法思想:将每个陆地单元格视为独立集合,遍历网格合并相邻陆地,最终统计集合数量。

并查集核心操作

  1. 初始化:每个节点的父节点指向自己
  2. 查找(Find):带路径压缩,找到节点的根节点
  3. 合并(Union):按秩合并,将小树挂到大树下

实现步骤

  1. 统计陆地总数,初始化并查集
  2. 遍历网格,对每个陆地单元格:
    • 将其与右侧、下方邻居合并(避免重复合并上、左方向)
  3. 统计并查集中集合数量

并查集类设计

python 复制代码
class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [1] * n
        self.count = n  # 初始集合数
    
    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # 路径压缩
        return self.parent[x]
    
    def union(self, x, y):
        root_x = self.find(x)
        root_y = self.find(y)
        
        if root_x == root_y:
            return False
        
        # 按秩合并:小树挂到大树下
        if self.rank[root_x] < self.rank[root_y]:
            root_x, root_y = root_y, root_x
        
        self.parent[root_y] = root_x
        self.rank[root_x] += self.rank[root_y]
        self.count -= 1
        return True

网格坐标映射 :将二维坐标 (i, j) 映射为一维索引 idx = i * n + j

并查集解法代码

python 复制代码
def numIslands_union_find(grid):
    if not grid or not grid[0]:
        return 0
    
    m, n = len(grid), len(grid[0])
    uf = UnionFind(m * n)
    
    # 统计陆地总数,初始化并查集
    land_count = 0
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                land_count += 1
    
    uf.count = land_count
    
    # 遍历合并相邻陆地
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '0':
                continue
            
            idx = i * n + j
            
            # 仅检查右侧和下侧邻居(避免重复合并)
            if j + 1 < n and grid[i][j + 1] == '1':
                uf.union(idx, i * n + (j + 1))
            
            if i + 1 < m and grid[i + 1][j] == '1':
                uf.union(idx, (i + 1) * n + j)
    
    return uf.count

复杂度分析

  • 时间复杂度: O ( m × n × α ( m × n ) ) O(m \times n \times \alpha(m \times n)) O(m×n×α(m×n)),其中 α \alpha α 是反阿克曼函数,近似常数
  • 空间复杂度: O ( m × n ) O(m \times n) O(m×n),存储父节点和秩数组

核心优势

  • 动态合并:支持网格动态变化(陆地变水或水变陆地)
  • 统一框架:适用于所有连通分量计数问题
  • 扩展性强:容易扩展到其他图算法问题

2.6 三种解法对比与选择

维度 DFS递归 BFS队列 并查集
时间复杂度 O ( m n ) O(mn) O(mn) O ( m n ) O(mn) O(mn) O ( m n ⋅ α ) O(mn \cdot \alpha) O(mn⋅α)
空间复杂度 O ( m n ) O(mn) O(mn) O ( min ⁡ ( m , n ) ) O(\min(m,n)) O(min(m,n)) O ( m n ) O(mn) O(mn)
适用场景 网格不大、代码简洁 超大网格、避免栈溢出 动态连通性、组件计数
面试评价 ⭐⭐⭐⭐⭐(首选) ⭐⭐⭐⭐(实用性强) ⭐⭐⭐(展示数据结构功底)

选择指南

  1. 面试首选:DFS递归,代码最简洁,逻辑最直观
  2. 工程实践:BFS队列,避免递归栈溢出,行为稳定
  3. 高级场景:并查集,需要支持动态更新或频繁查询

3. 题目二:课程表拓扑排序实战

3.1 问题描述

题目207. 课程表

总共有 numCourses 门课程需要学习,编号从 0numCourses - 1。给定数组 prerequisites,其中 prerequisites[i] = [a_i, b_i] 表示必须先修课程 b_i 才能学习课程 a_i。判断是否可能完成所有课程的学习。

示例

复制代码
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有2门课程。学习课程1之前,需要完成课程0。可以完成。

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有2门课程。学习课程1之前需要先修课程0,学习课程0之前又需要先修课程1,形成循环依赖,无法完成。

限制条件

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= a_i, b_i < numCourses
  • 所有课程对互不相同

3.2 问题本质与建模

核心洞察 :将课程视为图中的顶点,先修关系 [a, b] 表示从 ba 的一条有向边。问题转化为判断有向图中是否存在环

拓扑排序定义 :对有向无环图(DAG)的顶点进行线性排序,使得对于任意有向边 ( u , v ) (u, v) (u,v),顶点 u u u 在排序中都出现在顶点 v v v 的前面。

关键结论

  • 有向图存在拓扑排序 ⇔ \Leftrightarrow ⇔ 图是DAG(没有环)
  • 课程可以完成 ⇔ \Leftrightarrow ⇔ 课程依赖图是DAG

3.3 解法一:Kahn算法(BFS + 入度表)

算法思想:基于BFS,维护入度表和队列,不断选择入度为0的节点,减少其后继节点的入度。

Kahn算法步骤

  1. 构建图结构
    • 邻接表:存储每个节点的后继节点
    • 入度数组:记录每个节点的入度数(依赖数)
  2. 初始化队列:将所有入度为0的节点入队
  3. BFS循环
    • 出队节点 u,将其加入拓扑序列
    • 遍历 u 的所有后继节点 v
      • v 的入度减1
      • v 的入度变为0,入队
  4. 结果判断
    • 若拓扑序列长度等于节点总数,则无环(可完成)
    • 否则存在环(不可完成)

Kahn算法实现

python 复制代码
from collections import deque, defaultdict

def canFinish_kahn(numCourses, prerequisites):
    # 构建邻接表和入度数组
    graph = defaultdict(list)
    indegree = [0] * numCourses
    
    for a, b in prerequisites:
        graph[b].append(a)  # b -> a 的有向边
        indegree[a] += 1
    
    # 初始化队列:入度为0的节点
    queue = deque([i for i in range(numCourses) if indegree[i] == 0])
    visited = 0  # 已处理的节点数
    
    while queue:
        u = queue.popleft()
        visited += 1
        
        for v in graph[u]:
            indegree[v] -= 1
            if indegree[v] == 0:
                queue.append(v)
    
    return visited == numCourses

算法正确性证明

  • 若图无环,则至少存在一个入度为0的节点(否则会形成循环依赖)
  • 每次移除入度为0的节点及其出边,剩余图仍为DAG
  • 重复此过程可处理所有节点

复杂度分析

  • 时间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣),每个节点和边只处理一次
  • 空间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣),存储邻接表和入度数组

面试优势

  • 直观体现依赖消除过程
  • 天然支持输出拓扑序列
  • 非递归实现,避免栈溢出

3.4 解法二:DFS三色标记法

算法思想:基于DFS递归遍历,使用三色标记法检测环的存在。

三色标记状态

  • 0(白色/未访问):节点尚未被访问
  • 1(灰色/访问中):节点正在递归访问其子节点(在当前DFS栈中)
  • 2(黑色/已完成):节点及其所有子节点已处理完毕

DFS三色算法步骤

  1. 构建邻接表:存储图的边关系
  2. 初始化状态数组:所有节点标记为0(未访问)
  3. DFS遍历:对每个未访问节点启动DFS
  4. 环检测逻辑
    • 若遇到状态为1的邻居节点,说明存在回边(环)
    • 递归结束时,将当前节点标记为2
  5. 结果判断:遍历过程中未检测到环,则可完成

DFS三色实现

python 复制代码
def canFinish_dfs(numCourses, prerequisites):
    from collections import defaultdict
    
    # 构建邻接表
    graph = defaultdict(list)
    for a, b in prerequisites:
        graph[b].append(a)
    
    # 三色标记数组:0=未访问,1=访问中,2=已完成
    visited = [0] * numCourses
    
    def dfs(u):
        # 标记当前节点为访问中
        visited[u] = 1
        
        for v in graph[u]:
            if visited[v] == 0:
                if not dfs(v):
                    return False
            elif visited[v] == 1:
                # 遇到访问中的节点,存在环
                return False
        
        # 标记当前节点为已完成
        visited[u] = 2
        return True
    
    # 对每个节点启动DFS
    for i in range(numCourses):
        if visited[i] == 0:
            if not dfs(i):
                return False
    
    return True

算法正确性证明

  • 状态1表示节点在当前递归路径上
  • 若在DFS过程中遇到状态1的邻居,说明从当前节点到该邻居存在路径,且该邻居到当前节点也存在路径(通过递归栈),形成环
  • 状态2确保已处理节点不会被重复访问

复杂度分析

  • 时间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣),每个节点和边最多访问一次
  • 空间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣),存储邻接表和递归栈

面试优势

  • 代码简洁优雅
  • 理论深度高,体现算法理解
  • 空间效率较高(无需额外队列)

3.5 两种解法对比与选择

维度 Kahn算法(BFS) DFS三色法
时间复杂度 $O( V
空间复杂度 $O( V
输出拓扑序 天然支持(出队顺序) 需要额外栈(后序逆序)
适用场景 需要拓扑序列、大图 代码简洁、理论考察
面试评价 ⭐⭐⭐⭐(实用性强) ⭐⭐⭐⭐⭐(高频考察)

选择指南

  1. 需要拓扑序列:Kahn算法,可直接输出学习顺序
  2. 理论深度考察:DFS三色法,体现图遍历和环检测理解
  3. 大图避免递归:Kahn算法,非递归实现更稳定

4. 代码实现与优化技巧

4.1 完整可运行代码

以下代码整合了岛屿数量和课程表问题的所有解法,可直接运行测试:

python 复制代码
from collections import deque, defaultdict
from typing import List

# ========== 岛屿数量问题 ==========

class UnionFind:
    """并查集实现"""
    def __init__(self, n: int):
        self.parent = list(range(n))
        self.rank = [1] * n
        self.count = n
    
    def find(self, x: int) -> int:
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # 路径压缩
        return self.parent[x]
    
    def union(self, x: int, y: int) -> bool:
        root_x = self.find(x)
        root_y = self.find(y)
        
        if root_x == root_y:
            return False
        
        # 按秩合并
        if self.rank[root_x] < self.rank[root_y]:
            root_x, root_y = root_y, root_x
        
        self.parent[root_y] = root_x
        self.rank[root_x] += self.rank[root_y]
        self.count -= 1
        return True

def numIslands_dfs(grid: List[List[str]]) -> int:
    """DFS递归解法"""
    if not grid or not grid[0]:
        return 0
    
    m, n = len(grid), len(grid[0])
    
    def dfs(i: int, j: int):
        if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] == '0':
            return
        
        grid[i][j] = '0'
        dfs(i - 1, j)  # 上
        dfs(i + 1, j)  # 下
        dfs(i, j - 1)  # 左
        dfs(i, j + 1)  # 右
    
    count = 0
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                count += 1
                dfs(i, j)
    
    return count

def numIslands_bfs(grid: List[List[str]]) -> int:
    """BFS队列解法"""
    if not grid or not grid[0]:
        return 0
    
    m, n = len(grid), len(grid[0])
    directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    
    def bfs(i: int, j: int):
        queue = deque()
        queue.append((i, j))
        grid[i][j] = '0'
        
        while queue:
            x, y = queue.popleft()
            for dx, dy in directions:
                nx, ny = x + dx, y + dy
                if 0 <= nx < m and 0 <= ny < n and grid[nx][ny] == '1':
                    queue.append((nx, ny))
                    grid[nx][ny] = '0'
    
    count = 0
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                count += 1
                bfs(i, j)
    
    return count

def numIslands_union_find(grid: List[List[str]]) -> int:
    """并查集解法"""
    if not grid or not grid[0]:
        return 0
    
    m, n = len(grid), len(grid[0])
    
    # 统计陆地数量
    land_count = 0
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                land_count += 1
    
    if land_count == 0:
        return 0
    
    uf = UnionFind(m * n)
    uf.count = land_count
    
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '0':
                continue
            
            idx = i * n + j
            
            # 只检查右侧和下侧邻居(避免重复合并)
            if j + 1 < n and grid[i][j + 1] == '1':
                uf.union(idx, i * n + (j + 1))
            
            if i + 1 < m and grid[i + 1][j] == '1':
                uf.union(idx, (i + 1) * n + j)
    
    return uf.count

# ========== 课程表问题 ==========

def canFinish_kahn(numCourses: int, prerequisites: List[List[int]]) -> bool:
    """Kahn算法(BFS + 入度表)"""
    # 构建邻接表和入度数组
    graph = defaultdict(list)
    indegree = [0] * numCourses
    
    for a, b in prerequisites:
        graph[b].append(a)
        indegree[a] += 1
    
    # 初始化队列
    queue = deque([i for i in range(numCourses) if indegree[i] == 0])
    visited = 0
    
    while queue:
        u = queue.popleft()
        visited += 1
        
        for v in graph[u]:
            indegree[v] -= 1
            if indegree[v] == 0:
                queue.append(v)
    
    return visited == numCourses

def canFinish_dfs(numCourses: int, prerequisites: List[List[int]]) -> bool:
    """DFS三色标记法"""
    # 构建邻接表
    graph = defaultdict(list)
    for a, b in prerequisites:
        graph[b].append(a)
    
    visited = [0] * numCourses  # 0=未访问,1=访问中,2=已完成
    
    def dfs(u: int) -> bool:
        visited[u] = 1  # 标记为访问中
        
        for v in graph[u]:
            if visited[v] == 0:
                if not dfs(v):
                    return False
            elif visited[v] == 1:
                return False  # 存在环
        
        visited[u] = 2  # 标记为已完成
        return True
    
    for i in range(numCourses):
        if visited[i] == 0:
            if not dfs(i):
                return False
    
    return True

# ========== 测试代码 ==========

def test_islands():
    """测试岛屿数量问题"""
    grid1 = [
        ["1", "1", "0", "0", "0"],
        ["1", "1", "0", "0", "0"],
        ["0", "0", "1", "0", "0"],
        ["0", "0", "0", "1", "1"]
    ]
    
    # 注意:每种解法会修改原网格,需要复制测试
    import copy
    
    # 测试DFS
    grid = copy.deepcopy(grid1)
    print(f"DFS解法: {numIslands_dfs(grid)}")  # 期望: 3
    
    # 测试BFS
    grid = copy.deepcopy(grid1)
    print(f"BFS解法: {numIslands_bfs(grid)}")  # 期望: 3
    
    # 测试并查集
    grid = copy.deepcopy(grid1)
    print(f"并查集解法: {numIslands_union_find(grid)}")  # 期望: 3

def test_courses():
    """测试课程表问题"""
    # 无环情况
    numCourses1 = 2
    prerequisites1 = [[1, 0]]
    
    # 有环情况
    numCourses2 = 2
    prerequisites2 = [[1, 0], [0, 1]]
    
    print(f"Kahn算法无环: {canFinish_kahn(numCourses1, prerequisites1)}")  # 期望: True
    print(f"Kahn算法有环: {canFinish_kahn(numCourses2, prerequisites2)}")  # 期望: False
    
    print(f"DFS三色法无环: {canFinish_dfs(numCourses1, prerequisites1)}")  # 期望: True
    print(f"DFS三色法有环: {canFinish_dfs(numCourses2, prerequisites2)}")  # 期望: False

if __name__ == "__main__":
    print("=== 岛屿数量问题测试 ===")
    test_islands()
    print("\n=== 课程表问题测试 ===")
    test_courses()

4.2 边界条件与优化技巧

岛屿数量问题的边界处理

  1. 空网格检查if not grid or not grid[0]: return 0
  2. 坐标越界检查 :在递归/循环前检查 0 <= x < m and 0 <= y < n
  3. 原地修改优化 :直接修改 grid[i][j] = '0',节省 visited 数组空间

课程表问题的边界处理

  1. 空依赖列表if not prerequisites: return True
  2. 课程数较少 :当 numCourses <= 1 时直接返回 True
  3. 构建图时的方向 :注意依赖关系是 b → a(先修b才能学a)

通用优化技巧

  1. 方向数组统一管理directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
  2. 尽早返回:检测到环时立即返回,避免不必要的遍历
  3. 合理使用数据结构 :邻接表用 defaultdict(list),队列用 deque

4.3 算法复杂度总结

岛屿数量问题

  • DFS/BFS :时间复杂度 O ( m × n ) O(m \times n) O(m×n),空间复杂度 O ( m × n ) O(m \times n) O(m×n)(最坏递归栈)
  • 并查集 :时间复杂度 O ( m × n × α ) O(m \times n \times \alpha) O(m×n×α),空间复杂度 O ( m × n ) O(m \times n) O(m×n)

课程表问题

  • Kahn算法 :时间复杂度 O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣),空间复杂度 O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣)
  • DFS三色法 :时间复杂度 O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣),空间复杂度 O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣)(递归栈)

时间复杂度对比

  • 网格问题: O ( 单元格数 ) O(\text{单元格数}) O(单元格数)
  • 图问题: O ( 节点数 + 边数 ) O(\text{节点数} + \text{边数}) O(节点数+边数)

5. 面试考点与进阶拓展

5.1 高频面试考点

岛屿数量问题的面试要点

  1. 算法思想:连通分量计数,图遍历应用
  2. 实现细节:DFS递归终止条件,BFS入队标记时机
  3. 优化思路:原地修改,方向遍历简化
  4. 复杂度分析:时间/空间复杂度推导
  5. 变式讨论:不同解法的适用场景

课程表问题的面试要点

  1. 问题建模:将课程依赖抽象为有向图
  2. 环检测原理:拓扑排序与环的关系
  3. 算法对比:Kahn算法与DFS三色法的异同
  4. 正确性证明:为何拓扑排序能检测环
  5. 扩展应用:输出拓扑序列的具体实现

5.2 相似题目推荐

岛屿数量相似题

  1. 695. 岛屿的最大面积:在统计岛屿数量的基础上,记录每个岛屿的面积
  2. 463. 岛屿的周长:计算岛屿的边界长度
  3. 130. 被围绕的区域:类似沉岛思想,处理边界条件
  4. 694. 不同岛屿的数量:记录岛屿形状,需要序列化

课程表相似题

  1. 210. 课程表 II:在判断是否可完成的基础上,输出拓扑序列
  2. 630. 课程表 III:带时间限制的课程安排问题
  3. 1462. 课程表 IV:查询任意两门课程是否有先修关系
  4. 1136. 平行课程:最少学期完成所有课程

5.3 进阶拓展方向

图算法进阶学习

  1. 最短路径算法:Dijkstra、Bellman-Ford、Floyd-Warshall
  2. 最小生成树:Kruskal、Prim算法
  3. 网络流:最大流、最小割问题
  4. 强连通分量:Kosaraju、Tarjan算法
  5. 二分图匹配:匈牙利算法、最大流应用

图神经网络(GNN)入门

  1. 基础概念:节点嵌入、图卷积网络(GCN)
  2. 应用场景:社交网络分析、分子结构预测
  3. 学习资源:PyTorch Geometric、DGL框架

工程实践建议

  1. 图数据库应用:Neo4j、JanusGraph在图算法中的应用
  2. 分布式图计算:Spark GraphX、Flink Gelly
  3. 性能调优:大规模图数据的存储与计算优化

5.4 面试回答模板

岛屿数量问题回答模板

复制代码
"这道题本质是统计无向图中连通分量的数量。我首先想到用DFS递归实现,因为代码简洁直观。具体思路是:遍历网格,遇到未访问的陆地时,岛屿计数加1,然后通过DFS将整个岛屿的陆地标记为已访问(原地修改为'0')。时间复杂度是O(mn),每个单元格最多访问一次。如果面试官担心递归栈溢出,可以改用BFS队列实现,空间复杂度优化为O(min(m,n))。如果需要支持动态更新,还可以用并查集实现。"

课程表问题回答模板

复制代码
"这道题本质是判断有向图是否存在环。我可以用拓扑排序来解决。一种实现是Kahn算法:构建入度表,将入度为0的节点入队,然后不断出队并减少后继节点的入度。若最终处理的节点数等于总节点数,则无环,否则有环。时间复杂度O(V+E)。另一种实现是DFS三色标记法:用三种状态标记节点,若在DFS过程中遇到状态为'访问中'的邻居,说明存在环。两种方法各有优劣,Kahn算法天然支持输出拓扑序列,DFS三色法代码更简洁。"

5.5 总结

图算法是算法面试的核心难点,岛屿数量和课程表是其中的经典代表。通过本文的系统讲解,希望读者能够:

  1. 掌握基础:理解图的基本概念和遍历算法
  2. 深入原理:掌握DFS/BFS/并查集/拓扑排序的实现细节
  3. 灵活应用:根据不同场景选择合适的算法解法
  4. 应对面试:熟悉高频考点和回答技巧

图算法的学习需要理论与实践相结合,建议读者在理解本文内容的基础上,完成力扣hot100中相关题目的练习,逐步构建完整的图算法知识体系。

相关推荐
VALENIAN瓦伦尼安教学设备2 分钟前
设备对中不良的危害
数据库·嵌入式硬件·算法
m0_564914929 分钟前
AI学习课堂网站丨OPENMAIC丨清华团队开源项目
学习
不熬夜的熬润之25 分钟前
APCE-平均峰值相关能量
人工智能·算法·计算机视觉
yzx99101327 分钟前
实时数据流处理实战:从滑动窗口算法到Docker部署
算法·docker·容器
开源盛世!!33 分钟前
3.26-3.27学习笔记
笔记·学习
佩奇大王1 小时前
P674 三羊献瑞
算法·深度优先·图论
wertyuytrewm1 小时前
Java面试——Java基础
java·jvm·面试
studyForMokey1 小时前
【Android面试】View绘制流程专题
android·面试·职场和发展
发疯幼稚鬼1 小时前
大整数乘法运算
c语言·算法
宵时待雨2 小时前
C++笔记归纳17:哈希
数据结构·c++·笔记·算法·哈希算法