数据结构图的存储方式:从邻接矩阵到十字链表,一文打尽

数据结构图的存储方式:从邻接矩阵到十字链表,一文打尽

图是计算机科学中最灵活、最强大的数据结构之一。社交网络、地图导航、推荐系统......背后都离不开图。但图的存储方式直接影响算法的效率。今天,我们就来彻底搞清楚图的五种存储方式。

作为一名从 Java 后端转算法的开发者,我深知图存储的重要性。选错了存储方式,一个简单的遍历都可能从 O(n) 变成 O(n²),甚至内存爆炸。本文将从最基础的邻接矩阵讲起,逐步深入到十字链表、邻接多重表等高级结构,帮你建立完整的知识体系。


一、图的存储到底在存什么?

图由顶点组成。存储图的核心就是:

  • 存储顶点信息(比如编号、数据)
  • 存储边的关系(谁和谁相连,权重多少)

根据图的类型(有向/无向、带权/无权、稀疏/稠密),我们选择不同的存储方式。


二、五种主流存储方式对比速览

存储方式 适用图类型 空间复杂度 判断边是否存在 遍历邻接点 备注
邻接矩阵 稠密图 O(V²) O(1) O(V) 简单直观,但浪费空间
邻接表 稀疏图 O(V+E) O(degree) O(degree) 最常用,但删除边较慢
逆邻接表 有向图(入边查询) O(V+E) O(degree) O(degree) 邻接表的伴生结构
十字链表 有向图 O(V+E) O(1) 可优化 O(degree) 可同时遍历出边和入边
邻接多重表 无向图(边操作频繁) O(V+E) O(1) 可优化 O(degree) 避免边的重复存储

下面逐一详细讲解。


三、邻接矩阵(Adjacency Matrix)------ 简单粗暴

3.1 定义

用一个 V × V 的二维矩阵 M 表示图。对于无权图:

  • M[i][j] = 1 表示顶点 i 到 j 有边
  • M[i][j] = 0 表示无边

对于带权图,将 1 替换为权重,0 替换为无穷大()。

3.2 示例

无向图:

复制代码
顶点: 0, 1, 2, 3
边: 0-1, 0-2, 1-3, 2-3

邻接矩阵:
    0  1  2  3
0 [ 0, 1, 1, 0 ]
1 [ 1, 0, 0, 1 ]
2 [ 1, 0, 0, 1 ]
3 [ 0, 1, 1, 0 ]

3.3 优缺点

优点

  • 判断两点是否有边:O(1)
  • 实现简单,适合稠密图

缺点

  • 空间 O(V²),V=10000 时,矩阵有 1 亿个元素,内存爆炸
  • 遍历某个顶点的所有邻接点需要 O(V)(即使该顶点只有少数几个邻居)

3.4 Java 代码

java 复制代码
public class AdjacencyMatrix {
    private int[][] matrix;
    private int vertexCount;

    public AdjacencyMatrix(int vertexCount) {
        this.vertexCount = vertexCount;
        matrix = new int[vertexCount][vertexCount];
    }

    // 添加边(无向图)
    public void addEdge(int i, int j) {
        matrix[i][j] = 1;
        matrix[j][i] = 1;
    }

    // 判断边是否存在
    public boolean hasEdge(int i, int j) {
        return matrix[i][j] == 1;
    }

    // 遍历顶点 v 的所有邻接点
    public List<Integer> getNeighbors(int v) {
        List<Integer> neighbors = new ArrayList<>();
        for (int i = 0; i < vertexCount; i++) {
            if (matrix[v][i] == 1) {
                neighbors.add(i);
            }
        }
        return neighbors;
    }
}

四、邻接表(Adjacency List)------ 稀疏图王者

4.1 定义

为每个顶点维护一个链表(或动态数组),存储所有邻接点。对于带权图,链表节点还需存储权重。

4.2 示例

无向图同上:

复制代码
0 -> 1 -> 2
1 -> 0 -> 3
2 -> 0 -> 3
3 -> 1 -> 2

有向图:

复制代码
0 -> 1 -> 2
1 -> 3
2 -> 3
3 -> (空)

4.3 优缺点

优点

  • 空间 O(V+E),非常适合稀疏图(|E| << |V|²)
  • 遍历邻接点的时间 = O(degree(v)),高效

缺点

  • 判断边是否存在需要遍历邻接链表,最坏 O(degree)
  • 删除边稍微麻烦

4.4 Java 代码(使用 ArrayList)

java 复制代码
import java.util.*;

public class AdjacencyList {
    private List<List<Integer>> adj;
    private int vertexCount;

    public AdjacencyList(int vertexCount) {
        this.vertexCount = vertexCount;
        adj = new ArrayList<>(vertexCount);
        for (int i = 0; i < vertexCount; i++) {
            adj.add(new LinkedList<>());
        }
    }

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

    public List<Integer> getNeighbors(int v) {
        return adj.get(v);
    }

    public boolean hasEdge(int i, int j) {
        return adj.get(i).contains(j);
    }
}

五、逆邻接表(Reverse Adjacency List)------ 有向图的"反向索引"

邻接表只能快速获取出边。如果经常需要查询哪些顶点指向我(如 PageRank 算法),就需要逆邻接表。

