图是数据结构中最后一类重要的非线性结构。前面的链表是一对一,树是一对多,图是多对多。图的遍历(DFS/BFS)和最小生成树是 408 考研的重点。

一、图的基本概念

1. 定义

图由**顶点(Vertex)和边(Edge)**组成。

无向图:
   A —— B
   |    |
   C —— D

有向图(带箭头):
   A → B
   ↓    ↓
   C ← D

术语:

  • 顶点:图中的节点
  • 边:顶点之间的连线
  • 无向图:边没有方向,(A, B) = (B, A)
  • 有向图:边有方向,<A, B> ≠ <B, A>
  • 完全图:任意两个顶点之间都有边
  • 权:边上的数值(如距离、花费)
  • 连通图:任意两个顶点之间都有路径可达

二、图的存储结构

1. 邻接矩阵

// 用一个二维数组存储顶点之间的邻接关系
// graph[i][j] = 1 表示顶点 i 到 j 有边
// graph[i][j] = 0 表示没有边

public class GraphMatrix {
    private int[][] matrix;  // 邻接矩阵
    private int n;           // 顶点数
    private boolean directed; // 是否有向

    public GraphMatrix(int n, boolean directed) {
        this.n = n;
        this.directed = directed;
        matrix = new int[n][n];
    }

    // 添加边
    public void addEdge(int i, int j, int weight) {
        matrix[i][j] = weight;        // i → j
        if (!directed) {
            matrix[j][i] = weight;    // 无向图对称
        }
    }

    // 判断是否有边
    public boolean hasEdge(int i, int j) {
        return matrix[i][j] != 0;
    }
}
无向图的邻接矩阵(对称):
     A  B  C  D
  A [0, 1, 1, 0]
  B [1, 0, 0, 1]
  C [1, 0, 0, 1]
  D [0, 1, 1, 0]

优点: 判断两点是否有边 O(1),实现简单
缺点: 占用空间 O(n²),稀疏图浪费空间

2. 邻接表

// 每个顶点维护一个链表,存储与其相连的顶点

public class GraphList {
    private List<Integer>[] adj;  // 邻接表数组
    private int n;

    public GraphList(int n) {
        this.n = n;
        adj = new List[n];
        for (int i = 0; i < n; i++) {
            adj[i] = new ArrayList<>();
        }
    }

    public void addEdge(int i, int j) {
        adj[i].add(j);
        adj[j].add(i);  // 无向图
    }

    public List<Integer> getNeighbors(int v) {
        return adj[v];
    }
}
无向图的邻接表:
  A → [B, C]
  B → [A, D]
  C → [A, D]
  D → [B, C]

优点: 节省空间 O(n + e),适合稀疏图
缺点: 判断两点是否有边需要遍历链表 O(degree)

3. 邻接矩阵 vs 邻接表

对比邻接矩阵邻接表
空间O(n²)O(n + e)
判边O(1) 🏆O(度)
遍历邻居O(n)O(度) 🏆
适合稠密图稀疏图(常用) 🏆

三、图的遍历

1. 深度优先搜索(DFS)

沿着一条路走到黑,走不通了再回头。

public class GraphDFS {

    private List<Integer>[] adj;
    private boolean[] visited;

    public void dfs(int start) {
        visited = new boolean[adj.length];
        dfsRecursive(start);
    }

    private void dfsRecursive(int v) {
        visited[v] = true;
        System.out.print(v + " ");  // 访问当前顶点

        for (int neighbor : adj[v]) {
            if (!visited[neighbor]) {
                dfsRecursive(neighbor);  // 递归访问未访问的邻居
            }
        }
    }
}
从 A 开始 DFS(假设邻接顺序从小到大):
  A → B → D → C
  路径:A → B → D → C

  访问顺序:A, B, D, C

时间复杂度: O(n + e)
空间复杂度: O(n)(递归栈)

2. 广度优先搜索(BFS)

一层一层往外扩,像水的波纹。

public class GraphBFS {

    private List<Integer>[] adj;
    private boolean[] visited;

    public void bfs(int start) {
        visited = new boolean[adj.length];
        Queue<Integer> queue = new LinkedList<>();

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

        while (!queue.isEmpty()) {
            int v = queue.poll();
            System.out.print(v + " ");  // 访问当前顶点

            for (int neighbor : adj[v]) {
                if (!visited[neighbor]) {
                    visited[neighbor] = true;
                    queue.offer(neighbor);  // 未访问的邻居入队
                }
            }
        }
    }
}
从 A 开始 BFS:
  第一层:A
  第二层:B, C
  第三层:D

  访问顺序:A, B, C, D

时间复杂度: O(n + e)
空间复杂度: O(n)(队列)

3. DFS vs BFS

对比DFSBFS
数据结构栈(递归)队列
遍历顺序沿着路径深入逐层扩展
适用场景连通性判断、拓扑排序最短路径、层序遍历
实现难度递归,代码简洁循环,稍复杂

