无向图
无向图
图模型
四种重要的图模型:
- 无向图
- 有向图
- 加权图
- 加权有向图
无向图:边仅仅是两个顶点之间的连接。
术语表
特殊的情况:
- 自环
- 平行边
术语表:
- 相邻
- 度数
- 子图
- 路径:由边顺序连接的一系列顶点
- 路径的长度:其中所包含的边数
- 简单路径:一条没有重复顶点的路径
- 连通
- 连通图
- 极大连通子图
- 生成树
- 稀疏图/稠密图/二分图
表示无向图的数据模型
无向图 API:
public class Graph { Graph(int V); // 创建一个含有 V 个顶点但不含有边的图 Graph(In in); // 从标准输入流 in 读取一幅图 int V(); // 顶点数 int E(); // 边数 void addEdge(int v, int w); // 添加一个边 v-w Iterable<Integer> adj(int v); // 和 v 相邻的所有顶点 String toSting(); // 图的字符串表示 }
本节将学习的所有算法都基于
adj()
方法所抽象的基本操作
实现图 API 的三种数据结构:
- 邻接矩阵:用 V 乘 V 的布尔矩阵表示,顶点存在连接时值为 true,否则为 false(空间消耗大)
- 边的数组:使用一个 Edge 对象表示边(实现
adj()
需要检查图中所有边) - 邻接表数组:使用一个以顶点为索引的列表数组,列表保存该顶点的相邻顶点(本章选择的数据结构)
邻接表数组数据结构:
V 相邻顶点(邻接表) 0 -> 1, 2 1 -> 0 2 -> 0 3 -> 4 4 -> 3
// 无向图 public class Graph { private int V; // 顶点数 private int E; // 边数 private Bag<Integer>[] adj; // 邻接表数组 // 创建一个包含 V 个顶点,但不含有边的图 public Graph(int V) { this.V = V; this.E = 0; this.adj = new Bag[V]; for (int i = 0; i < V; i++) { adj[i] = new Bag<>(); } } // 从标准输入流读取一幅图 public Graph(In in) { this(in.readInt()); int E = in.readInt(); // 局部变量 for (int i = 0; i < V; i++) { int v = in.readInt(); int w = in.readInt(); addEdge(v, w); } } // 添加一条边 v-w public void addEdge(int v, int w) { adj[v].add(w); adj[w].add(v); E++; } // 顶点 v 的所有相邻顶点 public Iterable<Integer> adj(int v) { return adj[v]; } public int V() { return V; } public int E() { return E; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(String.format("%d vertices, %d edges%n", V, E)); for (int v = 0; v < V; v++) { builder.append(v + ": "); for (int w : adj(v)) { builder.append(w +" "); } builder.append(System.lineSeparator()); } return builder.toString(); } }
图处理算法设计模式
为每个任务(算法)创建一个相应的类,用例可以创建相应的对象来完成任务。
用例:
- 创建一幅图
- 将图传递给实现某个算法的类
- 调用实现算法的类对象完成任务
图搜索算法
搜索迷宫:检查入口和出口是否连通
图搜索算法 API:
public class Search { Search(Graph G, int s); // 找到和起点 s 连通的所有顶点 boolean marked(int v); // v 和 s 是否连通 int count(); // 与 s 连通的顶点总数 }
深度优先搜索:
// 深度优先搜索 // 在访问一个顶点时: // * 将它标记为已访问 // * 递归地访问它的所有没有被标记过的邻居顶点 public class DepthFirstSearch implements Search { private boolean[] marked; private int count; // s:起点 public DepthFirstSearch(Graph G, int s) { marked = new boolean[G.V()]; count = 0; dfs(G, s); } private void dfs(Graph G, int v) { marked[v] = true; count++; for (int w : G.adj(v)) { if (!marked[w]) { dfs(G, w); } } } @Override public boolean marked(int v) { return marked[v]; } @Override public int count() { return count; } }
单点路径
单点路径 API:
public class Paths { Paths(Graph G, int s); // 在 G 中找出所有起点为 s 的路径 boolean hasPathTo(int v); // 是否存在从 s 到 v 的路径 Iterable<Integer> pathTo(int v); // s 到 v 的路径,如果不存在则返回 null }
深度优先搜索:
// 使用深度优先搜索解决单点路径问题 public class DepthFirstPaths implements Paths { private boolean[] marked; private int[] pathTo; private int s; // s: 起点 public DepthFirstPaths(Graph G, int s) { marked = new boolean[G.V()]; pathTo = new int[G.V()]; this.s = s; dfs(G, s); } private void dfs(Graph G, int v) { marked[v] = true; for (int w : G.adj(v)) { if (!marked[w]) { pathTo[w] = v; dfs(G, w); } } } // 是否存在 s 到 v 的路径 @Override public boolean hasPathTo(int v) { return marked[v]; } // 返回 s 到 v 的路径,如果不存在返回 null @Override public Iterable<Integer> pathTo(int v) { if (!hasPathTo(v)) { return null; } Stack<Integer> stack = new Stack<>(); while (v != s) { stack.push(v); v = pathTo[v]; } stack.push(s); return stack; } }
广度优先搜索(单点最短路径):
public class BreadthFirstPaths implements Paths { private boolean[] marked; private int[] pathTo; private int s; public BreadthFirstPaths(Graph G, int s) { marked = new boolean[G.V()]; pathTo = new int[G.V()]; this.s = s; bfs(G, s); } private void bfs(Graph G, int s) { Queue<Integer> queue = new Queue<>(); queue.add(s); marked[s] = true; while (!queue.isEmpty()) { int v = queue.remove(); for (int w : G.adj(v)) { if (!marked[w]) { pathTo[w] = v; queue.add(w); marked[w] = true; } } } } @Override public boolean hasPathTo(int v) { return marked[v]; } @Override public Iterable<Integer> pathTo(int v) { if (!hasPathTo(v)) { return null; } Stack<Integer> stack = new Stack<>(); while (v != s) { stack.push(v); v = pathTo[v]; } stack.push(s); return stack; } }
连通分量
很多用例都需要能够独立地处理每个连通分量。
连通分量 API:
public class CC { CC(Graph G); // 预处理构造函数 boolean connected(int v, int w); // v 和 w 连通吗 int count(); // 连通分量数量 int id(int v); // v 所在的连通分量的标识符 (0 - count() - 1) }
深度优先搜索:
public class CC { private boolean[] marked; private int[] ids; private int count; public CC(Graph G) { marked = new boolean[G.V()]; ids = new int[G.V()]; count = 0; for (int v = 0; v < G.V(); v++) { if (!marked[v]) { dfs(G, v, count); count++; } } } private void dfs(Graph G, int v, int id) { marked[v] = true; ids[v] = id; for (int w : G.adj(v)) { if (!marked[w]) { dfs(G, w, id); } } } public boolean connected(int v, int w) { return ids[v] == ids[w]; } public int count() { return count; } public int id(int v) { return ids[v]; } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话