图论基础原理与题目说明
文章目录
- 图论基础原理与题目说明
-
- [一、 图论基本概念](#一、 图论基本概念)
-
- [1.1 图的核心定义](#1.1 图的核心定义)
- [1.2 图的存储方式](#1.2 图的存储方式)
- [二、 图的遍历核心:DFS 与 BFS](#二、 图的遍历核心:DFS 与 BFS)
-
- [2.1 DFS(深度优先遍历)](#2.1 DFS(深度优先遍历))
- [2.2 BFS(广度优先遍历)](#2.2 BFS(广度优先遍历))
- [三、 高阶概念:有向图与 Trie 树](#三、 高阶概念:有向图与 Trie 树)
-
- [3.1 入度与出度(拓扑排序前置知识)](#3.1 入度与出度(拓扑排序前置知识))
- [3.2 Trie(前缀树 / 字典树)](#3.2 Trie(前缀树 / 字典树))
- [四、 图论算法实战演练](#四、 图论算法实战演练)
-
- [[200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/)](#200. 岛屿数量)
- [[994. 腐烂的橘子](https://leetcode.cn/problems/rotting-oranges/)](#994. 腐烂的橘子)
- [[207. 课程表](https://leetcode.cn/problems/course-schedule/)](#207. 课程表)
- [[208. 实现 Trie (前缀树)](https://leetcode.cn/problems/implement-trie-prefix-tree/)](#208. 实现 Trie (前缀树))
🔗 查看完整专栏(LeetCode基础算法专栏)

特别说明:
本文为个人的 LeetCode 刷题与学习笔记,内容仅供学习与交流使用,禁止转载或用于商业用途。需要强调的是,文中的题目解法不一定是最优解(可能存在时间或空间复杂度的进一步优化空间),主要目的是分享个人的解题思路与逻辑实现,仅供参考。 笔记内容为个人理解与总结,可能存在疏漏或偏差,欢迎读者自行甄别并交流探讨。
一、 图论基本概念
1.1 图的核心定义
图是由顶点(节点)**和**边 组成的结构(记为 G = ( V , E ) G=(V,E) G=(V,E))。
| 分类维度 | 类型 | 特点(刷题关注点) |
|---|---|---|
| 边的方向 | 无向图 | 边无方向(如社交网络的好友关系) |
| 有向图 | 边有方向(如道路单行道、任务依赖关系) | |
| 边的权重 | 无权图 | 边无值(仅表示连通状态) |
| 有权图 | 边有值(如两点间的距离、费用、时间等) | |
| 环的存在 | 无环图 | 无闭环(如经典的 DAG:有向无环图) |
| 有环图 | 存在闭环(遍历时必须加标记以避免死循环) |
1.2 图的存储方式
- 邻接表:用数组或哈希表存储每个节点的相邻节点列表(空间复杂度通常更优,最常用)。
- 邻接矩阵 :二维数组
matrix[i][j]。值为 1 代表相连,0 代表不连;若为有权图则直接存储权重值。
二、 图的遍历核心:DFS 与 BFS
图的遍历是所有图论算法的基石,本质上与二叉树的遍历同源。唯一且最致命的区别在于:图可能存在环,必须利用 visited 标记访问状态,防止无限死循环。
2.1 DFS(深度优先遍历)
- 核心思路:从起点出发,沿着一条路径走到黑(不撞南墙不回头),再回溯走其他路径(同二叉树的前序/后序递归逻辑)。
- 基础模板(邻接表 + 递归):
py
def dfs(node, adj, visited):
visited[node] = True # 标记当前节点已访问
# print(node) # 访问节点(执行业务逻辑)
# 遍历所有相邻节点
for neighbor in adj[node]:
if not visited[neighbor]:
dfs(neighbor, adj, visited)
2.2 BFS(广度优先遍历)
- 核心思路:从起点出发,像水波纹一样,先访问所有直接相邻的节点(一层),再访问相邻节点的相邻节点(下一层)(同二叉树的层序遍历逻辑)。
- 基础模板(邻接表 + 队列):
py
from collections import deque
def bfs(start, adj, visited):
q = deque([start])
visited[start] = True
while q:
node = q.popleft()
# print(node) # 访问节点
for neighbor in adj[node]:
if not visited[neighbor]:
visited[neighbor] = True
q.append(neighbor)
三、 高阶概念:有向图与 Trie 树
3.1 入度与出度(拓扑排序前置知识)
在有向图中,入度 与出度是描述「节点与边」关系的核心指标。
| 概念 | 通俗定义 | 数学定义 |
|---|---|---|
| 入度 | 指向当前节点的边的数量(有多少个 "前驱" 指向我) | i n _ d e g r e e ( u ) in\_degree(u) in_degree(u) = 以 u u u 为终点的边数 |
| 出度 | 从当前节点出发的边的数量(我有多少个 "后继") | o u t _ d e g r e e ( u ) out\_degree(u) out_degree(u) = 以 u u u 为起点的边数 |
- 入度为 0 的节点:没有任何前驱依赖,可以直接作为起点开始。
- 出度为 0 的节点:没有任何后继依赖,是终点。
- 拓扑排序 :把有向无环图(DAG)的节点排成一个序列,满足 "所有边的起点必须排在终点的前面"。若图中有环,则无法完成拓扑排序(因为永远找不到入度为 0 的起始节点)。
3.2 Trie(前缀树 / 字典树)
Trie 是专门用于高效存储和检索字符串集合的树形数据结构。
- 核心优势 :「共享字符串前缀」。例如存储
"app"和"apple"时,前缀"app"会被共享,大幅节省空间。检索时间仅与目标字符串长度相关,与集合大小无关。 - 节点设计 :
children:字典(key=字符,value=子节点),存储当前节点的所有分支。is_end:布尔值,标记该节点是否是某个完整单词的结尾。
四、 图论算法实战演练
200. 岛屿数量
题目描述:
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
解题思路:
选用DFS(深度优先搜索)嵌套递归法。
遍历网格,遇到未被访问的陆地('1')时,启动一次 DFS 递归。在 DFS 过程中,将该陆地及其所有相连的陆地原地修改为水('0') 。这相当于利用原网格代替了 visited 数组。每启动一次 DFS 就代表消灭并找到了一个独立岛屿,最终统计 DFS 的启动次数即可。
核心代码:
py
from typing import List
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
if not grid or not grid[0]:
return 0
rows = len(grid)
cols = len(grid[0])
ans = 0
# 定义 DFS 函数,作用是淹没 (x,y) 所在的整个相连岛屿
def dfs(x, y):
# 递归终止条件:越界,或者当前单元格已经是水
if not 0 <= x < rows or not 0 <= y < cols or grid[x][y] == '0':
return
# 核心操作:将当前陆地标记为水(替代 visited 数组)
grid[x][y] = '0'
# 递归遍历上下左右,淹没相连陆地
dfs(x - 1, y)
dfs(x + 1, y)
dfs(x, y - 1)
dfs(x, y + 1)
# 遍历网格寻找岛屿起点
for i in range(rows):
for j in range(cols):
if grid[i][j] == '1':
ans += 1 # 发现新岛屿
dfs(i, j) # 启动 DFS 淹没它
return ans
994. 腐烂的橘子
题目描述:
每分钟,腐烂的橘子周围 4 个方向上相邻的新鲜橘子都会腐烂。返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能全腐烂,返回 -1。
解题思路:
-
「同时腐烂」说明过程是分层扩散的;
-
「多个初始腐烂点」说明需从多个起点同时开始。
这完美契合 多源 BFS 算法:
- 初始化:将所有初始腐烂橘子的坐标加入队列,并统计新鲜橘子的总数。
- 分层 BFS:每一层循环代表"过了一分钟"。逐个取出队列中的腐烂橘子向四周扩散,将新鲜橘子腐烂并加入下一层队列,同时新鲜橘子总数减一。
- 结果判定 :若最终新鲜橘子数 > 0 >0 >0,返回 -1;否则返回统计的分钟数。
核心代码:
py
import collections
from typing import List
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
if not grid or not grid[0]:
return 0
rows = len(grid)
cols = len(grid[0])
queue = collections.deque()
fresh_num = 0
time = 0
# 1. 收集初始腐烂橘子(多源起点)并统计新鲜橘子
for i in range(rows):
for j in range(cols):
if grid[i][j] == 1:
fresh_num += 1
elif grid[i][j] == 2:
queue.append((i, j))
dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# 2. 分层 BFS 模拟腐烂过程
while queue and fresh_num > 0:
level_size = len(queue)
for _ in range(level_size):
x, y = queue.popleft()
for dx, dy in dirs:
nx, ny = x + dx, y + dy
# 发现相邻的新鲜橘子
if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == 1:
grid[nx][ny] = 2
fresh_num -= 1
queue.append((nx, ny))
time += 1
return time if fresh_num == 0 else -1
207. 课程表
题目描述:
判断是否可能完成所有课程的学习,课程之间存在先修依赖关系。本质是判断有向图中是否存在环。
解题思路:
使用基于入度 的 BFS 拓扑排序:
- 构建图 :构建邻接表
adj记录每门课的后续课程,构建in_degree数组记录每门课的前置条件数量。 - 初始化队列:将所有入度为 0(无前置依赖)的课程加入队列。
- BFS 模拟修课:取出队列中的课(算作修完一门)。然后将其所有后续课程的入度减 1。若后续课程的入度降为 0,说明其前置条件已全部满足,加入队列。
- 结果判定:最后统计修完的课程数,若等于总课数则说明无环(可以完成选课)。
核心代码:
py
import collections
from typing import List
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
# 1. 初始化邻接表和入度数组
adj = [[] for _ in range(numCourses)]
in_degree = [0] * numCourses
for a, b in prerequisites:
adj[b].append(a) # b -> a:修 a 需先修 b
in_degree[a] += 1
# 2. 收集所有入度为 0 的节点(可以直接学的课)
queue = collections.deque()
for i in range(numCourses):
if in_degree[i] == 0:
queue.append(i)
# 3. BFS 模拟选课过程
processed = 0
while queue:
course = queue.popleft()
processed += 1
for next_course in adj[course]:
in_degree[next_course] -= 1
# 前置条件全部满足,可以开始学了
if in_degree[next_course] == 0:
queue.append(next_course)
return processed == numCourses
208. 实现 Trie (前缀树)
解题思路:
根据前述理论,构建 TrieNode 辅助类,随后实现树的初始化、插入和查询逻辑。对于 startsWith,只需路径存在即可返回 True,无需校验 is_end。
核心代码:
py
class TrieNode:
"""定义 Trie 的节点类"""
def __init__(self):
self.children = {} # key: 字符, value: 对应的 TrieNode
self.is_end = False # 标记是否是单词结尾
class Trie:
def __init__(self):
self.root = TrieNode() # 根节点不存储实际字符
def insert(self, word: str) -> None:
current_node = self.root
for char in word:
# 字符不存在对应分支则创建
if char not in current_node.children:
current_node.children[char] = TrieNode()
current_node = current_node.children[char]
# 单词插入完毕,打上结尾标记
current_node.is_end = True
def search(self, word: str) -> bool:
current_node = self.root
for char in word:
if char not in current_node.children:
return False
current_node = current_node.children[char]
# 必须是某个完整单词的结尾
return current_node.is_end
def startsWith(self, prefix: str) -> bool:
current_node = self.root
for char in prefix:
if char not in current_node.children:
return False
current_node = current_node.children[char]
# 只要存在这条路径即可,无需判断 is_end
return True