图(Graph)是一种比树更复杂的非线性数据结构,它由顶点(Vertex) 和边(Edge) 组成,用于描述事物之间的多对多关系(树仅能描述一对多的层级关系)。从社交网络的 "好友关系" 到地图的 "路线连接",从电路的 "元件连接" 到互联网的 "节点通信",图的应用贯穿生活与技术的方方面面,核心价值是 "建模复杂关联关系并支持高效查询"。
一、图中的基本概念
- 顶点(Vertex):图中的核心元素,也称为 "节点",用于表示一个实体(如社交网络中的用户、地图中的地点)。
- 边(Edge):连接两个顶点的线,用于表示顶点之间的关系(如社交网络中的 "好友"、地图中的 "道路")。
- 邻接(Adjacent):若两个顶点之间存在边,则称这两个顶点互为邻接顶点(如顶点 A 和 B 之间有边,则 A 邻接于 B,B 邻接于 A)。
- 度(Degree) :一个顶点拥有的边的数量。在有向图中,度分为入度(In-degree,指向该顶点的边数) 和出度(Out-degree,从该顶点出发的边数)。
在具有 n 个顶点、e 条边的无向图中,始终有 总度数 = 2e,即无向图的全部顶点的度的和等于边数的 2 倍,因为每条变和两个顶点相关联。
对于有向图,始终有 总度数 = 入度 + 出度;在具有 n 个顶点、e 条边的有向图中,有向图的全部顶点的入度之和与出度之和相等,并且等于边数。
- **简单图:**一个图 G 若满足:①不存在重复边;②不存在顶点到自身的边,则称图G 为简单图。
- **多重图:**若图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则G为多重图。
- **完全图:**对于无向图,∣ E ∣的取值范围是0到n ( n − 1 ) / 2 ,有n ( n − 1 ) / 2条边的无向图称为完全图,在完全图中任意两个顶点之间都存在边。对于有向图,∣ E ∣的取值范围是0到n ( n − 1 ) ,有n ( n − 1 ) 条弧的有向图称为有向完全图,在有向完全图中任意两个顶点之间都存在方向相反的两条弧。
- **子图:**设有两个图G = ( V , E )和G ′ = ( V ′ , E ′ ) , 若V ′ 是V的子集,且E ′ 是E的子集,则称G ′ 是G的子图。若有满足V ( G ′ ) = V ( G )的子图G ′ ,则称其为G的生成子图。
- 路径(Path):从一个顶点到另一个顶点的边的序列(如顶点 A→B→C,路径长度为 2,即边的数量)。
- 路径长度: 路径上边的数目称为路径长度。
- 环(Cycle):起点和终点相同的路径(如顶点 A→B→C→A,形成一个环);无环的图称为 "无环图"。若一个图有n 个顶点,并且有大于n − 1条边,则此图一定有环。
- 权重(Weight):边的附加信息,用于表示关系的 "代价" 或 "强度"(如地图中道路的长度、社交网络中好友的亲密度)
- 网: 边上带有权值的图称为带权图 ,也称网。
- **稠密图、稀疏图:**边数很少的图称为稀疏图,反之称为稠密图。稀疏和稠密本身是模糊的概念,稀疏图和稠密图常常是相对而言的。一般当图G满足∣ E ∣ < ∣V∣ log∣V∣ 时,可以将G视为稀疏图。
- **简单路径:**在路径序列中,顶点不重复出现的路径称为简单路径。
- **简单回路:**除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
- **距离:**从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。若从u到v根本不存在路径,则记该距离为无穷( ∞ )。
- **有向树:**一个顶点的入度为0、其余顶点的入度均为1的有向图,称为有向树。
- **生成树:**连通图的生成树是包含图中全部顶点的一个极小连通子图。若图中顶点数为n,则它的生成树含有n−1条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。
- **生成森林:**在非连通图中,连通分量的生成树构成了非连通图的生成森林。图的一个生成树如下图所示。

