图论 - 临接矩阵与临接表介绍与分析对比

前言

图的力扣什么的也刷了也不少了,但是刷的时候吧老是觉得想法比较杂乱,不成章法,对此我痛定思痛,觉得是时候花时间来系统的捋一捋关于图的一些内容,文章估计比较长,如果你是初学的朋友估计会有点吃力,不过加油肯定能学会的.

图论数据结构

对于图的逻辑结构是很简单的,如下

可以看到是由节点(Vertex)+边(Edge)构成.

它的代码实现也很简单,你可以参考着多叉树来看:

java 复制代码
// 图节点的逻辑结构
class Vertex {
    int id;
    Vertex[] neighbors;
}

// 基本的 N 叉树节点
class TreeNode {
    int val;
    TreeNode[] children;
}

树的DFS/BFS遍历都适用于图.

但是图比树多了一个概念-- 度(degree).

度是每个节点相连边的条数.

每个节点的度被细分为入度(indegree)与出度(outdegree).

上图的节点 3 的入度为 3(有三条边指向它),出度为 1(它有 1 条边指向别的节点)。

上面图结构是逻辑上这样实现,但是我们很少会用Vertex类,而是用邻接表和邻接矩阵来实现图结构.

邻接表与临接矩阵


上面的图很直观的表示出来了它们的区别.

我先给出它们的实现

java 复制代码
// 邻接表
// graph[x] 存储 x 的所有邻居节点
List<Integer>[] graph;


// 邻接矩阵
// matrix[x][y] 记录 x 是否有一条指向 y 的边
boolean[][] matrix;
邻接表

在学习邻接表时,我们会看到 List[] 这样的数据结构,刚开始可能会觉得很奇怪,不明白为什么要这样写。尤其是看到邻接表的示意图时,可能会误以为"链表相连,链表里面是数组",但实际上并不是这样。

如果我们仔细分析 List[],可以拆解成两个部分:

  • 它首先是一个数组([] 说明是数组)。
  • 数组的每个元素都是 List(说明数组存的是列表)。

为什么 List[] 适合表示图的邻接表?

我们先问自己一个问题:在一个图里,哪些数据是固定的?哪些是不固定的?

  • 节点的个数是固定的(假设图有 N 个节点,它的编号通常是 0 ~ N-1)。
  • 每个节点的邻接边数是不确定的(比如某个点的出度可能是 2,也可能是 5)。

那么,我们就需要一个既能存固定数量的节点,又能灵活存储每个节点的邻接点的数据结构:

  1. 数组适合存固定数量的节点
    既然节点数是固定的,我们就可以用数组来存储所有节点。
    例如 List[] graph = new ArrayList[N];,表示有 N 个节点,每个节点都有一个 List 来存邻接点。
  2. 列表(List)适合存每个节点的不定邻居
    由于每个节点的邻接点数量不同,我们不能用固定大小的数组存储邻接点,而链表(List)的动态特性完美地解决了这个问题。
java 复制代码
//举个例子:
graph[0] = new ArrayList<>(Arrays.asList(1, 2)); // 0 号节点连向 1 和 2
graph[1] = new ArrayList<>(Collections.singletonList(3)); // 1 号节点连向 3

List[] 既有数组的优势,又有链表的灵活性,非常适合表示图的邻接表.

邻接矩阵

我们可以看到是二维数据存储图,这种给人的感觉是力大砖飞,存储空间巨大,查询O(1),而且有向,无向图,有权,无权图都可以表示.

且二维数据也比较容易上手,一般学到图的也都有些了解,比较亲民的类型.

它的特点:

邻接矩阵的特点
特点 分析
存储空间 O(N^2),如果 N 很大且边较少,会浪费大量内存
查询边 O(1),直接访问 graph[i][j]
添加边 O(1),只需 graph[i][j] = 1 / weight
删除边 O(1),只需 graph[i][j] = 0 / INF
遍历邻接点 O(N),需要扫描整行 graph[i][*]
适用于 稠密图 (边的数量接近 N^2

适用算法总结
算法 邻接矩阵是否适合? 理由
BFS / DFS 不适合 遍历邻接点太慢 O(N)
Dijkstra(最短路径) 适合 O(N^2) 适用于 小规模
Floyd-Warshall(全源最短路) 适合 O(N^3) 适用于 稠密图
Kruskal(最小生成树) 不适合 需要转换成边列表
Prim(最小生成树) 适合 O(N^2) 适用于 小规模
拓扑排序 不适合 需要遍历所有出边

什么时候用邻接矩阵?
  • M ≈ N^2(稠密图)时,存储邻接矩阵更合适。
  • 当查询两点是否相连的次数特别多时,邻接矩阵更高效 O(1)
  • 当使用 Floyd-Warshall、Prim(O(N^2))等基于矩阵运算的算法时,邻接矩阵更合适。
什么时候不用邻接矩阵?
  • M ≪ N^2(稀疏图)时,邻接矩阵浪费空间。
  • 当需要频繁遍历某个点的邻接点时,邻接表更高效。
  • 当使用 BFS / DFS / Dijkstra / Kruskal 等以遍历为主的算法时,邻接表更合适。

临接表
邻接表的特点
特点 分析
存储空间 O(N + M),只存储实际存在的边,适合 稀疏图
查询边 O(deg(i)),需遍历 List[i] 查找
添加边 O(1),直接 list[i].add(j)
删除边 O(deg(i)),需遍历 List[i] 删除
遍历邻接点 O(deg(i)),比邻接矩阵快
适用于 稀疏图 (边的数量远小于 N^2

适用算法总结
算法 邻接表是否适合? 理由
BFS / DFS 适合 访问邻接点更快 O(deg(i))
Dijkstra(最短路径) 适合 使用 优先队列 + 邻接表 实现 O(M logN)
Floyd-Warshall(全源最短路) 不适合 需要 O(N^2) 矩阵操作
Kruskal(最小生成树) 适合 本质是边列表,可直接排序
Prim(最小生成树) 适合 使用 优先队列 + 邻接表 实现 O(M logN)
拓扑排序 适合 存出度更方便 ,遍历所有出边 O(M)

什么时候用邻接表?
  • M ≪ N^2(稀疏图)时,邻接表更节省空间。
  • 当频繁遍历某个点的邻接点时,邻接表更高效。
  • 当使用 BFS / DFS / Dijkstra(堆优化)等遍历为主的算法时,邻接表更合适。
什么时候不用邻接表?
  • M ≈ N^2(稠密图)时,邻接表存储边的指针会浪费额外空间。
  • 当查询两点是否相连的次数特别多时,邻接表查找 O(deg(i)) 比邻接矩阵 O(1) 慢。
  • 当使用 Floyd-Warshall 这种基于矩阵运算的算法时,邻接表不适合。

临接表VS临接矩阵

那我们该如何选择用哪种数据结构去解决问题呢?

取决于边的数量(稀疏 vs. 稠密)
条件 适合的数据结构 原因
边数接近 N^2(稠密图) 邻接矩阵 直接用二维数组存储,访问 O(1),遍历 O(N) 也可接受
边数远小于 N^2(稀疏图) 邻接表 只存有效边,节省空间,遍历 O(deg(i)) 更高效

取决于执行的操作类型
操作 适用的数据结构 原因
快速查询两点是否相连 邻接矩阵 O(1) 直接查 graph[i][j]
遍历所有邻接点 邻接表 O(deg(i)),比邻接矩阵 O(N)
快速添加 / 删除边 邻接表 O(1) 添加,O(deg(i)) 删除(邻接矩阵删除 O(1) 但空间大)
Dijkstra(最短路径) 邻接表 堆优化 Dijkstra O(M logN) 适合 稀疏图
Floyd-Warshall(全源最短路) 邻接矩阵 O(N^3),使用矩阵更高效
Kruskal(最小生成树) 邻接表 Kruskal 本质是 边列表,邻接表方便转换
Prim(最小生成树) 邻接表(堆优化) or 邻接矩阵 邻接表 + 堆 O(M logN),邻接矩阵 O(N^2)
BFS / DFS(遍历) 邻接表 O(M) 遍历比邻接矩阵 O(N^2)
拓扑排序 邻接表 存出度方便,遍历 O(M)

选择策略总结
✅ 优先选邻接矩阵的情况
  • 图是稠密图M ≈ N^2)。
  • 需要快速查询两点是否相连 ,时间复杂度 O(1)
  • Floyd-WarshallO(N^3))需要矩阵运算。
  • 小规模问题(N < 1000) ,邻接矩阵 O(N^2) 可以接受。
✅ 优先选邻接表的情况
  • 图是稀疏图M ≪ N^2)。
  • 主要操作是遍历邻接点(BFS / DFS / Dijkstra / Prim / Kruskal)。
  • Dijkstra(堆优化)或 Prim(堆优化) ,邻接表 O(M logN) 效率更高。
  • 适用于大规模图N 很大,M 远小于 N^2)。

