跳到主要内容

为什么图很重要

图用于建模实体之间的关系——无数现实世界系统的基础:

  • 社交网络:朋友、关注者、连接(Facebook、Twitter、LinkedIn)
  • 地图与导航:道路、交叉口、路线(Google Maps)
  • 网络拓扑:计算机网络、服务器连接
  • 依赖图:包管理器、构建系统、任务调度
  • 知识图谱:Wikipedia 链接、概念关系

实际影响:Google 的 PageRank 算法使用图遍历来对网页排序。 Dijkstra 算法为 GPS 导航系统在数百万节点中毫秒级找到最优路线。

核心概念

图的结构

Graph = (V, E) 其中 V = 顶点(节点), E = 边

关键术语

  • 顶点(Vertex/Node):基本单元
  • 边(Edge):两个顶点之间的连接
  • 度(Degree):与顶点相关联的边数
  • 路径(Path):相连顶点的序列
  • 环(Cycle):起点和终点相同的路径
  • 连通(Connected):每个顶点都可以从其他任何顶点到达

图的类型

类型方向示例
无向图无箭头双向社交网络好友关系
有向图有箭头单向Twitter 关注
有权图有权重/代价任一道路网络(距离)
无权图无权重任一网页链接

图的表示

邻接矩阵

int[][] graph = new int[V][V];  // V = 顶点数
graph[u][v] = weight; // 从 u 到 v 的边,权重为 weight
优点缺点
O(1) 边查找O(V²) 空间
实现简单稀疏图浪费空间
适合稠密图遍历邻居慢

邻接表

List<List<int[]>> graph = new ArrayList<>();
for (int i = 0; i < V; i++) {
graph.add(new ArrayList<>());
}
graph.get(u).add(new int[]{v, weight}); // 边 u→v
优点缺点
O(V + E) 空间O(度) 邻居查找
高效迭代稍复杂
适合稀疏图

建议:大多数情况使用邻接表(大多数现实图是稀疏的)。

深入理解

图遍历

广度优先搜索(BFS)

逐层探索,使用队列:

public void bfs(int start, int V, List<List<Integer>> graph) {
boolean[] visited = new boolean[V];
Queue<Integer> queue = new LinkedList<>();

visited[start] = true;
queue.offer(start);

while (!queue.isEmpty()) {
int node = queue.poll();
System.out.print(node + " ");

for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.offer(neighbor);
}
}
}
}

BFS 顺序:1, 2, 3, 4, 5(逐层)

使用场景

  • 无权图最短路径
  • 层序遍历
  • 连通分量

复杂度:O(V + E) 时间, O(V) 空间

深度优先搜索(DFS)

尽可能深入探索,使用栈/递归:

// 递归 DFS
public void dfs(int node, boolean[] visited, List<List<Integer>> graph) {
visited[node] = true;
System.out.print(node + " ");

for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
dfs(neighbor, visited, graph);
}
}
}

// 迭代 DFS
public void dfsIterative(int start, int V, List<List<Integer>> graph) {
boolean[] visited = new boolean[V];
Deque<Integer> stack = new ArrayDeque<>();

stack.push(start);

while (!stack.isEmpty()) {
int node = stack.pop();

if (!visited[node]) {
visited[node] = true;
System.out.print(node + " ");

// 反序压入邻居
for (int i = graph.get(node).size() - 1; i >= 0; i--) {
int neighbor = graph.get(node).get(i);
if (!visited[neighbor]) {
stack.push(neighbor);
}
}
}
}
}

DFS 顺序:1, 2, 4, 5, 3(深度优先)

使用场景

  • 检测环
  • 拓扑排序
  • 路径查找
  • 连通分量

复杂度:O(V + E) 时间, O(V) 空间

BFS vs DFS

方面BFSDFS
数据结构队列栈 / 递归
内存宽图 O(V)深图 O(h)
最短路径是(无权图)
探索方式逐层逐分支
实现迭代(自然)递归(自然)

环检测

无向图

public boolean hasCycleUndirected(int V, List<List<Integer>> graph) {
boolean[] visited = new boolean[V];

for (int i = 0; i < V; i++) {
if (!visited[i]) {
if (dfsCycleUndirected(i, -1, visited, graph)) {
return true;
}
}
}

return false;
}

private boolean dfsCycleUndirected(int node, int parent,
boolean[] visited, List<List<Integer>> graph) {
visited[node] = true;

for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
if (dfsCycleUndirected(neighbor, node, visited, graph)) {
return true;
}
} else if (neighbor != parent) {
return true; // 访问了非父节点的节点 = 有环
}
}

