Java的图数据结构探索-常用的算法快速入门
1. 概述
在本教程中,我们将了解图作为数据结构的基本概念。
我们还将探讨它在 Java 中的实现以及在图上可能进行的各种操作。我们还将讨论提供图实现的 Java 库。
延伸阅读:
Java 中的 Dijkstra 最短路径算法
JGraphT简介
2. 图数据结构
图是一种数据结构,用于存储连接的数据,例如社交媒体平台上的人员网络。
图由顶点和边组成。顶点表示实体(例如,人),边表示实体之间的关系(例如,一个人的友谊)。
让我们定义一个简单的 Graph 来更好地理解这一点:

这里我们定义了一个简单的图,有5个顶点和6条边。圆圈是代表人的顶点,连接两个顶点的线是代表在线交友软件上的朋友的边。
根据边缘的属性,这个简单的图形有一些变化。让我们在下一节中简要介绍一下它们。
但是,在本教程中,我们只关注这里给出的Java示例的简单图。
2.1. 有向图
到目前为止,我们定义的图具有没有任何方向的边缘。如果这些边具有方向,则生成的图称为有向图。
一个例子可以表示谁在在线交友软件上发送了朋友请求::
在这里我们可以看到边缘具有固定的方向。边缘也可以是双向的。
2.2. 加权图
同样,我们的简单图具有无偏向或无加权的边。
相反,如果这些边具有相对权重,则此图称为加权图。
这一实际应用的一个例子可以代表在线交友软件朋友间上一段友谊的时长的比重:

在这里,我们可以看到边缘具有与之关联的权重。这为这些边缘提供了相对含义。
3.图表示
图可以用不同的形式表示,例如邻接矩阵和邻接列表。每个在不同的设置中都有其优点和缺点。
我们将在本节中介绍这些图表示形式。
3.1. 邻接矩阵
邻接矩阵是维度等于图中顶点数的方阵。
矩阵的元素通常具有值 0 或 1。值 1 表示行和列中顶点之间的邻接关系,否则值为 0。
让我们看看上一节中简单图的邻接矩阵是什么样子的:

