图
图(Graph)是数据结构中一种非常强大且应用广泛的非线性结构。与线性表(一对一)和树(一对多)不同,图主要用于描述事物之间多对多的复杂关系。
定义
图 G 是由两个集合组成的二元组:G = (V, E)
- V (Vertex):顶点的有穷非空集合。顶点是图中的基本单元,也称为节点(Node)。
- E (Edge):边的集合,表示顶点之间的关系。边是连接两个顶点的线段,也可以称为弧(Arc)。
术语
-
顶点 (Vertex):图中的数据元素,是构成图的基本单位。
-
边 (Edge):连接两个顶点的线段,表示它们之间存在关系。
-
权 (Weight) :与边相关的数值,可以表示距离、耗费、时间等度量。带有权重的图被称为网 (Network)。
-
度 (Degree):
- 在无向图中,一个顶点的度是与它相连的边的数量。
- 在有向图 中,度分为入度 (指向该顶点的边的数量)和出度(从该顶点出发的边的数量)。
-
路径 (Path):从一个顶点到另一个顶点所经过的顶点序列。路径的长度是路径上边的数量(或带权图中所有边的权重之和)。
-
邻接点 (Adjacent Vertex):如果两个顶点由一条边直接相连,则称它们互为邻接点。
分类
按边的方向性划分
- 无向图 (Undirected Graph) :边没有方向。例如,
(A, B)和(B, A)代表同一条边。这可以用来表示微信好友关系(双向的)。 - 有向图 (Directed Graph) :边有方向,也称为弧。例如,
<A, B>表示从顶点 A 指向顶点 B 的一条弧,这与<B, A>是不同的。这可以用来表示微博的关注关系(单向的)。
按边的权重划分
- 无权图 (Unweighted Graph):边没有附带权重,只表示连接关系。
- 带权图 / 网 (Weighted Graph / Network):边附带有权重,表示某种具体的度量,如城市间的距离。
按连通性划分
- 连通图 (Connected Graph):在无向图中,如果任意两个顶点之间都存在路径,则该图是连通图。
- 强连通图 (Strongly Connected Graph):在有向图中,如果任意两个顶点之间都存在双向路径(即从 A 能到 B,从 B 也能到 A),则该图是强连通图。
存储结构
在计算机中,图主要有两种存储方式,它们各有优劣,适用于不同的场景。
| 存储方式 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 邻接矩阵 | 使用一个二维数组 matrix[i][j] 来表示顶点 i 和 j 之间是否存在边。 |
判断两点是否相邻非常快,时间复杂度为 O(1)。 | 空间复杂度高,为 O(V²),会浪费大量空间存储不存在的边。 | 稠密图(边很多的图) |
| 邻接表 | 为每个顶点建立一个链表,链表中存储所有与它相邻的顶点。 | 节省空间,空间复杂度为 O(V+E),遍历邻接点效率高。 | 判断两点是否相邻需要遍历链表,效率较低。 | 稀疏图(边很少的图) |
遍历
遍历是图算法的基础,目的是系统地访问图中的每一个顶点。
- 深度优先搜索 (DFS):类似于"一条路走到黑",尽可能深地搜索图的分支。通常用递归或栈实现。
- 广度优先搜索 (BFS):类似于"水波扩散",从起始点开始,逐层向外访问。通常用队列实现。
最小生成树 (MST)
在一个带权的连通图中,找到一个包含所有顶点的子图,它是一棵树,并且所有边的权重之和最小。
- Prim 算法 :从一个顶点开始,逐步添加边来构建树,适合稠密图。
- Kruskal 算法 :从权重最小的边开始,逐步构建树,适合稀疏图。
拓扑排序
针对有向无环图 (DAG) ,将所有顶点排成一个线性序列,使得图中任意一条有向边 <u, v>,u 都出现在 v 之前。常用于解决任务调度和依赖关系问题。
应用场景
- 社交网络:用户是顶点,好友关系是边。用于实现好友推荐、社区发现等功能。
- 地图导航:地点是顶点,道路是边,道路长度是权重。用于计算两点间的最短或最快路径。
- 任务调度:任务是顶点,任务间的依赖关系是边。用于确定任务的执行顺序,如软件编译、课程安排。
- 推荐系统:用户和商品是顶点,购买或浏览行为是边。用于分析用户偏好,进行商品或内容推荐。
- 知识图谱:实体是顶点,实体间的关系是边。用于构建结构化的知识库,支持智能搜索和问答。
使用示例
使用"邻接矩阵"实现图
查询两点是否相连非常快 O(1),但如果图很大且边很少,会浪费空间。
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
public class GraphMatrix {
// 顶点列表
private ArrayList<String> vertexList;
// 邻接矩阵(二维数组)
private int[][] edges;
// 边的数量
private int numOfEdges;
// 标记节点是否被访问过
private boolean[] isVisited;
public GraphMatrix(int n) {
edges = new int[n][n];
vertexList = new ArrayList<>(n);
numOfEdges = 0;
isVisited = new boolean[n];
}
// 添加顶点
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
// 添加边 (无向图:i->j 和 j->i 都要设为1)
public void insertEdge(int v1, int v2) {
edges[v1][v2] = 1;
edges[v2][v1] = 1;
numOfEdges++;
}
// --- 核心算法:深度优先搜索 (DFS) ---
// 重载方法:遍历所有节点(处理非连通图)
public void dfs() {
isVisited = new boolean[vertexList.size()]; // 重置访问标记
System.out.print("DFS 遍历结果: ");
// 循环检查每个节点,防止漏掉非连通的分量
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisited[i]) {
dfs(i);
}
}
System.out.println();
}
// 递归实现 DFS
private void dfs(int i) {
// 1. 访问当前节点
System.out.print(vertexList.get(i) + "->");
isVisited[i] = true;
// 2. 查找当前节点的第一个邻接节点
int w = getFirstNeighbor(i);
while (w != -1) {
// 如果邻接节点没被访问过,就递归访问它
if (!isVisited[w]) {
dfs(w);
}
// 如果访问过了,或者递归回来了,继续找下一个邻接节点
w = getNextNeighbor(i, w);
}
}
// --- 核心算法:广度优先搜索 (BFS) ---
public void bfs() {
isVisited = new boolean[vertexList.size()];
System.out.print("BFS 遍历结果: ");
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisited[i]) {
bfs(i);
}
}
System.out.println();
}
private void bfs(int i) {
int u; // 队列头节点
int w; // 邻接节点
LinkedList<Integer> queue = new LinkedList<>();
// 访问初始节点并入队
System.out.print(vertexList.get(i) + "->");
isVisited[i] = true;
queue.addLast(i);
while (!queue.isEmpty()) {
u = queue.removeFirst(); // 出队
w = getFirstNeighbor(u);
while (w != -1) {
if (!isVisited[w]) {
System.out.print(vertexList.get(w) + "->");
isVisited[w] = true;
queue.addLast(w); // 入队
}
w = getNextNeighbor(u, w);
}
}
}
// --- 辅助方法 ---
// 获取节点 i 的第一个邻接节点下标
private int getFirstNeighbor(int i) {
for (int j = 0; j < vertexList.size(); j++) {
if (edges[i][j] > 0) {
return j;
}
}
return -1;
}
// 获取节点 i 在节点 v 之后的下一个邻接节点下标
private int getNextNeighbor(int i, int v) {
for (int j = v + 1; j < vertexList.size(); j++) {
if (edges[i][j] > 0) {
return j;
}
}
return -1;
}
// --- 测试主函数 ---
public static void main(String[] args) {
String[] vertexs = {"A", "B", "C", "D", "E"};
GraphMatrix graph = new GraphMatrix(vertexs.length);
// 添加顶点
for (String vertex : vertexs) {
graph.insertVertex(vertex);
}
// 添加边 (构建一个五边形结构 A-B-C-D-E-A,外加 A-C)
graph.insertEdge(0, 1); // A-B
graph.insertEdge(1, 2); // B-C
graph.insertEdge(2, 3); // C-D
graph.insertEdge(3, 4); // D-E
graph.insertEdge(4, 0); // E-A
graph.insertEdge(0, 2); // A-C
graph.dfs();
graph.bfs();
}
}
使用"邻接表"实现图
节省空间,特别是对于像社交网络这样"点很多、但每个人好友有限"的稀疏图。
java
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
public class GraphList {
// 顶点数量
private int vertices;
// 邻接表:数组的每个元素是一个链表,存储相邻节点
private ArrayList<Integer>[] adjList;
@SuppressWarnings("unchecked")
public GraphList(int vertices) {
this.vertices = vertices;
adjList = new ArrayList[vertices];
// 初始化每个顶点的链表
for (int i = 0; i < vertices; i++) {
adjList[i] = new ArrayList<>();
}
}
// 添加边 (无向图:互相添加)
public void addEdge(int u, int v) {
adjList[u].add(v);
adjList[v].add(u);
}
// --- 深度优先搜索 (DFS) ---
public void dfs(int start) {
boolean[] visited = new boolean[vertices];
System.out.print("DFS 从节点 " + start + " 开始: ");
dfsUtil(start, visited);
System.out.println();
}
private void dfsUtil(int u, boolean[] visited) {
visited[u] = true;
System.out.print(u + " ");
// 遍历 u 的所有邻接点
for (int v : adjList[u]) {
if (!visited[v]) {
dfsUtil(v, visited);
}
}
}
// --- 广度优先搜索 (BFS) ---
public void bfs(int start) {
boolean[] visited = new boolean[vertices];
Queue<Integer> queue = new LinkedList<>();
visited[start] = true;
queue.add(start);
System.out.print("BFS 从节点 " + start + " 开始: ");
while (!queue.isEmpty()) {
int u = queue.poll();
System.out.print(u + " ");
// 遍历 u 的所有邻接点
for (int v : adjList[u]) {
if (!visited[v]) {
visited[v] = true;
queue.add(v);
}
}
}
System.out.println();
}
// --- 测试主函数 ---
public static void main(String[] args) {
GraphList graph = new GraphList(5);
// 构建图结构
graph.addEdge(0, 1);
graph.addEdge(0, 4);
graph.addEdge(1, 2);
graph.addEdge(1, 3);
graph.addEdge(1, 4);
graph.addEdge(2, 3);
graph.addEdge(3, 4);
graph.dfs(0);
graph.bfs(0);
}
}
最短路径问题
计算一个顶点到另一个顶点的最短路径。
Dijkstra 算法 :用于求解非负权图中,从一个源点到其他所有顶点的最短路径。
java
import java.util.Arrays;
public class DijkstraMatrix {
// 表示无穷大(不可达)
private static final int INF = Integer.MAX_VALUE;
// 顶点数量
private int vertices;
// 邻接矩阵
private int[][] matrix;
public DijkstraMatrix(int v) {
this.vertices = v;
matrix = new int[v][v];
// 初始化矩阵
for (int i = 0; i < v; i++) {
Arrays.fill(matrix[i], INF);
matrix[i][i] = 0; // 自己到自己距离为0
}
}
// 添加边 (无向图需添加双向,有向图只添加单向)
public void addEdge(int u, int v, int weight) {
matrix[u][v] = weight;
matrix[v][u] = weight; // 如果是有向图,去掉这行
}
// Dijkstra 核心算法
public void dijkstra(int start) {
// dist[i] 存储 start 到 i 的最短距离
int[] dist = new int[vertices];
// visited[i] 标记是否已找到最短路径
boolean[] visited = new boolean[vertices];
// 1. 初始化
Arrays.fill(dist, INF);
dist[start] = 0;
// 2. 循环 V-1 次(每次确定一个点的最短路径)
for (int i = 0; i < vertices - 1; i++) {
// 寻找当前未访问节点中距离最小的
int u = findMinDistance(dist, visited);
// 如果找不到(说明剩下的点都不可达),提前结束
if (u == -1) break;
// 标记该节点已访问
visited[u] = true;
// 3. 松弛操作:通过 u 更新其邻居的距离
for (int v = 0; v < vertices; v++) {
// 如果 u->v 有边,且 v 未被访问,且通过 u 过去更近
if (matrix[u][v] != INF && !visited[v] &&
dist[u] != INF && dist[u] + matrix[u][v] < dist[v]) {
dist[v] = dist[u] + matrix[u][v];
}
}
}
// 打印结果
printSolution(dist, start);
}
// 辅助方法:寻找距离最小的未访问节点
private int findMinDistance(int[] dist, boolean[] visited) {
int min = INF;
int index = -1;
for (int i = 0; i < vertices; i++) {
if (!visited[i] && dist[i] <= min) {
min = dist[i];
index = i;
}
}
return index;
}
private void printSolution(int[] dist, int src) {
System.out.println("从起点 " + src + " 出发的最短路径:");
for (int i = 0; i < vertices; i++) {
System.out.println("到节点 " + i + " 的距离: " +
(dist[i] == INF ? "不可达" : dist[i]));
}
}
// 测试
public static void main(String[] args) {
DijkstraMatrix graph = new DijkstraMatrix(5);
// 构建图
graph.addEdge(0, 1, 4);
graph.addEdge(0, 2, 1);
graph.addEdge(1, 2, 2);
graph.addEdge(1, 3, 5);
graph.addEdge(2, 3, 8);
graph.addEdge(2, 4, 2);
graph.addEdge(3, 4, 1);
graph.dijkstra(0);
}
}
Floyd-Warshall 算法 :用于求解图中任意两个顶点之间的最短路径。
java
import java.util.*;
public class DijkstraOptimized {
// 边类
static class Edge {
int to; // 目标节点
int weight; // 权重
public Edge(int to, int weight) {
this.to = to;
this.weight = weight;
}
}
// 图类
private int vertices;
private List<List<Edge>> adjList;
public DijkstraOptimized(int vertices) {
this.vertices = vertices;
adjList = new ArrayList<>();
for (int i = 0; i < vertices; i++) {
adjList.add(new ArrayList<>());
}
}
// 添加边
public void addEdge(int u, int v, int weight) {
adjList.get(u).add(new Edge(v, weight));
adjList.get(v).add(new Edge(u, weight)); // 无向图
}
// 核心算法
public void dijkstra(int start) {
// 距离数组
int[] dist = new int[vertices];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;
// 优先队列:按照距离从小到大排序
// 数组格式:[节点ID, 当前距离]
PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
pq.offer(new int[]{start, 0});
// 记录是否已确定最短路径
boolean[] visited = new boolean[vertices];
while (!pq.isEmpty()) {
// 取出距离最小的节点
int[] current = pq.poll();
int u = current[0];
// 如果已经处理过,跳过(懒删除策略)
if (visited[u]) continue;
visited[u] = true;
// 遍历邻居
for (Edge edge : adjList.get(u)) {
int v = edge.to;
int weight = edge.weight;
// 松弛操作:如果通过 u 到 v 更近,则更新
if (!visited[v] && dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
pq.offer(new int[]{v, dist[v]});
}
}
}
// 打印结果
System.out.println("优化版 Dijkstra 结果 (起点: " + start + "):");
for (int i = 0; i < vertices; i++) {
System.out.println("到节点 " + i + " 的距离: " +
(dist[i] == Integer.MAX_VALUE ? "不可达" : dist[i]));
}
}
// 测试
public static void main(String[] args) {
DijkstraOptimized graph = new DijkstraOptimized(5);
graph.addEdge(0, 1, 4);
graph.addEdge(0, 2, 1);
graph.addEdge(1, 2, 2);
graph.addEdge(1, 3, 5);
graph.addEdge(2, 3, 8);
graph.addEdge(2, 4, 2);
graph.addEdge(3, 4, 1);
graph.dijkstra(0);
}
}