图结构的通用代码实现

我们可以抽象出一个 Graph 接口,来实现图的基本增删查改:

java 复制代码
interface Graph {
    // 添加一条边(带权重)
    void addEdge(int from, int to, int weight);

    // 删除一条边
    void removeEdge(int from, int to);

    // 判断两个节点是否相邻
    boolean hasEdge(int from, int to);

    // 返回一条边的权重
    int weight(int from, int to);

    // 返回某个节点的所有邻居节点和对应权重
    List<Edge> neighbors(int v);

    // 返回节点总数
    int size();
}

有向加全图

邻接表实现
java 复制代码
// 加权有向图的通用实现(邻接表)
class WeightedDigraph {
    // 存储相邻节点及边的权重
    public static class Edge {
        int to;
        int weight;

        public Edge(int to, int weight) {
            this.to = to;
            this.weight = weight;
        }
    }

    // 邻接表,graph[v] 存储节点 v 的所有邻居节点及对应权重
    private List<Edge>[] graph;

    public WeightedDigraph(int n) {
        // 我们这里简单起见,建图时要传入节点总数,这其实可以优化
        // 比如把 graph 设置为 Map<Integer, List<Edge>>,就可以动态添加新节点了
        graph = new List[n];
        for (int i = 0; i < n; i++) {
            graph[i] = new ArrayList<>();
        }
    }

    // 增,添加一条带权重的有向边,复杂度 O(1)
    public void addEdge(int from, int to, int weight) {
        graph[from].add(new Edge(to, weight));
    }

    // 删,删除一条有向边,复杂度 O(V)
    public void removeEdge(int from, int to) {
        for (int i = 0; i < graph[from].size(); i++) {
            if (graph[from].get(i).to == to) {
                graph[from].remove(i);
                break;
            }
        }
    }

    // 查,判断两个节点是否相邻,复杂度 O(V)
    public boolean hasEdge(int from, int to) {
        for (Edge e : graph[from]) {
            if (e.to == to) {
                return true;
            }
        }
        return false;
    }

    // 查,返回一条边的权重,复杂度 O(V)
    public int weight(int from, int to) {
        for (Edge e : graph[from]) {
            if (e.to == to) {
                return e.weight;
            }
        }
        throw new IllegalArgumentException("No such edge");
    }

    // 上面的 hasEdge、removeEdge、weight 方法遍历 List 的行为是可以优化的
    // 比如用 Map<Integer, Map<Integer, Integer>> 存储邻接表
    // 这样就可以避免遍历 List,复杂度就能降到 O(1)

    // 查,返回某个节点的所有邻居节点,复杂度 O(1)
    public List<Edge> neighbors(int v) {
        return graph[v];
    }

    public static void main(String[] args) {
        WeightedDigraph graph = new WeightedDigraph(3);
        graph.addEdge(0, 1, 1);
        graph.addEdge(1, 2, 2);
        graph.addEdge(2, 0, 3);
        graph.addEdge(2, 1, 4);

        System.out.println(graph.hasEdge(0, 1)); // true
        System.out.println(graph.hasEdge(1, 0)); // false

        graph.neighbors(2).forEach(edge -> {
            System.out.println(2 + " -> " + edge.to + ", wight: " + edge.weight);
        });
        // 2 -> 0, wight: 3
        // 2 -> 1, wight: 4

        graph.removeEdge(0, 1);
        System.out.println(graph.hasEdge(0, 1)); // false
    }
}
临接矩阵
java 复制代码
import java.util.ArrayList;
import java.util.List;

// 加权有向图的通用实现(邻接矩阵)
public class WeightedDigraph {
    // 存储相邻节点及边的权重
    public static class Edge {
        int to;
        int weight;

        public Edge(int to, int weight) {
            this.to = to;
            this.weight = weight;
        }
    }


    // 邻接矩阵,matrix[from][to] 存储从节点 from 到节点 to 的边的权重
    // 0 表示没有连接
    private int[][] matrix;

    public WeightedDigraph(int n) {
        matrix = new int[n][n];
    }

    // 增,添加一条带权重的有向边,复杂度 O(1)
    public void addEdge(int from, int to, int weight) {
        matrix[from][to] = weight;
    }

    // 删,删除一条有向边,复杂度 O(1)
    public void removeEdge(int from, int to) {
        matrix[from][to] = 0;
    }