return false;
}

有向图

public boolean hasCycleDirected(int V, List<List<Integer>> graph) {
int[] state = new int[V]; // 0=未访问, 1=访问中, 2=已完成

for (int i = 0; i < V; i++) {
if (state[i] == 0) {
if (dfsCycleDirected(i, state, graph)) {
return true;
}
}
}

return false;
}

private boolean dfsCycleDirected(int node, int[] state,
List<List<Integer>> graph) {
state[node] = 1; // 标记为访问中

for (int neighbor : graph.get(node)) {
if (state[neighbor] == 1) {
return true; // 回边 = 有环
}
if (state[neighbor] == 0 && dfsCycleDirected(neighbor, state, graph)) {
return true;
}
}

state[node] = 2; // 标记为已完成
return false;
}

拓扑排序

仅适用于有向无环图(DAG)

public List<Integer> topologicalSort(int V, List<List<Integer>> graph) {
List<Integer> order = new ArrayList<>();
boolean[] visited = new boolean[V];

for (int i = 0; i < V; i++) {
if (!visited[i]) {
dfsTopo(i, visited, graph, order);
}
}

Collections.reverse(order); // 反转后序
return order;
}

private void dfsTopo(int node, boolean[] visited,
List<List<Integer>> graph, List<Integer> order) {
visited[node] = true;

for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
dfsTopo(neighbor, visited, graph, order);
}
}

order.add(node); // 完成后添加
}

使用场景

  • 构建系统(Maven、Gradle)
  • 课程先修关系
  • 任务调度
  • 编译器

Dijkstra 算法(最短路径)

带权图最短路径算法:

public int[] dijkstra(int V, List<List<int[]>> graph, int source) {
int[] dist = new int[V];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[source] = 0;

PriorityQueue<int[]> minHeap =
new PriorityQueue<>((a, b) -> a[1] - b[1]);
minHeap.offer(new int[]{source, 0});

while (!minHeap.isEmpty()) {
int[] current = minHeap.poll();
int node = current[0], d = current[1];

if (d > dist[node]) continue; // 已找到更短路径

for (int[] edge : graph.get(node)) {
int neighbor = edge[0], weight = edge[1];
int newDist = dist[node] + weight;

if (newDist < dist[neighbor]) {
dist[neighbor] = newDist;
minHeap.offer(new int[]{neighbor, newDist});
}
}
}

return dist;
}

关键点

  • 贪心:总是扩展最近的未访问节点
  • 需要优先队列(最小堆)
  • 不处理负权边(用 Bellman-Ford 代替)

复杂度:O((V + E) log V) 使用优先队列

面试题

Q1:岛屿数量(中等)

题目:统计二维网格中的岛屿数量。

方法:DFS/BFS 淹没连通区域

复杂度:O(m × n) 时间, O(m × n) 空间

public int numIslands(char[][] grid) {
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') {
count++;
dfs(grid, i, j, m, n);
}
}
}

return count;
}

private void dfs(char[][] grid, int i, int j, int m, int n) {
if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != '1') return;

grid[i][j] = '0'; // 标记为已访问

dfs(grid, i + 1, j, m, n);
dfs(grid, i - 1, j, m, n);
dfs(grid, i, j + 1, m, n);
dfs(grid, i, j - 1, m, n);
}

Q2:克隆图(中等)

题目:深拷贝无向图。

方法:BFS/DFS + HashMap 映射原节点到新节点

复杂度:O(V + E) 时间, O(V) 空间

public Node cloneGraph(Node node) {
if (node == null) return null;

Map<Node, Node> visited = new HashMap<>();
return dfsClone(node, visited);
}

private Node dfsClone(Node node, Map<Node, Node> visited) {
if (visited.containsKey(node)) {
return visited.get(node);
}

Node copy = new Node(node.val);
visited.put(node, copy);

for (Node neighbor : node.neighbors) {
copy.neighbors.add(dfsClone(neighbor, visited));
}

return copy;
}

Q3:课程表(中等)

题目:完成所有课程的最少学期数(拓扑排序)。

方法:拓扑排序 + 最长路径

复杂度:O(V + E) 时间, O(V) 空间

public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> graph = new ArrayList<>();
int[] inDegree = new int[numCourses];

for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}

for (int[] pre : prerequisites) {
graph.get(pre[1]).add(pre[0]);
inDegree[pre[0]]++;
}

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 course = queue.poll();
count++;