注意:包含无向图中全部顶点的极小连通子图,只有生成树满足条件,因为砍去生成树的任一条边,图将不再连通。
- **连通、连通图、连通分量:**在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。无向图中的极大连通子图称为连通分量。若一个图有n个顶点,并且边数小于n-1,则此图必是非连通图。如下图(a)所示, 图G4有3个连通分量,如图(b)所示。

注意:弄清连通、连通图、连通分量的概念非常重要。首先要区分极大连通子图和极小连通子图,极大连通子图是无向图的连通分量,极大即要求该连通子图包含其所有的边;极小连通子图是既要保持图连通又要使得边数最少的子图。
-
强连通图、强连通分量: 在有向图中,若从顶点v到顶点w和从顶点w到项点v之间都有路径,则称这两个顶点是强连通的。若图中任何一对顶点都是强连通的,则称此图为强连通图。有向图中的极大强连通子图称为有向图的强连通分量,图G1的强连通分量如下图所示。

注意:强连通图、强连通分量只是针对有向图而言的。一般在无向图中讨论连通性,在有向图中考虑强连通性。
二、图的分类
|----------|-------------------------|----------------------------------------------|------------------|
| 分类维度 | 具体类型 | 核心特点 | 示例场景 |
| 边是否有方向 | 无向图(Undirected Graph) | 边无方向,若 A、B 存在,则 A 可到 B、B 也可到 A (边用(A,B)表示) | 社交网络的好友关系、无向道路 |
| | 有向图(Directed Graph) | 边有方向,若 A→B 存在,不代表 B→A 存在(边用<A,B>表示) | 互联网的 URL 跳转、任务依赖 |
| 边是否有权重 | 无权图(Unweighted Graph) | 边无附加信息,仅表示 "是否关联" | 社交网络的关注关系 |
| | 加权图(Weighted Graph) | 边有权重,用于表示 "关联的代 价"(也称为 "网") | 地图的路线长度、物流成本 |
| 顶点是否连通 | 连通图(Connected Graph) | 无向图中任意两个顶点都存在路径(有向图中称为 "强连通图",需双向可达) | 完整的城市交通网 |
| | 非连通(Disconnected Graph) | 存在至少两个顶点无路径连通,图由多个 "连通分量" 组成 | 多个独立的局域网 |
| 其他特殊类型 | 树(Tree) | 无环的连通图(边数 = 顶点数 - 1),是图的特殊子集 | 组织架构、文件系统 |
| | 有向无环图(DAG) | 无环的有向图,常用于 "拓扑排 序" | 项目任务调度、课程依赖关系 |
三、图的存储方式
图的存储需高效表达 "顶点集合" 和 "边的关联关系",常见的存储方式有两种:邻接矩阵(Adjacency Matrix) 和 邻接表(Adjacency List),二者在空间复杂度和时间复杂度上各有优劣。
一、邻接矩阵
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
1.注意:
①在简单应用中,可直接用二维数组作为图的邻接矩阵(顶点信息等均可省略)。
②当邻接矩阵中的元素仅表示相应的边是否存在时,EdgeType可定义为值为0和1的枚举类型。
③无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩阵可采用压缩存储。
④邻接矩阵表示法的空间复杂度为O ( n^2 ), 其中n为图的顶点数∣ V ∣。
⑤ 用邻接矩阵法存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。
⑥ 稠密图适合使用邻接矩阵的存储表示。


可以看出:
-
无向图的邻接矩阵一定是一个对称矩阵(即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的)。 因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素。
-
对于无向图,邻接矩阵的第i 行(或第i 列)非零元素(或非∞元素)的个数正好是第i 个顶点的度TD ( v i )。比如顶点v1的度就是1 + 0 + 1 + 0 = 2 。
3.对于有向图,邻接矩阵不一定会对称;邻接矩阵的第i 行和第i 列非零元素的和为该顶点的度。
- 求顶点vi 的所有邻接点就是将矩阵中第i行元素扫描一遍,A [ i ] [ j ] 为 1就是邻接点。


