图
图
图
是一种非线性数据结构,由
顶点
和
边
组成。
相较于线性关系的链表和分治关系的树,网络关系的图自由度更高
常见类型与术语
根据边是否具有方向,可分为
无向图:

有向图:

根据所有顶点是否连通,可分为
连通图:

非连通图:

根据是否
为边添加“权重”变量,可分为
无权图:

有权图:

常用术语:
邻接:当两顶点之间存在边相连时,称这两顶点“邻接”。
路径:从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的“路径”。
度
:一个顶点拥有的边数。对于有向图,
入度
表示有多少条边指向该顶点,
出度
表示有多少条边从该顶点指出。
图的表示
邻接矩阵
这个图的邻接矩阵为:
邻接表
这个图的邻接表为:
基础操作
基于邻接矩阵的实现
import java.util.ArrayList;
import java.util.List;
/* 基于邻接矩阵实现的无向图类 */
class GraphAdjMat {
private List<Integer> vertices; // 顶点列表,元素代表“顶点值”,索引代表“顶点索引”
private int[][] adjMat; // 邻接矩阵,行列索引对应“顶点索引”
/* 构造方法 */
public GraphAdjMat(int[] vertices, int[][] edges) {
this.vertices = new ArrayList<>();
this.adjMat = new int[vertices.length][vertices.length]; // 初始化邻接矩阵
// 添加顶点
for (int val : vertices) {
addVertex(val);
}
// 添加边
for (int[] e : edges) {
addEdge(e[0], e[1]);
}
}
/* 获取顶点数量 */
public int size() {
return vertices.size();
}
/* 添加顶点 */
public void addVertex(int val) {
vertices.add(val);
// 扩展邻接矩阵
int n = size();
int[][] newAdjMat = new int[n][n];
// 复制原有邻接矩阵的内容
for (int i = 0; i < n - 1; i++) {
System.arraycopy(adjMat[i], 0, newAdjMat[i], 0, n - 1);
}
// 设置新顶点的邻接关系为0
for (int i = 0; i < n; i++) {
newAdjMat[i][n - 1] = 0; // 新列
newAdjMat[n - 1][i] = 0; // 新行
}
adjMat = newAdjMat; // 更新邻接矩阵引用
}
/* 删除顶点 */
public void removeVertex(int index) {
if (index >= size()) {
throw new IndexOutOfBoundsException();
}
// 在顶点列表中移除索引 index 的顶点
vertices.remove(index);
int n = size();
int[][] newAdjMat = new int[n - 1][n - 1];
for (int i = 0, newRow = 0; i < n; i++) {
if (i != index) {
for (int j = 0, newCol = 0; j < n; j++) {
if (j != index) {
newAdjMat[newRow][newCol++] = adjMat[i][j]; // 复制不包含被删除顶点的行列
}
}
newRow++;
}
}
adjMat = newAdjMat; // 更新邻接矩阵引用
}
/* 添加边 */
// 参数 i, j 对应 vertices 元素索引
public void addEdge(int i, int j) {
// 索引越界与相等处理
if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {
throw new IndexOutOfBoundsException();
}
// 在无向图中,邻接矩阵关于主对角线对称
adjMat[i][j] = 1;
adjMat[j][i] = 1;
}
/* 删除边 */
// 参数 i, j 对应 vertices 元素索引
public void removeEdge(int i, int j) {
// 索引越界与相等处理
if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {
throw new IndexOutOfBoundsException();
}
adjMat[i][j] = 0;
adjMat[j][i] = 0;
}
/* 打印邻接矩阵 */
public void print() {
System.out.print("顶点列表 = ");
System.out.println(vertices);
System.out.println("邻接矩阵 =");
for (int[] row : adjMat) {
for (int val : row) {
System.out.print(val + " ");
}
System.out.println();
}
}
}
主要功能
- 添加顶点:增加新的顶点并扩展邻接矩阵。
- 删除顶点:移除指定索引的顶点和对应的邻接关系。
- 添加边:在邻接矩阵中设置两个顶点之间的边。
- 删除边:移除两个顶点之间的边。
- 打印邻接矩阵:以易于阅读的格式输出邻接矩阵。
基于邻接表的实现
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/* 顶点类 */
class Vertex {
int val; // 顶点值
public Vertex(int val) {
this.val = val;
}
// 重写 equals 和 hashCode,方便在 Map 中使用。
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Vertex)) return false;
Vertex other = (Vertex) obj;
return this.val == other.val;
}
@Override
public int hashCode() {
return Integer.hashCode(val);
}
}
/* 基于邻接表实现的无向图类 */
class GraphAdjList {
// 邻接表,key:顶点,value:该顶点的所有邻接顶点
Map<Vertex, List<Vertex>> adjList;
/* 构造方法 */
public GraphAdjList(Vertex[][] edges) {
this.adjList = new HashMap<>();
// 添加所有顶点和边
for (Vertex[] edge : edges) {
addVertex(edge[0]);
addVertex(edge[1]);
addEdge(edge[0], edge[1]);
}
}
/* 获取顶点数量 */
public int size() {
return adjList.size();
}
/* 添加边 */
public void addEdge(Vertex vet1, Vertex vet2) {
if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1.equals(vet2)) {
throw new IllegalArgumentException();
}
// 添加边 vet1 - vet2
adjList.get(vet1).add(vet2);
adjList.get(vet2).add(vet1);
}
/* 删除边 */
public void removeEdge(Vertex vet1, Vertex vet2) {
if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1.equals(vet2)) {
throw new IllegalArgumentException();
}
// 删除边 vet1 - vet2
adjList.get(vet1).remove(vet2);
adjList.get(vet2).remove(vet1);
}
/* 添加顶点 */
public void addVertex(Vertex vet) {
if (!adjList.containsKey(vet)) {
// 在邻接表中添加一个新链表
adjList.put(vet, new ArrayList<>());
}
}
/* 删除顶点 */
public void removeVertex(Vertex vet) {
if (!adjList.containsKey(vet)) {
throw new IllegalArgumentException();
}
// 在邻接表中删除顶点 vet 对应的链表
adjList.remove(vet);
// 遍历其他顶点的链表,删除所有包含 vet 的边
for (List<Vertex> list : adjList.values()) {
list.remove(vet);
}
}
/* 打印邻接表 */
public void print() {
System.out.println("邻接表 =");
for (Map.Entry<Vertex, List<Vertex>> pair : adjList.entrySet()) {
List<Integer> tmp = new ArrayList<>();
for (Vertex vertex : pair.getValue()) {
tmp.add(vertex.val);
}
System.out.println(pair.getKey().val + ": " + tmp + ",");
}
}
}
// 示例使用
class Main {
public static void main(String[] args) {
Vertex v1 = new Vertex(1);
Vertex v2 = new Vertex(2);
Vertex v3 = new Vertex(3);
Vertex[][] edges = {{v1, v2}, {v2, v3}, {v1, v3}};
GraphAdjList graph = new GraphAdjList(edges);
graph.print();
}
}
这个类实现了基于邻接表的无向图,包括了添加、删除顶点和边的功能,并且重写了 Vertex
类的 equals
和 hashCode
方法,以便在 HashMap
中正确使用。打印功能将输出每个顶点及其邻接顶点列表。
遍历
广度优先
广度优先遍历是一种由近及远的遍历方式,从某个节点出发,始终优先访问距离最近的顶点,并一层层向外
扩张
。
/* 广度优先遍历 */
List<Integer> graphBFS(GraphAdjMat graph, int startIdx) {
// 顶点遍历序列
List<Integer> res = new ArrayList<>();
// 哈希集,用于记录已被访问过的顶点
Set<Integer> visited = new HashSet<>();
visited.add(startIdx); // 将起始顶点标记为已访问
// 队列用于实现 BFS
Queue<Integer> que = new LinkedList<>();
que.offer(startIdx); // 将起始顶点入队
// 以顶点 startIdx 为起点,循环直至访问完所有顶点
while (!que.isEmpty()) {
int idx = que.poll(); // 队首顶点出队
res.add(graph.vertices.get(idx)); // 记录访问顶点
// 遍历该顶点的所有邻接顶点
for (int j = 0; j < graph.size(); j++) {
if (graph.adjMat[idx][j] == 1 && !visited.contains(j)) { // 判断是否相邻且未访问
que.offer(j); // 只入队未访问的顶点
visited.add(j); // 标记该顶点已被访问
}
}
}
// 返回顶点遍历序列
return res;
}
主要功能
- 输入参数:接收一个
GraphAdjMat
对象和起始顶点的索引。 - 结果列表:使用一个列表
res
来记录访问的顶点。 - 访问记录:使用一个哈希集
visited
来记录已经访问过的顶点,避免重复访问。 - 队列实现BFS:使用
Queue
来按层次遍历图的顶点。
注意事项
- 确保在构造图时,已知起始顶点的索引。
- 广度优先遍历的结果将返回按照层次顺序访问的顶点,适合用于查找最短路径或层级关系等应用场合。
深度优先
深度优先遍历是一种优先走到底、无路可走再回头的遍历方式
。
/* 深度优先遍历辅助函数 */
void dfs(GraphAdjMat graph, Set<Integer> visited, List<Integer> res, int index) {
res.add(graph.vertices.get(index)); // 记录访问顶点
visited.add(index); // 标记该顶点已被访问
// 遍历该顶点的所有邻接顶点
for (int j = 0; j < graph.size(); j++) {
if (graph.adjMat[index][j] == 1 && !visited.contains(j)) { // 判断是否相邻且未访问
dfs(graph, visited, res, j); // 递归访问邻接顶点
}
}
}
/* 深度优先遍历 */
// 使用邻接矩阵表示图,以便获取指定顶点的所有邻接顶点
List<Integer> graphDFS(GraphAdjMat graph, int startIdx) {
// 顶点遍历序列
List<Integer> res = new ArrayList<>();
// 哈希表,用于记录已被访问过的顶点
Set<Integer> visited = new HashSet<>();
// 调用辅助函数进行深度优先遍历
dfs(graph, visited, res, startIdx);
return res;
}
主要功能
-
辅助函数
dfs
:- 该函数负责递归访问图中的顶点。
- 记录当前顶点并将其标记为已访问。
- 遍历所有邻接的顶点,如果相邻顶点未被访问,则递归调用
dfs
。
-
主函数
graphDFS
:- 接收一个
GraphAdjMat
对象和起始顶点的索引。 - 初始化结果列表
res
和已访问顶点集合visited
。 - 调用
dfs
辅助函数开始深度优先遍历并返回最终的访问顺序。
- 接收一个
注意事项
- 确保在构造图时已知起始顶点的索引。
- 深度优先遍历适合用于搜索路径、分析连通性等场景。
文章记录了学习Krahets的《Hello 算法》的轨迹,代码均使用Java语言,原书支持 Python、C++、Java、C#、Go、Swift、JavaScript、TypeScript、Dart、 Rust、C 和 Zig 等语言。