数据结构图的存储方式:从邻接矩阵到十字链表,一文打尽
图是计算机科学中最灵活、最强大的数据结构之一。社交网络、地图导航、推荐系统......背后都离不开图。但图的存储方式直接影响算法的效率。今天,我们就来彻底搞清楚图的五种存储方式。
作为一名从 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→1,0→1 的 tailLink 指向 0→2。顶点 3 的 firstIn 指向弧 1→3,1→3 的 headLink 指向 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,未经授权禁止转载。