2.具体代码实现:
java
public class GraphMatrix {
private int vertexCount; // 顶点数量
private int[][] matrix; // 邻接矩阵
private static final int INF = Integer.MAX_VALUE; // 表示无边
// 构造函数:初始化顶点数和邻接矩阵
public GraphMatrix(int vertexCount) {
this.vertexCount = vertexCount;
matrix = new int[vertexCount][vertexCount];
// 初始化矩阵:对角线为0(自身无连接),其他为INF(无边)
for (int i = 0; i < vertexCount; i++) {
for (int j = 0; j < vertexCount; j++) {
if (i == j) matrix[i][j] = 0;
else matrix[i][j] = INF;
}
}
}
// 添加边(无向加权图)
public void addEdge(int from, int to, int weight) {
if (from < 0 || from >= vertexCount || to < 0 || to >= vertexCount) {
throw new IllegalArgumentException("顶点索引无效");
}
matrix[from][to] = weight;
matrix[to][from] = weight; // 无向图:双向边
}
// 打印邻接矩阵
public void printMatrix() {
for (int i = 0; i < vertexCount; i++) {
for (int j = 0; j < vertexCount; j++) {
if (matrix[i][j] == INF) {
System.out.print("∞ ");
} else {
System.out.print(matrix[i][j] + " ");
}
}
System.out.println();
}
}
// 测试
public static void main(String[] args) {
GraphMatrix graph = new GraphMatrix(3);
graph.addEdge(0, 1, 2);
graph.addEdge(0, 2, 5);
graph.addEdge(1, 2, 1);
graph.printMatrix();
// 输出:
// 0 2 5
// 2 0 1
// 5 1 0
}
}
二、邻接表
所谓邻接表,是指对图G 中的每个顶点vi 建立一个单链表,第i 个单链表中的结点表示依附于顶点vi 的边(对于有向图则是以顶点vi 为尾的弧),这个单链表就称为顶点vi 的边表(对于有向图则称为出边表)。
1.注意:
边表的头指针和顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种结点:顶点表结点和边表结点。

邻接表是用 "数组 + 链表(或集合)" 存储图的方式:
- 用一个数组(或列表)存储所有顶点,数组索引对应顶点编号;
- 每个顶点对应一个 "邻接列表",存储该顶点的所有邻接顶点(及边的权重,若为加权图)。
- 适用于元素比较少的稀疏图。


