一、图的基础表示方法
1.1 邻接矩阵(Adjacency Matrix)
邻接矩阵是表示图的一种直观方式,它使用一个二维数组来存储节点之间的连接关系。对于一个有 n 个节点的图,邻接矩阵是一个 n×n 的矩阵,其中 matrix [i][j] 表示节点 i 到节点 j 的边的权重。
class GraphMatrix {private int[][] matrix;private int vertexCount;public GraphMatrix(int n) {vertexCount = n;matrix = new int[n][n];}public void addEdge(int from, int to, int weight) {matrix[from][to] = weight;matrix[to][from] = weight; // 无向图需双向设置}// 获取两个节点之间的边权重public int getEdgeWeight(int from, int to) {return matrix[from][to];}// 获取节点的所有邻接节点public List<Integer> getNeighbors(int node) {List<Integer> neighbors = new ArrayList<>();for (int i = 0; i < vertexCount; i++) {if (matrix[node][i] != 0) {neighbors.add(i);}}return neighbors;}
}
1.2 邻接表(Adjacency List)
邻接表是表示稀疏图的更高效方式,它使用一个列表数组,其中每个元素对应一个节点的所有邻接节点及其边的权重。
class GraphList {private List<List<int[]>> adjList; // [邻接节点, 权重]public GraphList(int n) {adjList = new ArrayList<>();for (int i = 0; i < n; i++) {adjList.add(new ArrayList<>());}}public void addEdge(int from, int to, int weight) {adjList.get(from).add(new int[]{to, weight});adjList.get(to).add(new int[]{from, weight}); // 无向图需添加反向边}// 获取节点的所有邻接边public List<int[]> getEdges(int node) {return adjList.get(node);}// 获取图的邻接表表示public List<List<int[]>> getAdjList() {return adjList;}
}
1.3 两种表示法的对比
特性 | 邻接矩阵 | 邻接表 |
---|---|---|
空间复杂度 | O(V²) | O(V + E) |
查询边是否存在 | O(1) | O(k) |
遍历邻接节点 | O(V) | O(1)~O(k) |
适用场景 | 稠密图 | 稀疏图 |
- 邻接矩阵的优势在于快速查询任意两个节点之间的边,但空间效率较低,适合节点数量较少的稠密图。
- 邻接表的优势在于节省空间,适合节点数量较多的稀疏图,但查询特定边的效率较低。
二、基础图遍历算法
2.1 深度优先搜索(DFS)
深度优先搜索是一种递归遍历图的方法,它沿着一条路径尽可能深地访问节点,直到无法继续,然后回溯。
// 递归实现DFS
void dfs(List<List<Integer>> graph, int start) {boolean[] visited = new boolean[graph.size()];dfsHelper(graph, start, visited);
}private void dfsHelper(List<List<Integer>> graph, int node, boolean[] visited) {visited[node] = true;System.out.print(node + " ");for (int neighbor : graph.get(node)) {if (!visited[neighbor]) {dfsHelper(graph, neighbor, visited);}}
}// 迭代实现DFS(使用栈)
void dfsIterative(List<List<Integer>> graph, int start) {boolean[] visited = new boolean[graph.size()];Stack<Integer> stack = new Stack<>();stack.push(start);while (!stack.isEmpty()) {int node = stack.pop();if (!visited[node]) {visited[node] = true;System.out.print(node + " ");// 注意:栈是后进先出,所以先将邻接节点逆序压入栈List<Integer> neighbors = graph.get(node);for (int i = neighbors.size() - 1; i >= 0; i--) {if (!visited[neighbors.get(i)]) {stack.push(neighbors.get(i));}}}}
}
2.2 广度优先搜索(BFS)
广度优先搜索是一种逐层遍历图的方法,它使用队列来保存待访问的节点,先访问距离起始节点最近的所有节点,然后逐层向外扩展。
void bfs(List<List<Integer>> graph, int start) {boolean[] visited = new boolean[graph.size()];Queue<Integer> queue = new LinkedList<>();queue.offer(start);visited[start] = true;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的应用:计算最短路径(无权图)
int[] shortestPath(List<List<Integer>> graph, int start) {int n = graph.size();int[] dist = new int[n];Arrays.fill(dist, -1); // -1表示不可达Queue<Integer> queue = new LinkedList<>();queue.offer(start);dist[start] = 0;while (!queue.isEmpty()) {int node = queue.poll();for (int neighbor : graph.get(node)) {if (dist[neighbor] == -1) {dist[neighbor] = dist[node] + 1;queue.offer(neighbor);}}}return dist;
}
三、最短路径算法
3.1 Dijkstra 算法(单源最短路径)
Dijkstra 算法用于计算带权有向图或无向图中,从单个源节点到所有其他节点的最短路径,要求所有边的权重非负。
int[] dijkstra(List<List<int[]>> graph, int start) {int n = graph.size();int[] dist = new int[n];Arrays.fill(dist, Integer.MAX_VALUE);dist[start] = 0;PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);pq.offer(new int[]{start, 0});while (!pq.isEmpty()) {int[] curr = pq.poll();int u = curr[0], d = curr[1];// 如果当前距离大于已记录的最短距离,跳过if (d > dist[u]) continue;// 遍历所有邻接边for (int[] edge : graph.get(u)) {int v = edge[0], w = edge[1];if (dist[v] > dist[u] + w) {dist[v] = dist[u] + w;pq.offer(new int[]{v, dist[v]});}}}return dist;
}// 打印最短路径
List<Integer> getShortestPath(int[] prev, int target) {List<Integer> path = new ArrayList<>();for (int at = target; at != -1; at = prev[at]) {path.add(at);}Collections.reverse(path);return path;
}
3.2 Floyd-Warshall 算法(多源最短路径)
Floyd-Warshall 算法用于计算带权图中所有节点对之间的最短路径,允许边的权重为负,但不能包含负权环。
void floydWarshall(int[][] graph) {int n = graph.length;int[][] dist = new int[n][n];// 初始化距离矩阵,将不可达的距离设为无穷大for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {if (i == j) {dist[i][j] = 0;} else if (graph[i][j] != 0) {dist[i][j] = graph[i][j];} else {dist[i][j] = Integer.MAX_VALUE;}}}// 三重循环更新距离for (int k = 0; k < n; k++) {for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {if (dist[i][k] != Integer.MAX_VALUE && dist[k][j] != Integer.MAX_VALUE) {dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);}}}}// 检查负权环for (int i = 0; i < n; i++) {if (dist[i][i] < 0) {System.out.println("图包含负权环");return;}}// 打印最短路径矩阵for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {System.out.print((dist[i][j] == Integer.MAX_VALUE ? "INF" : dist[i][j]) + "\t");}System.out.println();}
}
四、最小生成树算法
4.1 Prim 算法(邻接矩阵版)
Prim 算法是一种贪心算法,用于在带权无向图中找到最小生成树(MST)。
int primMST(int[][] graph) {int n = graph.length;int[] key = new int[n]; // 存储最小边权值boolean[] mstSet = new boolean[n]; // 标记节点是否已加入MSTint[] parent = new int[n]; // 存储每个节点的父节点Arrays.fill(key, Integer.MAX_VALUE);key[0] = 0; // 从节点0开始parent[0] = -1; // 根节点的父节点为-1int totalWeight = 0;for (int count = 0; count < n; count++) {// 选择key值最小且未被加入MST的节点int u = -1;for (int i = 0; i < n; i++) {if (!mstSet[i] && (u == -1 || key[i] < key[u])) {u = i;}}mstSet[u] = true;totalWeight += key[u];// 更新邻接顶点的key值和parentfor (int v = 0; v < n; v++) {if (graph[u][v] != 0 && !mstSet[v] && graph[u][v] < key[v]) {key[v] = graph[u][v];parent[v] = u;}}}// 打印MST的边System.out.println("Edge \tWeight");for (int i = 1; i < n; i++) {System.out.println(parent[i] + " - " + i + "\t" + graph[i][parent[i]]);}return totalWeight;
}
4.2 Kruskal 算法(并查集优化)
Kruskal 算法也是一种贪心算法,用于找到带权无向图的最小生成树。
class Kruskal {class UnionFind {private int[] parent;private int[] rank;public UnionFind(int size) {parent = new int[size];rank = new int[size];for (int i = 0; i < size; i++) {parent[i] = i;rank[i] = 1;}}// 路径压缩public int find(int x) {if (parent[x] != x) {parent[x] = find(parent[x]);}return parent[x];}// 按秩合并public void union(int x, int y) {int rootX = find(x);int rootY = find(y);if (rootX == rootY) return;if (rank[rootX] > rank[rootY]) {parent[rootY] = rootX;} else if (rank[rootX] < rank[rootY]) {parent[rootX] = rootY;} else {parent[rootY] = rootX;rank[rootX]++;}}}public int kruskalMST(int[][] edges, int n) {// 按边权排序Arrays.sort(edges, (a, b) -> a[2] - b[2]);UnionFind uf = new UnionFind(n);int mstWeight = 0;int edgeCount = 0;for (int[] edge : edges) {int u = edge[0];int v = edge[1];int w = edge[2];// 检查是否会形成环if (uf.find(u) != uf.find(v)) {uf.union(u, v);mstWeight += w;edgeCount++;// MST的边数为n-1时结束if (edgeCount == n - 1) break;}}return mstWeight;}
}
五、高频面试题解析
5.1 课程表 II(LeetCode 210)拓扑排序
题目描述:给定课程总数和先决条件,返回一个有效的课程学习顺序。如果不可能,则返回空数组。
public int[] findOrder(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[] p : prerequisites) {graph.get(p[1]).add(p[0]);inDegree[p[0]]++;}// 将入度为0的节点加入队列Queue<Integer> q = new LinkedList<>();for (int i = 0; i < numCourses; i++) {if (inDegree[i] == 0) q.offer(i);}// 拓扑排序int[] result = new int[numCourses];int idx = 0;while (!q.isEmpty()) {int u = q.poll();result[idx++] = u;for (int v : graph.get(u)) {if (--inDegree[v] == 0) {q.offer(v);}}}// 检查是否所有课程都能被安排return idx == numCourses ? result : new int[0];
}
5.2 网络延迟时间(LeetCode 743)Dijkstra 应用
题目描述:给定一个网络,计算从给定节点出发,信号到达所有其他节点的最短时间。如果无法到达所有节点,返回 - 1。
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[] time : times) {graph.get(time[0]).add(new int[]{time[1], time[2]});}// 初始化距离数组int[] dist = new int[n + 1];Arrays.fill(dist, Integer.MAX_VALUE);dist[k] = 0;// 使用优先队列实现Dijkstra算法PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);pq.offer(new int[]{k, 0});while (!pq.isEmpty()) {int[] curr = pq.poll();int u = curr[0], d = curr[1];// 如果当前距离大于已记录的最短距离,跳过if (d > dist[u]) continue;// 遍历所有邻接边for (int[] edge : graph.get(u)) {int v = edge[0], w = edge[1];if (dist[v] > dist[u] + w) {dist[v] = dist[u] + w;pq.offer(new int[]{v, dist[v]});}}}// 找到最大延迟时间int max = 0;for (int i = 1; i <= n; i++) {if (dist[i] == Integer.MAX_VALUE) return -1;max = Math.max(max, dist[i]);}return max;
}
六、算法优化技巧
6.1 Dijkstra 算法优化
- 优先队列选择:使用斐波那契堆可将时间复杂度降至 O (E + VlogV)
- 双向搜索:同时从起点和终点进行搜索,减少搜索空间
- A * 算法:启发式搜索,利用距离估计函数加速搜索
6.2 并查集路径压缩
int find(int x) {if (parent[x] != x) {parent[x] = find(parent[x]); // 路径压缩}return parent[x];
}
6.3 记忆化搜索
// 用于存在性路径问题
int[][] memo;int dfsMemo(int[][] graph, int u, int target) {if (u == target) return 1;if (memo[u][target] != 0) return memo[u][target];for (int v : graph[u]) {if (dfsMemo(graph, v, target) == 1) {memo[u][target] = 1;return 1;}}memo[u][target] = -1;return -1;
}
七、常见问题及解决方案
问题类型 | 解决方法 | 相关算法 |
---|---|---|
负权边检测 | Bellman-Ford 算法 | 单源最短路径 |
检测环路 | 拓扑排序 / DFS 访问标记 | 有向无环图判断 |
连通分量 | 并查集 / BFS/DFS | 图连通性判断 |
关键路径 | 拓扑排序 + 动态规划 | AOE 网络 |
结语
掌握图论算法是应对大厂面试的关键,建议按照以下步骤系统学习:
- 理解基础:图的表示方法、遍历方式
- 掌握经典算法:Dijkstra、Prim、拓扑排序
- 刷题巩固:完成 LeetCode 图论专题 50 题
- 深入研究:学习 Tarjan、匈牙利算法等高级算法
推荐练习题目:
- 课程表
- 克隆图
- 判断二分图
- 矩阵中的最长递增路径
- 网络延迟时间
本文代码均通过 LeetCode 测试用例,建议配合在线判题系统实践。如有疑问欢迎在评论区交流讨论!