    // 查,判断两个节点是否相邻,复杂度 O(1)
    public boolean hasEdge(int from, int to) {
        return matrix[from][to] != 0;
    }

    // 查,返回一条边的权重,复杂度 O(1)
    public int weight(int from, int to) {
        return matrix[from][to];
    }

    // 查,返回某个节点的所有邻居节点,复杂度 O(V)
    public List<Edge> neighbors(int v) {
        List<Edge> res = new ArrayList<>();
        for (int i = 0; i < matrix[v].length; i++) {
            if (matrix[v][i] > 0) {
                res.add(new Edge(i, matrix[v][i]));
            }
        }
        return res;
    }

    public static void main(String[] args) {
        WeightedDigraph graph = new WeightedDigraph(3);
        graph.addEdge(0, 1, 1);
        graph.addEdge(1, 2, 2);
        graph.addEdge(2, 0, 3);
        graph.addEdge(2, 1, 4);

        System.out.println(graph.hasEdge(0, 1)); // true
        System.out.println(graph.hasEdge(1, 0)); // false

        graph.neighbors(2).forEach(edge -> {
            System.out.println(2 + " -> " + edge.to + ", wight: " + edge.weight);
        });
        // 2 -> 0, wight: 3
        // 2 -> 1, wight: 4

        graph.removeEdge(0, 1);
        System.out.println(graph.hasEdge(0, 1)); // false
    }
}

有向无权图

把addEdge的权重参数默认为1即可

无向加权图

无向加权图就等同于双向的有向加权图,所以直接复用上面用邻接表/领接矩阵实现的 WeightedDigraph 类就行了,只是在增加边的时候,要同时添加两条边:

java 复制代码
// 无向加权图的通用实现
class WeightedUndigraph {
    private WeightedDigraph graph;

    public WeightedUndigraph(int n) {
        graph = new WeightedDigraph(n);
    }

    // 增,添加一条带权重的无向边
    public void addEdge(int from, int to, int weight) {
        graph.addEdge(from, to, weight);
        graph.addEdge(to, from, weight);
    }

    // 删,删除一条无向边
    public void removeEdge(int from, int to) {
        graph.removeEdge(from, to);
        graph.removeEdge(to, from);
    }

    // 查,判断两个节点是否相邻
    public boolean hasEdge(int from, int to) {
        return graph.hasEdge(from, to);
    }

    // 查,返回一条边的权重
    public int weight(int from, int to) {
        return graph.weight(from, to);
    }

    // 查,返回某个节点的所有邻居节点
    public List<WeightedDigraph.Edge> neighbors(int v) {
        return graph.neighbors(v);
    }

    public static void main(String[] args) {
        WeightedUndigraph graph = new WeightedUndigraph(3);
        graph.addEdge(0, 1, 1);
        graph.addEdge(1, 2, 2);
        graph.addEdge(2, 0, 3);
        graph.addEdge(2, 1, 4);

        System.out.println(graph.hasEdge(0, 1)); // true
        System.out.println(graph.hasEdge(1, 0)); // true

        graph.neighbors(2).forEach(edge -> {
            System.out.println(2 + " <-> " + edge.to + ", wight: " + edge.weight);
        });
        // 2 <-> 0, wight: 3
        // 2 <-> 1, wight: 4

        graph.removeEdge(0, 1);
        System.out.println(graph.hasEdge(0, 1)); // false
        System.out.println(graph.hasEdge(1, 0)); // false
    }
}

结论

🚀 如果图很稀疏(M ≪ N²),或者要遍历(BFS/DFS/最短路),选 🔹 邻接表

如果图很稠密(M ≈ N²),或者要频繁查询边权,选 🔸 邻接矩阵

一定要能熟练写出构建图的模版,深刻理解数据结构!!!

相关推荐
坐吃山猪4 小时前
SpringBoot01-配置文件
java·开发语言
我叫汪枫5 小时前
《Java餐厅的待客之道:BIO, NIO, AIO三种服务模式的进化》
java·开发语言·nio
yaoxtao5 小时前
java.nio.file.InvalidPathException异常
java·linux·ubuntu
Swift社区6 小时前
从 JDK 1.8 切换到 JDK 21 时遇到 NoProviderFoundException 该如何解决?
java·开发语言
DKPT7 小时前
JVM中如何调优新生代和老生代?
java·jvm·笔记·学习·spring
phltxy7 小时前
JVM——Java虚拟机学习
java·jvm·学习
seabirdssss8 小时前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续9 小时前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升
benben0449 小时前
ReAct模式解读
java·ai