另外,对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可。
2.具体代码实现:
java
// 边的实体类:存储邻接顶点和边的权重
class Edge {
int to; // 邻接顶点的编号
int weight; // 边的权重
public Edge(int to, int weight) {
this.to = to;
this.weight = weight;
}
@Override
public String toString() {
return "(" + to + ", " + weight + ")";
}
}
java
import java.util.ArrayList;
import java.util.List;
public class GraphList {
private int vertexCount; // 顶点数量
private List<List<Edge>> adjList; // 邻接表:索引=顶点,值=邻接边列表
// 构造函数:初始化顶点数和邻接表
public GraphList(int vertexCount) {
this.vertexCount = vertexCount;
adjList = new ArrayList<>(vertexCount);
// 为每个顶点初始化空的邻接列表
for (int i = 0; i < vertexCount; i++) {
adjList.add(new ArrayList<>());
}
}
// 添加边(无向加权图)
public void addEdge(int from, int to, int weight) {
if (from < 0 || from >= vertexCount || to < 0 || to >= vertexCount) {
throw new IllegalArgumentException("顶点索引无效");
}
adjList.get(from).add(new Edge(to, weight));
adjList.get(to).add(new Edge(from, weight)); // 无向图:双向添加
}
// 打印邻接表
public void printAdjList() {
for (int i = 0; i < vertexCount; i++) {
System.out.print("顶点" + i + "的邻接列表:" + adjList.get(i));
System.out.println();
}
}
// 测试
public static void main(String[] args) {
GraphList graph = new GraphList(3);
graph.addEdge(0, 1, 2);
graph.addEdge(0, 2, 5);
graph.addEdge(1, 2, 1);
graph.printAdjList();
// 输出:
// 顶点0的邻接列表:[(1, 2), (2, 5)]
// 顶点1的邻接列表:[(0, 2), (2, 1)]
// 顶点2的邻接列表:[(0, 5), (1, 1)]
}
}
四、图的遍历
图的遍历是和树的遍历类似,我们希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次, 这一过程就叫做图的遍历(Traversing Graph)。
对于图的遍历来,通常有两种遍历次序方案:它们是深度优先搜索(DFS)和广度优先搜索(BFS)。
一、深度优先遍历(Depth First Search)
DFS 的核心思想是 "递归 / 栈":从起点出发,优先访问当前顶点的邻接顶点,直到无法继续深入,再回溯到上一顶点,选择其他未访问的邻接顶点继续深入(类似 "迷宫探索")。
1.深度搜索过程
深度优先搜索类似于树的先序遍历。如其名称中所暗含的意思一样,这种搜索算法所遵循的搜索策略是尽可能"深"地搜索一个图。它的基本思想如下:首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一顶点w1,再访问与w1邻接且未被访问的任一顶点...重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。
2.算法实现(递归实现)
- 初始化一个
visited数组,标记顶点是否已访问; - 从起点
start出发,标记start为已访问; - 遍历
start的所有邻接顶点,若未访问,则递归调用 DFS 访问该顶点。
3. 代码实现:基于 GraphList 类扩展
java
// DFS:递归实现,从start顶点开始遍历
public void dfs(int start, boolean[] visited) {
// 标记当前顶点为已访问
visited[start] = true;
System.out.print(start + " "); // 访问顶点(此处为打印,实际可替换为业务逻辑)
// 遍历当前顶点的所有邻接顶点
for (Edge edge : adjList.get(start)) {
int neighbor = edge.to;
if (!visited[neighbor]) { // 若邻接顶点未访问,递归遍历
dfs(neighbor, visited);
}
}
}
// 对外提供的DFS方法(初始化visited数组)
public void dfs(int start) {
if (start < 0 || start >= vertexCount) {
throw new IllegalArgumentException("起点索引无效");
}
boolean[] visited = new boolean[vertexCount]; // 初始均为false
dfs(start, visited);
}
// 测试DFS:从顶点0开始遍历
public static void main(String[] args) {
GraphList graph = new GraphList(4);
graph.addEdge(0, 1, 1);
graph.addEdge(0, 2, 1);
graph.addEdge(1, 3, 1);
graph.addEdge(2, 3, 1);
System.out.print("DFS遍历结果(从0开始):");
graph.dfs(0); // 输出:0 1 3 2(或0 2 3 1,取决于邻接列表顺序)
}
非递归实现(栈)
递归实现可能因图深度过大导致栈溢出,可改用 "栈" 模拟递归过程:
java
// DFS:非递归实现(栈)
public void dfsNonRecursive(int start) {
if (start < 0 || start >= vertexCount) {
throw new IllegalArgumentException("起点索引无效");
}
boolean[] visited = new boolean[vertexCount];
Stack<Integer> stack = new Stack<>();
// 起点入栈并标记
stack.push(start);
visited[start] = true;
while (!stack.isEmpty()) {
int current = stack.pop(); // 弹出当前顶点
System.out.print(current + " "); // 访问顶点
// 遍历邻接顶点(注意:栈是LIFO,为保持与递归一致的顺序,需逆序入栈)
List<Edge> neighbors = adjList.get(current);
for (int i = neighbors.size() - 1; i >= 0; i--) {
int neighbor = neighbors.get(i).to;
if (!visited[neighbor]) {
visited[neighbor] = true;
stack.push(neighbor);
}
}
}
}
4.DFS 算法的性能分析
DFS算法是一个递归算法,需要借助一个递归工作栈,故其空间复杂度为O(V)。对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点的邻接点需要访问矩阵中的所有元素,因此都需要O(V^2)的时间。而邻接表做存储结构时,找邻接点所需的时间取决于顶点和边的数量,所以是O(V+E)。 显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。
对于有向图而言,由于它只是对通道存在可行或不可行,算法上没有变化,是完全可以通用的。
5.DFS 应用场景
- 连通性判断(如判断两个顶点是否可达);
- 拓扑排序(有向无环图 DAG);
- 迷宫求解、路径查找(如深度优先的路径探索)。
6.深度优先的生成树和生成森林
深度优先搜索会产生一棵深度优先生成树。 当然,这是有条件的,即对连通图调用DFS才能产生深度优先生成树,否则产生的将是深度优先生成森林,如下图所示。基于邻接表存储的深度优先生成树是不唯一的 。

