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 图算法面试核心考点
- 连通分量计数:统计图中连通区域的数量(如岛屿数量问题)
- 环检测:判断有向图或无向图中是否存在环(如课程表问题)
- 拓扑排序:对有向无环图的顶点进行线性排序
- 最短路径:寻找两点间权重最小的路径
- 最小生成树:寻找连接所有顶点的最小权重子图
本文聚焦前两个核心考点,通过力扣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.lengthn == grid[i].length1 <= m, n <= 300grid[i][j]的值为'0'或'1'
2.2 问题本质与建模
核心洞察 :将网格视为图,每个陆地单元格('1')是图中的一个节点,上下左右四个方向的相邻陆地之间有一条边。问题转化为统计无向图中连通分量的数量。
网格图的特点:
- 顶点数:陆地单元格的数量
- 边数:相邻陆地关系的数量(最多为每个陆地与4个邻居的连接)
- 图结构隐含在网格坐标中,无需显式构建邻接表
2.3 解法一:DFS递归实现(沉岛法)
算法思想 :遍历网格,当遇到未访问的陆地时,岛屿计数加1,并通过DFS递归将该岛屿的所有陆地标记为已访问(原地修改为 '0')。
实现步骤:
- 处理边界条件:网格为空则返回0
- 遍历每个网格单元格
(i, j) - 若
grid[i][j] == '1':- 岛屿计数
count += 1 - 调用
dfs(i, j)淹没整个岛屿
- 岛屿计数
- 返回
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队列实现
算法思想:用队列替代递归,实现层次遍历,同样采用沉岛策略。
实现步骤:
- 初始化队列,将起点入队时立即标记为已访问
- 当队列非空时:
- 出队当前节点
- 遍历四个方向的邻居
- 若邻居是未访问的陆地,则入队并标记
- 重复步骤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)优化
算法思想:将每个陆地单元格视为独立集合,遍历网格合并相邻陆地,最终统计集合数量。
并查集核心操作:
- 初始化:每个节点的父节点指向自己
- 查找(Find):带路径压缩,找到节点的根节点
- 合并(Union):按秩合并,将小树挂到大树下
实现步骤:
- 统计陆地总数,初始化并查集
- 遍历网格,对每个陆地单元格:
- 将其与右侧、下方邻居合并(避免重复合并上、左方向)
- 统计并查集中集合数量
并查集类设计:
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) |
| 适用场景 | 网格不大、代码简洁 | 超大网格、避免栈溢出 | 动态连通性、组件计数 |
| 面试评价 | ⭐⭐⭐⭐⭐(首选) | ⭐⭐⭐⭐(实用性强) | ⭐⭐⭐(展示数据结构功底) |
选择指南:
- 面试首选:DFS递归,代码最简洁,逻辑最直观
- 工程实践:BFS队列,避免递归栈溢出,行为稳定
- 高级场景:并查集,需要支持动态更新或频繁查询
3. 题目二:课程表拓扑排序实战
3.1 问题描述
题目 :207. 课程表
总共有 numCourses 门课程需要学习,编号从 0 到 numCourses - 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 <= 20000 <= prerequisites.length <= 5000prerequisites[i].length == 20 <= a_i, b_i < numCourses- 所有课程对互不相同
3.2 问题本质与建模
核心洞察 :将课程视为图中的顶点,先修关系 [a, b] 表示从 b 到 a 的一条有向边。问题转化为判断有向图中是否存在环。
拓扑排序定义 :对有向无环图(DAG)的顶点进行线性排序,使得对于任意有向边 ( u , v ) (u, v) (u,v),顶点 u u u 在排序中都出现在顶点 v v v 的前面。
关键结论:
- 有向图存在拓扑排序 ⇔ \Leftrightarrow ⇔ 图是DAG(没有环)
- 课程可以完成 ⇔ \Leftrightarrow ⇔ 课程依赖图是DAG
3.3 解法一:Kahn算法(BFS + 入度表)
算法思想:基于BFS,维护入度表和队列,不断选择入度为0的节点,减少其后继节点的入度。
Kahn算法步骤:
- 构建图结构 :
- 邻接表:存储每个节点的后继节点
- 入度数组:记录每个节点的入度数(依赖数)
- 初始化队列:将所有入度为0的节点入队
- BFS循环 :
- 出队节点
u,将其加入拓扑序列 - 遍历
u的所有后继节点v:- 将
v的入度减1 - 若
v的入度变为0,入队
- 将
- 出队节点
- 结果判断 :
- 若拓扑序列长度等于节点总数,则无环(可完成)
- 否则存在环(不可完成)
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三色算法步骤:
- 构建邻接表:存储图的边关系
- 初始化状态数组:所有节点标记为0(未访问)
- DFS遍历:对每个未访问节点启动DFS
- 环检测逻辑 :
- 若遇到状态为1的邻居节点,说明存在回边(环)
- 递归结束时,将当前节点标记为2
- 结果判断:遍历过程中未检测到环,则可完成
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 |
| 输出拓扑序 | 天然支持(出队顺序) | 需要额外栈(后序逆序) |
| 适用场景 | 需要拓扑序列、大图 | 代码简洁、理论考察 |
| 面试评价 | ⭐⭐⭐⭐(实用性强) | ⭐⭐⭐⭐⭐(高频考察) |
选择指南:
- 需要拓扑序列:Kahn算法,可直接输出学习顺序
- 理论深度考察:DFS三色法,体现图遍历和环检测理解
- 大图避免递归: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 边界条件与优化技巧
岛屿数量问题的边界处理:
- 空网格检查 :
if not grid or not grid[0]: return 0 - 坐标越界检查 :在递归/循环前检查
0 <= x < m and 0 <= y < n - 原地修改优化 :直接修改
grid[i][j] = '0',节省visited数组空间
课程表问题的边界处理:
- 空依赖列表 :
if not prerequisites: return True - 课程数较少 :当
numCourses <= 1时直接返回True - 构建图时的方向 :注意依赖关系是
b → a(先修b才能学a)
通用优化技巧:
- 方向数组统一管理 :
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] - 尽早返回:检测到环时立即返回,避免不必要的遍历
- 合理使用数据结构 :邻接表用
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 高频面试考点
岛屿数量问题的面试要点:
- 算法思想:连通分量计数,图遍历应用
- 实现细节:DFS递归终止条件,BFS入队标记时机
- 优化思路:原地修改,方向遍历简化
- 复杂度分析:时间/空间复杂度推导
- 变式讨论:不同解法的适用场景
课程表问题的面试要点:
- 问题建模:将课程依赖抽象为有向图
- 环检测原理:拓扑排序与环的关系
- 算法对比:Kahn算法与DFS三色法的异同
- 正确性证明:为何拓扑排序能检测环
- 扩展应用:输出拓扑序列的具体实现
5.2 相似题目推荐
岛屿数量相似题:
- 695. 岛屿的最大面积:在统计岛屿数量的基础上,记录每个岛屿的面积
- 463. 岛屿的周长:计算岛屿的边界长度
- 130. 被围绕的区域:类似沉岛思想,处理边界条件
- 694. 不同岛屿的数量:记录岛屿形状,需要序列化
课程表相似题:
- 210. 课程表 II:在判断是否可完成的基础上,输出拓扑序列
- 630. 课程表 III:带时间限制的课程安排问题
- 1462. 课程表 IV:查询任意两门课程是否有先修关系
- 1136. 平行课程:最少学期完成所有课程
5.3 进阶拓展方向
图算法进阶学习:
- 最短路径算法:Dijkstra、Bellman-Ford、Floyd-Warshall
- 最小生成树:Kruskal、Prim算法
- 网络流:最大流、最小割问题
- 强连通分量:Kosaraju、Tarjan算法
- 二分图匹配:匈牙利算法、最大流应用
图神经网络(GNN)入门:
- 基础概念:节点嵌入、图卷积网络(GCN)
- 应用场景:社交网络分析、分子结构预测
- 学习资源:PyTorch Geometric、DGL框架
工程实践建议:
- 图数据库应用:Neo4j、JanusGraph在图算法中的应用
- 分布式图计算:Spark GraphX、Flink Gelly
- 性能调优:大规模图数据的存储与计算优化
5.4 面试回答模板
岛屿数量问题回答模板:
"这道题本质是统计无向图中连通分量的数量。我首先想到用DFS递归实现,因为代码简洁直观。具体思路是:遍历网格,遇到未访问的陆地时,岛屿计数加1,然后通过DFS将整个岛屿的陆地标记为已访问(原地修改为'0')。时间复杂度是O(mn),每个单元格最多访问一次。如果面试官担心递归栈溢出,可以改用BFS队列实现,空间复杂度优化为O(min(m,n))。如果需要支持动态更新,还可以用并查集实现。"
课程表问题回答模板:
"这道题本质是判断有向图是否存在环。我可以用拓扑排序来解决。一种实现是Kahn算法:构建入度表,将入度为0的节点入队,然后不断出队并减少后继节点的入度。若最终处理的节点数等于总节点数,则无环,否则有环。时间复杂度O(V+E)。另一种实现是DFS三色标记法:用三种状态标记节点,若在DFS过程中遇到状态为'访问中'的邻居,说明存在环。两种方法各有优劣,Kahn算法天然支持输出拓扑序列,DFS三色法代码更简洁。"
5.5 总结
图算法是算法面试的核心难点,岛屿数量和课程表是其中的经典代表。通过本文的系统讲解,希望读者能够:
- 掌握基础:理解图的基本概念和遍历算法
- 深入原理:掌握DFS/BFS/并查集/拓扑排序的实现细节
- 灵活应用:根据不同场景选择合适的算法解法
- 应对面试:熟悉高频考点和回答技巧
图算法的学习需要理论与实践相结合,建议读者在理解本文内容的基础上,完成力扣hot100中相关题目的练习,逐步构建完整的图算法知识体系。