for (int next : graph.get(course)) {
if (--inDegree[next] == 0) {
queue.offer(next);
}
}
}

return count == numCourses;
}

Q4:腐烂的橘子(中等)

题目:找到所有橘子腐烂的最少分钟数。

方法:多源 BFS

复杂度:O(m × n) 时间, O(m × n) 空间

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, 0}); // [行, 列, 时间]
} else if (grid[i][j] == 1) {
fresh++;
}
}
}

int maxTime = 0;
int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};

while (!queue.isEmpty()) {
int[] current = queue.poll();
int row = current[0], col = current[1], time = current[2];

for (int[] dir : dirs) {
int r = row + dir[0], c = col + dir[1];

if (r >= 0 && r < m && c >= 0 && c < n && grid[r][c] == 1) {
grid[r][c] = 2;
fresh--;
maxTime = Math.max(maxTime, time + 1);
queue.offer(new int[]{r, c, time + 1});
}
}
}

return fresh == 0 ? maxTime : -1;
}

Q5:网络延迟时间(中等)

题目:找到信号到达所有节点的最短时间(Dijkstra)。

方法:Dijkstra 算法

复杂度:O((V + E) log V) 时间, O(V) 空间

public int networkDelayTime(int[][] times, int n, int k) {
List<List<int[]>> graph = new ArrayList<>();
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<>());
}

for (int[] edge : times) {
graph.get(edge[0]).add(new int[]{edge[1], edge[2]});
}

int[] dist = new int[n + 1];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[k] = 0;

PriorityQueue<int[]> minHeap =
new PriorityQueue<>((a, b) -> a[1] - b[1]);
minHeap.offer(new int[]{k, 0});

while (!minHeap.isEmpty()) {
int[] current = minHeap.poll();
int node = current[0], time = current[1];

if (time > dist[node]) continue;

for (int[] edge : graph.get(node)) {
int neighbor = edge[0], weight = edge[1];
int newTime = dist[node] + weight;

if (newTime < dist[neighbor]) {
dist[neighbor] = newTime;
minHeap.offer(new int[]{neighbor, newTime});
}
}
}

int maxTime = 0;
for (int i = 1; i <= n; i++) {
maxTime = Math.max(maxTime, dist[i]);
if (dist[i] == Integer.MAX_VALUE) return -1;
}

return maxTime;
}

Q6:K 站中转最便宜的价格(中等)

题目:找到最多 K 次中转的最便宜路线。

方法:修改的 Dijkstra,带中转次数约束

复杂度:O(E × k) 时间, O(V) 空间

public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
List<List<int[]>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) {
graph.add(new ArrayList<>());
}

for (int[] flight : flights) {
graph.get(flight[0]).add(new int[]{flight[1], flight[2]});
}

PriorityQueue<int[]> minHeap =
new PriorityQueue<>((a, b) -> a[1] - b[1]); // [节点, 代价, 中转次数]
minHeap.offer(new int[]{src, 0, -1});

while (!minHeap.isEmpty()) {
int[] current = minHeap.poll();
int node = current[0], cost = current[1], stops = current[1];

if (node == dst) return cost;

if (stops < k) {
for (int[] flight : graph.get(node)) {
int neighbor = flight[0], price = flight[1];
minHeap.offer(new int[]{neighbor, cost + price, stops + 1});
}
}
}

return -1;
}

Q7:单词接龙(困难)

题目:找到从 beginWord 到 endWord 的最短转换序列长度。

方法:BFS,每次变换一个字母

复杂度:O(M² × N) 时间, O(M × N) 空间(M = 单词长度, N = 字典大小)

public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> wordSet = new HashSet<>(wordList);
if (!wordSet.contains(endWord)) return 0;

Queue<String> queue = new LinkedList<>();
queue.offer(beginWord);
int level = 1;

while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
String word = queue.poll();
char[] chars = word.toCharArray();

for (int j = 0; j < chars.length; j++) {
char original = chars[j];
for (char c = 'a'; c <= 'z'; c++) {
chars[j] = c;
String newWord = new String(chars);

if (newWord.equals(endWord)) return level + 1;

if (wordSet.contains(newWord)) {
wordSet.remove(newWord);
queue.offer(newWord);
}
}
chars[j] = original;
}
}
level++;
}

return 0;
}

延伸阅读

  • :图的特殊情况
  • DFS:用于拓扑排序、环检测
  • BFS:无权图最短路径
  • Dijkstra:使用优先队列的带权最短路径
  • LeetCode图题目