二、广度优先搜索(Breadth First Search)
BFS 的核心思想是 "队列":从起点出发,先访问当前顶点的所有邻接顶点(一层),再依次访问这些邻接顶点的邻接顶点(下一层),类似 "水波扩散"。
1.广度搜索过程
**图的广度优先遍历就类似于树的层序遍历了。**广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有往回退的情况,因此它不是一个递归的算法。为了实现逐层的访问,算法必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点。
2.算法实现(非递归实现)
- 初始化一个
visited数组,标记顶点是否已访问; - 从起点
start出发,标记start为已访问,将其加入队列; - 队列不为空时,弹出队首顶点,遍历其所有邻接顶点,若未访问,则标记并加入队列。
3.代码实现(基于 GraphList 类扩展)
java
import java.util.LinkedList;
import java.util.Queue;
// BFS:非递归实现(队列)
public void bfs(int start) {
if (start < 0 || start >= vertexCount) {
throw new IllegalArgumentException("起点索引无效");
}
boolean[] visited = new boolean[vertexCount];
Queue<Integer> queue = new LinkedList<>();
// 起点入队并标记
queue.offer(start);
visited[start] = true;
while (!queue.isEmpty()) {
int current = queue.poll(); // 弹出队首顶点
System.out.print(current + " "); // 访问顶点
// 遍历当前顶点的所有邻接顶点
for (Edge edge : adjList.get(current)) {
int neighbor = edge.to;
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.offer(neighbor); // 未访问的邻接顶点入队
}
}
}
}
// 测试BFS:从顶点0开始遍历
public static void main(String[] args) {
GraphList graph = new GraphList(4);
graph.addEdge(0, 1, 1);
graph.addEdge(0, 2, 1);
graph.addEdge(1, 3, 1);
graph.addEdge(2, 3, 1);
System.out.print("BFS遍历结果(从0开始):");
graph.bfs(0); // 输出:0 1 2 3(或0 2 1 3,取决于邻接列表顺序)
}
4.BFS算法性能分析
无论是邻接表还是邻接矩阵的存储方式,BFS 算法都需要借助一个辅助队列Q, n个顶点均需入队一次,在最坏的情况下,空间复杂度为O(V)。
采用邻接表存储方式时,每个顶点均需搜索一次(或入队一次),在搜索任一顶点的邻接点时,每条边至少访问一次,算法总的时间复杂度为O (V+E)。采用邻接矩阵存储方式时,查找每个顶点的邻接点所需的时间为O(V),故算法总的时间复杂度为O(V^2)。
5.BFS 应用场景
- 最短路径查找(无向无权图中,BFS 的层数即为最短路径长度);
- 层次遍历(如按 "距离起点的远近" 访问顶点);
- 广播通信(如网络中的消息扩散)。
6.不同存储方式的特性
注意:图的邻接矩阵表示是唯一的,但对于邻接表来说,若边的输入次序不同,生成的邻接表也不同。因此,对于同样一个图,基于邻接矩阵的遍历所得到的DFS序列和BFS序列是唯一的,基于邻接表的遍历所得到的DFS序列和BFS序列是不唯一的。
三、DFS 与 BFS 对比
|----------|-------------------------|---------------------------|
| 对比维度 | DFS(深度优先) | BFS(广度优先) |
| 数据结构 | 递归栈(或手动栈) | 队列 |
| 访问顺序 | 深度优先,先深入再回溯 | 广度优先,先访问一层再下一层 |
| 空间复杂度 | O (h)(h 为图的深度,最坏 O (n)) | O (w)(w 为图的最大宽度,最坏 O (n)) |
| 最短路径 | 无向无权图中不保证找到最短路径 | 无向无权图中可找到最短路径 |
| 适用场景 | 连通性判断、拓扑排序、迷宫求解 | 最短路径、层次遍历、广播通信 |