实现 :额外维护一个 reverseAdj 数组,每条有向边 i→j 同时添加到 reverseAdj.get(j) 中。

空间:仍然是 O(V+E),但两倍常数。

适用:有向图中需要同时正向和反向遍历的场景。


六、十字链表(Orthogonal List)------ 有向图的完美存储

邻接表 + 逆邻接表需要两份边存储,每条边被存两次。十字链表将出入边整合到一个节点中,每条边只存储一次,却能同时快速遍历出边和入边。

6.1 节点结构

顶点表:

复制代码
data   firstOut   firstIn

边表(弧节点):

复制代码
tailVex   headVex   tailLink   headLink   weight(可选)
  • tailVex:弧尾顶点下标
  • headVex:弧头顶点下标
  • tailLink:指向下一条以相同弧尾为起点的边
  • headLink:指向下一条以相同弧头为终点的边

6.2 示例图解

有向图:0→1, 0→2, 1→3, 2→3

顶点 0 的 firstOut 指向弧 0→10→1tailLink 指向 0→2。顶点 3 的 firstIn 指向弧 1→31→3headLink 指向 2→3

6.3 优点

  • 空间 O(V+E)(每条边只存储一次)
  • 可同时高效遍历出边和入边
  • 删除边时无需在两个表中找

6.4 适用场景

有向图且需要频繁查询入边和出边,如编译器中的依赖图、网络流算法。


七、邻接多重表(Adjacency Multi-list)------ 无向图的边复用

在无向图的邻接表中,每条边会被存储两次(在两个顶点的链表里)。这导致删除或修改边时,需要同时操作两个链表,容易出错。邻接多重表让每条边只用一个节点,两个顶点共享。

7.1 节点结构

顶点表:

复制代码
data   firstEdge

边表:

复制代码
mark   vertex1   vertex2   path1   path2
  • vertex1, vertex2:边的两个顶点
  • path1:指向与 vertex1 相连的下一条边
  • path2:指向与 vertex2 相连的下一条边
  • mark:标记边是否被访问(用于遍历)

7.2 优点

  • 每条边只存储一次,节省空间
  • 删除边时只需删除一个节点,操作简单
  • 适合对边频繁增删的场景

7.3 适用场景

无向图且边操作频繁,例如最小生成树算法中动态维护边集合。


八、如何选择?一张决策图

text 复制代码
图是否稠密?
    │
    ├── 是(|E| ≈ |V|²)→ 邻接矩阵
    │
    └── 否(稀疏图)
            │
            └── 是否无向图?
                    │
                    ├── 是 ──→ 是否需要频繁删除边或遍历边?
                    │           ├── 是 → 邻接多重表
                    │           └── 否 → 邻接表
                    │
                    └── 否(有向图)
                            │
                            └── 是否需要同时频繁遍历入边和出边?
                                ├── 是 → 十字链表
                                └── 否 → 邻接表 + (可选逆邻接表)

九、实战建议

场景 推荐存储 理由
社交网络好友关系(稀疏无向) 邻接表 空间省,遍历快
地图导航(有向带权,稀疏) 邻接表(或十字链表) 权重存储方便
蛋白质相互作用网络(稠密无向) 邻接矩阵 判断连接 O(1)
编译器依赖图(有向) 十字链表 同时查依赖和被依赖
图算法教学演示 邻接表 实现简单

十、总结

图的存储没有银弹。理解每种方式的空间复杂度、操作效率和适用图类型,才能在实际开发中做出正确选择。

一句话记忆

稠密用矩阵,稀疏用邻接;有向要双向,十字链表强;无向边多重,删边不慌张。

彩蛋:Facebook 的社交图如何存储?早期用邻接表(因为稀疏),后来发展到自研的 TAO 系统,但底层思想依然离不开这些经典结构。

如果你觉得有帮助,欢迎点赞、收藏、转发~


本文首发于 CSDN,未经授权禁止转载。

相关推荐
Queenie_Charlie2 小时前
关于二叉树(2)
数据结构·c++·二叉树·简单树结构
澈2072 小时前
算法进阶:二叉树翻转与环形链表解析
数据结构·算法·排序算法
代码飞天2 小时前
算法与数据结构之树——让数据查找更加迅速
数据结构·算法
故事和你912 小时前
洛谷-算法2-2-常见优化技巧1
开发语言·数据结构·c++·算法·动态规划·图论
酉鬼女又兒2 小时前
JavaLeetCode 第一题「两数之和」:从暴力枚举到一遍哈希表的正确与错误实现,详解HashMap核心知识点及常见陷阱
java·开发语言·数据结构·算法·leetcode·职场和发展·散列表
云淡风轻~窗明几净2 小时前
关于TSP的sealine算法与角谷猜想(2026-04-25)
数据结构·人工智能·算法·动态规划·模拟退火算法
自我意识的多元宇宙2 小时前
【数据结构】图----图的应用(拓扑排序)
数据结构·算法
Lazionr2 小时前
双向链表及链表篇总结
数据结构·链表
菜菜的顾清寒2 小时前
力扣HOT100(21)相交链表
算法·leetcode·链表