图论算法精解(Java 实现):从基础到高频面试题

一、图的基础表示方法

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]; // 标记节点是否已加入MST
    int[] parent = new int[n];    // 存储每个节点的父节点
    
    Arrays.fill(key, Integer.MAX_VALUE);
    key[0] = 0;  // 从节点0开始
    parent[0] = -1; // 根节点的父节点为-1
    int 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值和parent
        for (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 网络

结语

掌握图论算法是应对大厂面试的关键,建议按照以下步骤系统学习:

  1. 理解基础:图的表示方法、遍历方式
  2. 掌握经典算法:Dijkstra、Prim、拓扑排序
  3. 刷题巩固:完成 LeetCode 图论专题 50 题
  4. 深入研究:学习 Tarjan、匈牙利算法等高级算法

推荐练习题目

  • 课程表
  • 克隆图
  • 判断二分图
  • 矩阵中的最长递增路径
  • 网络延迟时间

本文代码均通过 LeetCode 测试用例,建议配合在线判题系统实践。如有疑问欢迎在评论区交流讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值