四、最小生成树

用最小的总权值把所有顶点连通。

1. Prim 算法

从一个顶点开始,每次找权值最小的边加入。

public class Prim {

    /**
     * Prim 算法求最小生成树
     * @param graph 邻接矩阵(权值,0表示无边)
     * @return 最小总权值
     */
    public static int prim(int[][] graph) {
        int n = graph.length;
        int[] lowCost = new int[n];   // 到各顶点的最小权值
        boolean[] visited = new boolean[n];
        int total = 0;

        // 从顶点0开始
        visited[0] = true;
        for (int i = 0; i < n; i++) {
            lowCost[i] = graph[0][i] == 0 ? Integer.MAX_VALUE : graph[0][i];
        }

        for (int i = 1; i < n; i++) {
            // 找权值最小的未访问顶点
            int min = Integer.MAX_VALUE;
            int minIdx = -1;
            for (int j = 0; j < n; j++) {
                if (!visited[j] && lowCost[j] < min) {
                    min = lowCost[j];
                    minIdx = j;
                }
            }

            if (minIdx == -1) break;  // 不连通

            visited[minIdx] = true;
            total += min;

            // 更新 lowCost
            for (int j = 0; j < n; j++) {
                if (!visited[j] && graph[minIdx][j] != 0
                    && graph[minIdx][j] < lowCost[j]) {
                    lowCost[j] = graph[minIdx][j];
                }
            }
        }
        return total;
    }
}

2. Kruskal 算法

每次选权值最小的边,只要不形成环就加入。

public class Kruskal {

    // 边的定义
    static class Edge implements Comparable<Edge> {
        int u, v, weight;
        public int compareTo(Edge o) { return this.weight - o.weight; }
    }

    // 并查集
    static class UnionFind {
        int[] parent;
        UnionFind(int n) {
            parent = new int[n];
            for (int i = 0; i < n; i++) parent[i] = i;
        }
        int find(int x) {
            return parent[x] == x ? x : (parent[x] = find(parent[x]));
        }
        boolean union(int x, int y) {
            int rx = find(x), ry = find(y);
            if (rx == ry) return false;
            parent[rx] = ry;
            return true;
        }
    }

    public static int kruskal(Edge[] edges, int n) {
        Arrays.sort(edges);                        // 按权值排序
        UnionFind uf = new UnionFind(n);
        int total = 0, count = 0;

        for (Edge e : edges) {
            if (uf.union(e.u, e.v)) {              // 不会形成环
                total += e.weight;
                count++;
                if (count == n - 1) break;          // n个顶点需要n-1条边
            }
        }
        return total;
    }
}

3. Prim vs Kruskal

对比PrimKruskal
策略选顶点选边
数据结构邻接矩阵边集 + 并查集
时间复杂度O(n²)O(e log e)
适合稠密图 🏆稀疏图 🏆

五、408 考研经典考题

题1:邻接矩阵转邻接表

List<Integer>[] matrixToList(int[][] matrix) {
    int n = matrix.length;
    List<Integer>[] adj = new List[n];
    for (int i = 0; i < n; i++) {
        adj[i] = new ArrayList<>();
        for (int j = 0; j < n; j++) {
            if (matrix[i][j] == 1) {
                adj[i].add(j);
            }
        }
    }
    return adj;
}

题2:判断图是否连通

boolean isConnected(List<Integer>[] adj) {
    int n = adj.length;
    boolean[] visited = new boolean[n];
    dfs(adj, visited, 0);
    for (boolean v : visited) {
        if (!v) return false;
    }
    return true;
}

题3:拓扑排序

有向无环图(DAG)的顶点线性排序,AOV 网中活动执行的先后顺序。

// 核心思想:不断删除入度为 0 的顶点
List<Integer> topologicalSort(List<Integer>[] adj) {
    int n = adj.length;
    int[] inDegree = new int[n];
    for (int i = 0; i < n; i++) {
        for (int v : adj[i]) inDegree[v]++;
    }

    Queue<Integer> queue = new LinkedList<>();
    for (int i = 0; i < n; i++) {
        if (inDegree[i] == 0) queue.offer(i);
    }

    List<Integer> result = new ArrayList<>();
    while (!queue.isEmpty()) {
        int v = queue.poll();
        result.add(v);
        for (int neighbor : adj[v]) {
            if (--inDegree[neighbor] == 0) {
                queue.offer(neighbor);
            }
        }
    }
    return result;
}

六、图的常见考点

1. 有 n 个顶点的无向完全图有 n(n-1)/2 条边
2. 有 n 个顶点的有向完全图有 n(n-1) 条边
3. 连通图至少需要 n-1 条边(树)
4. 图的邻接矩阵是对称矩阵(无向图)

DFS/BFS 时间复杂度的写法:
  邻接矩阵:O(n²)
  邻接表:O(n + e)

💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。

更多推荐