目录
4 十字链表(Orthogonal List / Cross-linked List, 十字链表)
5 邻接多重表(Adjacency Multilist / Incidence List for Undirected Graphs)
6 关联矩阵(Incidence Matrix / 关联矩阵)
在上文已经介绍图的基本概念之后,本节将围绕图的数据结构表示展开,系统介绍常见的在内存中表示图的几种经典数据结构:邻接矩阵、邻接表、边列表、十字链表、邻接多重表、关联矩阵,并给出每种表示的定义、存储结构、优缺点、适用场景、复杂度分析与Java朴素实现。最后讨论如何在关系型数据库(RDBMS)中建模图结构。
一 图的数据结构表示
1 邻接矩阵(Adjacency Matrix)
定义与结构 邻接矩阵用于表示顶点集合为 V=\{v_0,\dots,v_{n-1}\} 的图。构造一个 n\times n 的二维矩阵 A
,对于无向图,A[i][j]=1
表示顶点 v_i 与 v_j 之间存在边;若有权图则放权值 A[i][j]=w
;无边时可用 0
、∞
或 null
表示,取决于实现。
举例(无向无权) 顶点 0 与 2 有边,则 A[0][2] = A[2][0] = 1
。
优点
随机访问边存在性:判断两顶点是否相邻为 O(1)。
矩阵代数便于做图论中的一些理论运算。
实现简单,适合顶点数 n 较小或图密集(边数接近 n^2)时使用。
缺点
空间复杂度为 O(n^2),对稀疏图非常浪费。
枚举某一顶点的邻居需要 O(n) 扫描整行(即使度很小也要扫描)。
动态插入大量顶点成本高(需要扩展矩阵)。
复杂度
空间:O(n^2)
判断边存在:O(1)
枚举邻居:O(n)
插入/删除边:O(1)
适用场景
n 小或图密集(dense graph)。
需要频繁 O(1) 边查找或矩阵运算(例如某些线性代数方法)。
Java朴素实现
/** * 图的邻接矩阵表示 */ public class MatrixGraph<V,E,ED> { // 最大顶点个数 public static final int MAX_VERTEX_NUM = 20; // 顶点向量集合,用来存储顶点信息,数组索引作为数据索引,用来构建邻接矩阵 private Vertex<V>[] vexs; // 邻接矩阵 private AdjMatrix<E,ED> arcs = new AdjMatrix<>(); // 图的当前顶点数和弧数 public int vexnum; public int arcnum; // 图的种类标志 public GraphKind kind; @SuppressWarnings("unchecked") public MatrixGraph() { vexs = (Vertex<V>[])new Vertex<?>[MAX_VERTEX_NUM]; // 初始化邻接矩阵的每个单元格 for (int i = 0; i < MAX_VERTEX_NUM; i++) { for (int j = 0; j < MAX_VERTEX_NUM; j++) { arcs.cells[i][j] = new ArcCell<>(); } } } // 图的种类 {有向图,有向网,无向图,无向网} public enum GraphKind { DG, DN, UDG, UDN } public static class Vertex<V>{ public V data; } // 弧单元定义 public static class ArcCell<E,ED> { // 顶点关系类型。对无权图,用 1 或 0 表示相邻否;对带权图,则为权值类型 public E adj; // 该弧相关信息 public ED info; } // 邻接矩阵类型 public static class AdjMatrix<E,ED> { public ArcCell<E,ED>[][] cells = new ArcCell[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; } // 提供访问vexs和arcs的公共方法 public Vertex<V>[] getVexs() { return vexs; } public AdjMatrix<E,ED> getArcs() { return arcs; } }
2 邻接表(Adjacency List)
定义与结构 邻接表为每个顶点维护一个邻居列表(通常链表、动态数组或向量),每个列表包含该顶点的所有出/入(或无向)邻接顶点及可选的边权和边属性。
举例(有向有权) adj[u]
列表中存储 (v, weight, metadata)
表示从 u
到 v
的边。
优点
空间友好:空间复杂度为 O(n + m),其中 m 是边数,适用于稀疏图。
枚举某一顶点的所有出边非常高效,时间与该顶点度数成正比。
插入边易,删除边在双向链表或带索引结构中也较为方便。
缺点
判断任意两顶点是否有边不是 O(1)(需要扫描较短列表或使用辅助哈希)。
若需要高并发、并行访问单个顶点的邻居,需考虑同步和锁策略。
复杂度
空间:O(n + m)
枚举某一顶点邻居:O(\deg(v))
判断边存在(线性扫描):O(\deg(u))
插入边:均摊 O(1)
删除边:O(\deg(u))(可借辅助索引优化)
适用场景
大多数实际稀疏图(社交网络、道路网络、依赖图等)。
常配合图遍历(BFS、DFS)、最短路径算法(Dijkstra、Bellman-Ford)使用。
Java朴素实现
/** * 图的邻接表数据结构表示 */ public class AdjacencyListGraph<V, E> { public static final int MAX_VERTEX_NUM = 20; public VNode<V, E>[] vertices; // 邻接表 public int vexnum; // 图的当前顶点数 public int arcnum; // 图的当前弧数 public GraphKind kind = GraphKind.UDG; // 图的种类标志 @SuppressWarnings("unchecked") public AdjacencyListGraph() { vertices = (VNode<V, E>[]) new VNode<?,?>[MAX_VERTEX_NUM]; vexnum = 0; arcnum = 0; } // 图的种类 {有向图,有向网,无向图,无向网} public enum GraphKind { DG, DN, UDG, UDN } // 弧节点类 public static class ArcNode<E> { public int adjvex; // 该弧所指向的顶点的位置 public ArcNode<E> nextarc; // 指向下一条弧的指针 public E info; // 该弧相关信息的指针(泛型类型) public ArcNode(int adjvex, E info) { this.adjvex = adjvex; this.info = info; this.nextarc = null; } } // 顶点节点类 public static class VNode<V, E> { public V data; // 顶点信息(泛型类型) public ArcNode<E> firstarc; // 指向第一条依附该顶点的弧的指针 public VNode(V data) { this.data = data; this.firstarc = null; } } // 示例使用方法 public void addVertex(int index, V data) { if (index >= 0 && index < MAX_VERTEX_NUM) { vertices[index] = new VNode<>(data); vexnum++; } } public void addArc(int fromIndex, int toIndex, E info) { if (fromIndex >= 0 && fromIndex < MAX_VERTEX_NUM && toIndex >= 0 && toIndex < MAX_VERTEX_NUM) { ArcNode<E> newArc = new ArcNode<>(toIndex, info); newArc.nextarc = vertices[fromIndex].firstarc; vertices[fromIndex].firstarc = newArc; arcnum++; } } }
3 边列表(Edge List)
定义与结构 边列表把图表示为一组边的集合,每条边表示为二元组(或三元组带权):(u, v)
或 (u, v, w)
。通常用数组 edges[]
存储。
优点
表示简单、紧凑,易于序列化与外部存储。
适合做基于边的批处理算法(如 Kruskal 最小生成树需要排序边)。
插入边非常简单(append)。
缺点
判断边是否存在需要 O(m)(除非配合索引或哈希)。
枚举某一顶点的邻居低效(需扫描全部边或建立额外索引)。
复杂度
空间:O(m)
遍历所有边:O(m)
判断边存在:O(m)(可建哈希加速)
插入边:O(1)(append)
适用场景
批量边处理(排序、过滤、图转化)。
构建初始图数据后再转换为邻接表进行算法处理。
文件格式与数据交换(如 CSV、边列表文件)。
Java朴素实现
/** * 图的边列表表示 */ public class EdgeListGraph<V,E> { public static final int MAX_EDGE_NUM = 20; public Edge<V,E>[] edges; public int edgenum; public EdgeListGraph(){ this.edges = (Edge<V,E>[])new Edge<?,?>[MAX_EDGE_NUM]; this.edgenum = 0; } // 边类包含顶点数据,不包含顶点对象引用 public static class Edge<V,E>{ V fromData; // 直接存储顶点数据 V toData; E data; public Edge(V fromData, V toData, E data){ this.fromData = fromData; this.toData = toData; this.data = data; } } }
4 十字链表(Orthogonal List / Cross-linked List, 十字链表)
定义与结构 十字链表是面向有向图的一种链式表示,期望同时高效访问每个顶点的入边和出边。结构通常包含顶点节点数组,每个顶点维护两个链首:出边链表和入边链表;每条边节点包含 hlink
指向相同弧在出链中的下一个弧,tlink
指向相同弧在入链中的下一个弧,以及 tailvex
(弧尾顶点)、headvex
(弧头顶点)和边权等信息。
示意结构
Vertex { firstout; // 指向以该顶点为尾的第一条弧 firstin; // 指向以该顶点为头的第一条弧 } Edge { tailvex, headvex; tlink; // 在尾链(出链)上 hlink; // 在头链(入链)上 weight, info; }
优点
同时支持对入边和出边的高效遍历:枚举出边或入边都只需沿对应链表遍历。
适合需要频繁双方访问(入边和出边)的有向图算法(如强连通分量、反向遍历等)。
缺点
较邻接表实现更复杂(边维护两个链指针)。
空间开销较邻接表稍大(每条边额外的指针字段)。
在无向图上不适用(无向图可用邻接多重表替代)。
复杂度
空间:O(n + m)
枚举出边/入边:O(\deg_{out}(v)) 或 O(\deg_{in}(v))
插入边:O(1)
删除边:需要维护两条链,复杂度 O(1) 在已知前驱时,否则需要搜索。
适用场景
有向图且经常需要访问入边与出边。
图分析。
Java朴素实现
/** * 有向图的十字链表存储表示 */ public class OrthogonalListGraph<V, E> { public static final int MAX_VERTEX_NUM = 20; public VexNode<V,E>[] xlist; // 表头向量(顶点数组) public int vexnum; // 有向图的当前顶点数 public int arcnum; // 有向图的当前弧数 @SuppressWarnings("unchecked") public OrthogonalListGraph() { xlist = (VexNode<V,E>[]) new VexNode<?,?>[MAX_VERTEX_NUM]; vexnum = 0; arcnum = 0; } public static class ArcBox<E> { public int tailvex; // 该弧的尾顶点的位置 public int headvex; // 该弧的头顶点的位置 public ArcBox<E> hlink; // 弧头相同的弧的链域 public ArcBox<E> tlink; // 弧尾相同的弧的链域 public E info; // 该弧相关信息的指针 } public static class VexNode<V,E> { public V data; // 顶点数据 public ArcBox<E> firstin; // 指向该顶点第一条入弧 public ArcBox<E> firstout; // 指向该顶点第一条出弧 } }
5 邻接多重表(Adjacency Multilist / Incidence List for Undirected Graphs)
定义与结构 邻接多重表主要为无向图服务,目的是避免在无向图中对同一条边做两次独立记录(在普通邻接表中无向边常被记录两次:u->v 和 v->u)。邻接多重表为每条无向边只创建一个边结点,边结点有两个指针分别连接到两个端点的邻接链表中。
示意结构
Edge { iVex, jVex; // 两端点 iLink, jLink; // 分别连接到 v1 的邻接链及 v2 的邻接链 weight, info; } Vertex { firstedge; // 指向该顶点的第一条边(EdgeNode) }
优点
每条无向边仅存储一次,节省空间(对某些实现与场景)。
删除边时只需操作一个边节点,便于维护一致性。
遍历顶点邻居仍然高效,时间随度增长。
缺点
实现稍复杂(边节点需要两个邻接指针)。
在需要为边赋予方向属性或在有向图上使用时不适用。
复杂度
空间:O(n + m)
枚举邻居:O(\deg(v))
插入边:O(1)
删除边:在已知位置可为 O(1),否则需找到前驱。
适用场景
无向图中希望使每条边只存一次的场景。
图操作(删除、修改边)要求单点一致性的场景。
Java朴素实现
/** * 无向图的邻接多重表存储表示 */ public class AdjacencyMultilistGraph<V,E> { public static final int MAX_VERTEX_NUM = 20; public VexBox<V,E>[] adjmulist; // 邻接多重表 public int vexnum; // 当前顶点数 public int edgenum; // 当前边数 @SuppressWarnings("unchecked") public AdjacencyMultilistGraph() { this.adjmulist = (VexBox<V, E>[]) new VexBox<?,?>[MAX_VERTEX_NUM]; this.vexnum = 0; this.edgenum = 0; } public static class EBox<E> { public int iVex; // 该边依附的两个顶点的位置 public int jVex; // 该边依附的两个顶点的位置 public EBox<E> iLink; // 指向依附顶点iVex的下一条边 public EBox<E> jLink; // 指向依附顶点jVex的下一条边 public E info; // 该边信息指针 } public static class VexBox<V,E> { public V data; // 顶点数据 public EBox<E> firstedge; // 指向第一条依附该顶点的边 } }
6 关联矩阵(Incidence Matrix / 关联矩阵)
定义与结构 关联矩阵是 n \times m 的矩阵,行表示顶点,列表示边。对于无向图,如果边 e_j 连接顶点 v_i,则 B[i][j] = 1
(或其他标记,如端点编号);对于有向图可以用 1
表示出端点,-1
表示入端点(或用 1/0 和方向表述)。
举例(有向图) 若第 j 条边从 v_p 指向 v_q,则 B[p][j] = 1
,B[q][j] = -1
,其他行为 0。
优点
适合处理边与顶点之间的“关系”分析(如网络流基础理论、线性代数方法)。
在某些数学分析(如图的循环空间、切空间)中方便用线性代数处理。
缺点
空间复杂度 O(nm),对大型图不友好(尤其当边数 m 很大时)。
枚举邻接关系较为低效(需要扫描对应列或行)。
一般用于理论分析或非常特殊的算法场景,在工程实现中较少单独使用。
复杂度
空间:O(nm)
通过矩阵计算可得到路径关系等,但代价昂贵。
适用场景
图的代数性质分析(如环空间、基循环、切空间)。
网络流中部分数学建模。
Java朴素实现
/** * 图的关联矩阵表示 */ public class IncidenceMatrixGraph<V,E> { public static final int MAX_VERTEX_NUM = 20; public static final int MAX_EDGE_NUM = 100; // 顶点数组 public Vertex<V>[] vertices; // 边数组(只存储边数据,不存储端点信息) public Edge<E>[] edges; // 关联矩阵:表达顶点与边的关联关系 public int[][] matrix; // 当前顶点数和边数 public int vernum; public int edgenum; // 图类型:0-无向图,1-有向图 public int graphType; public IncidenceMatrixGraph(int graphType) { this.vertices = (Vertex<V>[])new Vertex<?>[MAX_VERTEX_NUM]; this.edges = (Edge<E>[])new Edge<?>[MAX_EDGE_NUM]; this.matrix = new int[MAX_VERTEX_NUM][MAX_EDGE_NUM]; this.vernum = 0; this.edgenum = 0; this.graphType = graphType; } public static class Vertex<V> { V data; public Vertex(V data) { this.data = data; } } // 边只存储自身数据,端点信息由矩阵表达 public static class Edge<E> { E data; public Edge(E data) { this.data = data; } } }
二 图的 RDBMS 建模
虽然图结构在本质上是非关系型的,但在没有图数据库(如 Neo4j、JanusGraph)可用的场景下,我们仍可以通过关系型数据库(RDBMS)来表达图的数据结构与连接关系。
1 建模基本思路
图在 RDBMS 中建模的核心思想是将顶点和边分别表示为关系表(Relation):
顶点表(Vertex Table):存储图中的所有节点;
边表(Edge Table):存储图中所有的边(连接关系);
标签表(Label Table): 存储顶点和边的标签信息;
2 表结构设计
(1)顶点表设计
CREATE TABLE vertex ( id INT PRIMARY KEY, -- 顶点唯一标识 label_id INT NOT NULL, -- 顶点类型 properties JSON, -- 顶点属性 FOREIGN KEY (label_id) REFERENCES label(id) );
properties
字段可以灵活地以 JSON 形式存储各类属性,适合属性异构的图场景。如果属性固定,也可将其展开为单独的列。
(2)边表设计(适用于有向图)
CREATE TABLE edge ( id INT PRIMARY KEY, -- 边的唯一标识 from_vertex INT NOT NULL, -- 起点 to_vertex INT NOT NULL, -- 终点 label_id INT NOT NULL, -- 边的类型 weight DECIMAL(10,2), -- 权重 properties JSON, -- 边的属性 FOREIGN KEY (from_vertex) REFERENCES vertex(id), FOREIGN KEY (to_vertex) REFERENCES vertex(id), FOREIGN KEY (label_id) REFERENCES label(id) );
该结构适用于有向图(Directed Graph);
支持为每条边添加类型、权重或其他灵活的属性;
使用外键约束维护边和顶点之间的完整性。
(3)标签元数据表设计
CREATE TABLE label ( id INT PRIMARY KEY, -- 标签唯一标识 label VARCHAR(100), -- 标签名称 category INT NOT NULL -- 标签分类:标识顶点标签还是边标签(例如:1=顶点,2=边) );
(4)无向图处理方法
对于无向图(Undirected Graph),可使用以下两种策略之一:
对称存储:每条边存储两次,分别是
(u → v)
和(v → u)
;约束存储顺序:强制存储一次,并约定
from_vertex < to_vertex
,查询时统一处理对称性。
三 图算法基础数据结构-邻接表
在前文我们已经介绍了图的多种数据结构表示方式(邻接矩阵、邻接表、边列表、十字链表、邻接多重表、关联矩阵),并且给出了其Java的朴素实现。在进入后续的图算法部分之前,有必要确定一种在算法实现中最常用的数据结构作为统一输入。实践中,邻接表(Adjacency List) 因其存储效率高、空间利用率好、便于图遍历与路径搜索,被广泛用于图算法的实现。因此,本节我们将详细介绍一个面向后续算法实现的邻接表对象建模与代码示例。
1 邻接表建模思路
在图的抽象建模中,我们至少需要三个核心要素:
顶点(Vertex):表示对象实体,可以附带标签(如类型)和属性信息。
边(Edge):表示对象之间的关系,通常包含权重、方向、标签和属性。
图(Graph):由一组顶点和边构成,提供统一的访问与管理接口。
此外,还需要一个标签类(Label),用来区分不同类型的顶点和边。这种抽象设计不仅符合图论的基本定义,同时为后续复杂算法(如最短路径、拓扑排序、强连通分量分解等)的实现提供了良好的扩展性。
2 代码实现
下面的 Java 代码示例展示了如何用面向对象方式构建邻接表:
(1) 标签类 Label
import java.util.concurrent.atomic.AtomicInteger; public class Label { private static final AtomicInteger labelIdCounter = new AtomicInteger(0); private Integer id; // 隐式ID,自增 private String label; // 标签名称 private Integer category; // 1-顶点 2-边 public Label(String label, Integer category) { this.id = genId(); this.label = label; this.category = category; } public int getId() { return id; } public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } public Integer getCategory() { return category; } public void setCategory(Integer category) { this.category = category; } private int genId() { return labelIdCounter.getAndIncrement(); } // 分类枚举 enum Category { VERTEX(1), EDGE(2); Integer id; Category(int id) { this.id = id; } } }
该类为顶点和边赋予标签及唯一 ID,方便后续分类和检索。
(2) 顶点类 Vertex
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; public class Vertex { private static final AtomicInteger nodeIdCounter = new AtomicInteger(0); private Integer id; // 顶点ID private String name; // 顶点显示名称 private Label label; // 顶点标签 private Map<String, Object> properties; // 顶点属性 private Map<Integer, Edge> edgeList; // 邻接边列表 public Vertex(String name, Label label) { this.id = genId(); this.name = name; this.label = label; this.properties = new HashMap<>(); this.edgeList = new HashMap<>(); } public Integer getId() { return id; } public String getName() { return name; } public Label getLabel() { return label; } public Map<String, Object> getProperties() { return properties; } public List<Edge> getEdgeList() { return new ArrayList<>(this.edgeList.values()); } public Object getProperty(String key) { return this.properties.get(key); } public void setProperty(String key, Object value) { this.properties.put(key, value); } private int genId() { return nodeIdCounter.getAndIncrement(); } // 添加邻接边 public void addEdge(Edge edge) { this.edgeList.put(edge.getId(), edge); } public void addEdge(Vertex to, int weight, Label label) { Edge edge = new Edge(this, to, weight, label); this.edgeList.put(edge.getId(), edge); } }
顶点类不仅保存了名称与标签,还维护了与其相连的边集合(邻接表的核心结构)。
(3) 边类 Edge
import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; public class Edge { private static final AtomicInteger edgeIdCounter = new AtomicInteger(0); private Integer id; // 边ID private Vertex from; // 起始顶点 private Vertex to; // 目标顶点 private Label label; // 边标签 private Integer weight; // 边权重 private Map<String, Object> properties; // 边属性 public Edge(Vertex from, Vertex to, int weight, Label label) { this.id = genId(); this.from = from; this.to = to; this.label = label; this.weight = weight; this.properties = new HashMap<>(); } public Integer getId() { return id; } public Vertex getFrom() { return from; } public Vertex getTo() { return to; } public Label getLabel() { return label; } public Integer getWeight() { return weight; } public Map<String, Object> getProperties() { return properties; } public Object getProperty(String key) { return this.properties.get(key); } public void setProperty(String key, Object value) { this.properties.put(key, value); } private int genId() { return edgeIdCounter.getAndIncrement(); } }
该类定义了边的基本信息,包括权重与属性信息,支持灵活扩展。
(4) 图类 AdjacencyGraph
import java.util.HashMap; import java.util.Map; public class AdjacencyGraph { private Map<Integer, Vertex> vertices; private Integer vertexNum; private Integer edgeNum; public AdjacencyGraph() { vertices = new HashMap<>(); vertexNum = 0; edgeNum = 0; } public Map<Integer, Vertex> getVertices() { return vertices; } public Integer getVertexNum() { return vertexNum; } public Integer getEdgeNum() { return edgeNum; } // 添加顶点 public void addVertex(Vertex v) { vertices.put(v.getId(), v); vertexNum++; } // 添加边 public void addEdge(Edge edge) { edge.getFrom().addEdge(edge); edgeNum++; } }
图类 AdjacencyGraph
用来维护全局的顶点与边集合,并且通过邻接表结构保证每个顶点能快速访问其邻接点。
3 说明
邻接表不仅是算法输入的常用数据结构,同时也非常贴合现实世界的图数据存储需求。通过 Label
、Vertex
、Edge
、AdjacencyGraph
的建模,我们能够清晰地表达图的各个组成部分,并为后续的图算法实现(如深度优先搜索、广度优先搜索、Dijkstra 最短路径、Prim/Kruskal 最小生成树等)提供了统一的数据接口。