这种表示形式相当容易实现,查询起来也很有效。但是,就占用的空间而言,它的效率较低。
3.2. 邻接列表
邻接列表只不过是列表数组。数组的大小相当于图中的顶点数。
数组特定索引处的列表表示该数组索引所表示的顶点的相邻顶点。
让我们看看上一节中简单图的邻接列表是什么样子的:
此表示形式相对难以创建,查询效率较低。但是,它提供了更好的空间效率。
在本教程中,我们将使用邻接列表来表示图。
4. Java中的图
Java 没有图数据结构的默认实现。
但是,我们可以使用 Java 集合来实现图。
让我们从定义一个顶点开始:
class Vertex {
String label;
Vertex(String label) {
this.label = label;
}
// equals and hashCode
}
顶点的上述定义仅具有标签,但可以表示任何可能的实体,例如人或城市。
另外,请注意,我们必须覆盖equals() 和hashCode() 方法,因为它们是使用 Java 集合所必需的。
正如我们前面所讨论的,图只不过是顶点和边的集合,可以表示为邻接矩阵或邻接列表。
让我们看看如何使用邻接列表来定义它:
class Graph {
private Map<Vertex, List<Vertex>> adjVertices;
// standard constructor, getters, setters
}
正如我们所看到的,类 Graph正在使用 Java 集合中的Map来定义邻接列表。
可以对图数据结构进行多种操作,例如创建、更新或搜索图。
我们将介绍一些更常见的操作,并了解如何在 Java 中实现它们。
5. 图操作
首先,我们将定义一些方法来改变图数据结构。
让我们定义添加和删除顶点的方法:
void add(String label) {
adjVertices.putIfAbsent(new Vertex(label), new ArrayList<>());
}
void removeVertex(String label) {
Vertex v = new Vertex(label);
adjVertices.values().stream().forEach(e -> e.remove(v));
adjVertices.remove(new Vertex(label));
}
这些方法只是在顶点集中添加和删除元素。
现在,让我们也定义一个添加边缘的方法:
void addEdge(String label1, String label2) {
Vertex v1 = new Vertex(label1);
Vertex v2 = new Vertex(label2);
adjVertices.get(v1).add(v2);
adjVertices.get(v2).add(v1);
}
此方法将创建新的边并更新相邻的顶点映射。
以类似的方式,我们将定义removeEdge() 方法:
void removeEdge(String label1, String label2) {
Vertex v1 = new Vertex(label1);
Vertex v2 = new Vertex(label2);
List<Vertex> eV1 = adjVertices.get(v1);
List<Vertex> eV2 = adjVertices.get(v2);
if (eV1 != null)
eV1.remove(v2);
if (eV2 != null)
eV2.remove(v1);
}
接下来,让我们看看如何使用我们目前定义的方法创建我们之前绘制的简单图:
Graph createGraph() {
Graph graph = new Graph();
graph.addVertex("Bob");
graph.addVertex("Alice");
graph.addVertex("Mark");
graph.addVertex("Rob");
graph.addVertex("Maria");
graph.addEdge("Bob", "Alice");
graph.addEdge("Bob", "Rob");
graph.addEdge("Alice", "Mark");
graph.addEdge("Rob", "Mark");
graph.addEdge("Alice", "Maria");
graph.addEdge("Rob", "Maria");
return graph;
}
最后,我们将定义一个方法来获取特定顶点的相邻顶点:
List<Vertex> getAdjVertices(String label) {
return adjVertices.get(new Vertex(label));
}
6. 遍历图
现在我们已经定义了图数据结构和函数来创建和更新它,我们可以定义一些额外的函数来遍历图。
我们需要遍历图来执行任何有意义的操作,例如在图内搜索。
有两种遍历图的可能方法:深度优先遍历和广度优先遍历。
6.1. 深度优先遍历
深度优先遍历从任意根顶点开始,沿每个分支尽可能深入地探索折点,然后再在同一级别上探索折点。
让我们定义一个执行深度优先遍历的方法:
Set<String> depthFirstTraversal(Graph graph, String root) {
Set<String> visited = new LinkedHashSet<String>();
Stack<String> stack = new Stack<String>();
stack.push(root);
while (!stack.isEmpty()) {
String vertex = stack.pop();
if (!visited.contains(vertex)) {
visited.add(vertex);
for (Vertex v : graph.getAdjVertices(vertex)) {
stack.push(v.label);
}
}
}
return visited;
}
在这里,我们使用堆栈来存储需要遍历的顶点。
让我们在上一小节中创建的图上运行它:
assertEquals("[Bob, Rob, Maria, Alice, Mark]", depthFirstTraversal(graph, "Bob").toString());
请注意,我们在这里使用顶点“Bob”作为遍历的根,但这可以是任何其他顶点。
6.2. 广度优先遍历
相比之下,广度优先遍历从任意根顶点开始,并在同一级别探索所有相邻顶点,然后再深入图。
现在,让我们定义一个执行广度优先遍历的方法:
Set<String> breadthFirstTraversal(Graph graph, String root) {
Set<String> visited = new LinkedHashSet<String>();
Queue<String> queue = new LinkedList<String>();
queue.add(root);
visited.add(root);
while (!queue.isEmpty()) {
String vertex = queue.poll();
for (Vertex v : graph.getAdjVertices(vertex)) {
if (!visited.contains(v.label)) {
visited.add(v.label);
queue.add(v.label);
}
}
}
return visited;
}
请注意,广度优先遍历使用Queue来存储需要遍历的顶点。
让我们再次在同一图上运行此遍历:
assertEquals(
"[Bob, Alice, Rob, Mark, Maria]", breadthFirstTraversal(graph, "Bob").toString());
同样,根顶点(此处为“Bob”)也可以是任何其他顶点。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!