news 2026/10/31 5:44:45

图算法实战:Java/Python手敲6大经典图算法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
图算法实战:Java/Python手敲6大经典图算法

1. 这不是教科书里的图,是能跑起来的图——从代码里长出来的数据结构

“数据结构-图(Graph)案例代码Java/Python均有实现”——看到这个标题,我第一反应不是去翻《算法导论》第22章,而是打开IDEA和PyCharm,新建两个空项目,把邻接表、DFS递归栈、Dijkstra优先队列这些词敲进文件名里。为什么?因为过去五年里,我带过的二十多个实际项目——从某高校课程设计平台的拓扑排序排课模块,到某物流调度系统中路径优化子模块,再到某社交关系分析Demo的连通分量识别——没有一个靠背定义能跑通。图不是画在黑板上的圆圈加箭头,它是内存里跳动的引用、是递归调用栈里层层压入的顶点ID、是PriorityQueue里不断弹出又插入的<节点, 距离>对。

你手头可能正卡在:

  • Java写完邻接表,addEdge()方法一调就NullPointerException,但debug半天发现是Node类没重写hashCode(),导致HashMap里存丢了;
  • Python用defaultdict(list)建图很顺,可一跑Kruskal就报TypeError: unhashable type: 'list',其实只是你把边元组写成了[0,1,5]而不是(0,1,5);
  • 看懂了BFS伪代码,但自己写队列时总漏掉visited数组的同步更新,结果死循环卡住,CPU飙到90%;
  • Dijkstra输出的最短路径长度是对的,可路径本身怎么也还原不出来——因为你没在松弛操作时同步记录parent数组。

这篇内容就是为解决这些“能看懂、写不出、跑不通”的真实卡点而写的。它不讲图论公理,不列复杂度证明,只聚焦一件事:让图的每一种经典算法,在Java和Python里都真正活过来。我会逐行拆解关键代码段,告诉你为什么这里必须用LinkedList而不是ArrayList做BFS队列,为什么Python的heapq要配合索引映射才能避免过期条目干扰,为什么Kruskal的并查集Union操作里,路径压缩比按秩合并更值得优先实现。无论你是刚学完链表的计算机专业大二学生,还是需要快速补上图算法能力的转行开发者,只要你会写for循环、能看懂if判断,就能跟着把这六种核心图算法——无向图连通性检测、有向图拓扑排序、单源最短路径(Dijkstra)、全源最短路径(Floyd-Warshall)、最小生成树(Prim/Kruskal)、强连通分量(Kosaraju)——全部亲手敲出来、调试通、测明白。下面所有代码,都经过实测:Java用OpenJDK 17,Python用3.11,输入输出格式严格对齐LeetCode和AcWing常见题型,连测试用例的构造逻辑都给你写清楚。

2. 图的底层实现:为什么邻接表是默认选择,以及什么时候该换邻接矩阵

2.1 邻接表 vs 邻接矩阵:不是选“高级”,而是选“不拖后腿”

图的存储方式只有两种主流选择:邻接表(Adjacency List)和邻接矩阵(Adjacency Matrix)。很多初学者一上来就纠结“哪个更好”,其实这个问题本身就有陷阱——不存在绝对更好的存储结构,只有更适合当前场景的结构。我见过太多人硬着头皮用邻接矩阵去存一个稀疏社交网络(10万用户,平均每人好友不到10个),结果光初始化那个10万×10万的boolean[][]数组,JVM直接OOM;也见过有人用邻接表去处理一个固定规模的棋盘状态转移(8×8=64个节点,每个节点最多8个邻居),结果遍历邻居时缓存不友好,性能反而不如矩阵的O(1)随机访问。

我们先看数据特征。假设图有V个顶点、E条边:

  • 邻接表空间复杂度是O(V + E):每个顶点一个头结点,所有边被存一次;
  • 邻接矩阵空间复杂度是O(V²):不管有没有边,整个V×V的二维数组都得占着。

计算一下临界点:当E < V²/2时,邻接表开始显现出空间优势。对于V=1000的图,E只要小于50万,邻接表就赢;而现实中,除了交通路网、电路板布线这类稠密图,绝大多数场景(社交关系、网页链接、依赖管理)的E远小于V²。比如某课程设计系统有200门课,依赖关系最多也就几百条边,用邻接矩阵就得开200×200=4万个布尔位,其中95%以上是false——纯属浪费。

但空间不是唯一维度。再看操作效率:

操作邻接表邻接矩阵实际影响
获取顶点v的所有邻居O(degree(v))O(V)BFS/DFS遍历时,邻接表快得多;矩阵得扫完整行
判断边(v,u)是否存在O(degree(v))O(1)频繁查边存在性时(如Kruskal找最小边),矩阵有优势
添加/删除边O(1)均摊O(1)差距不大,但邻接表增删更自然
添加/删除顶点O(1)O(V²)动态图场景(如实时添加用户)邻接表碾压

所以结论很明确:除非你的图非常小(V<100)且极度稠密,或者算法核心极度依赖O(1)边查询(如某些图匹配算法),否则邻接表是默认且安全的选择。这也是所有工业级图处理库(如Neo4j内部、NetworkX默认)的底层基石。

2.2 Java实现:用HashMap+LinkedList构建动态邻接表

Java里实现邻接表,核心是解决两个问题:一是顶点如何标识,二是边如何存储。我推荐用Map<Integer, List<Edge>> graph,其中:

  • Key是顶点编号(int),简单直接,避免Integer装箱开销;
  • Value是List<Edge>,Edge是一个内部静态类,包含目标顶点和权重。

为什么不用Map<Integer, Set<Integer>>存无权图?因为Set无法携带权重信息,后续扩展成本高;为什么不用ArrayList而用LinkedList?因为图的边增删在算法中并不频繁,但BFS/DFS遍历时需要频繁从头部取邻居(list.get(0)),LinkedList的get(0)是O(1),而ArrayList是O(1)没错,但LinkedList在迭代时内存局部性稍差,不过对于图算法这种以逻辑复杂度为主导的场景,这点差异可忽略,且LinkedList的addLast()和removeFirst()语义更清晰。

下面是精简但完整的Java邻接表骨架:

import java.util.*; public class Graph { // 邻接表:顶点ID -> 边列表 private final Map<Integer, List<Edge>> adjList; // 记录顶点总数,用于初始化等 private int vertexCount; public Graph(int n) { this.adjList = new HashMap<>(); this.vertexCount = n; // 预初始化所有顶点,避免addEdge时null检查 for (int i = 0; i < n; i++) { adjList.put(i, new LinkedList<>()); } } // 添加有向边 v->u,权重w public void addEdge(int v, int u, int w) { if (!adjList.containsKey(v)) { adjList.put(v, new LinkedList<>()); } adjList.get(v).add(new Edge(u, w)); } // 获取顶点v的所有邻居(含权重) public List<Edge> getNeighbors(int v) { return adjList.getOrDefault(v, Collections.emptyList()); } // 内部静态类:边 public static class Edge { public final int to; // 目标顶点 public final int weight; // 权重 public Edge(int to, int weight) { this.to = to; this.weight = weight; } @Override public String toString() { return "(" + to + "," + weight + ")"; } } }

关键细节说明:

  • adjList声明为final,保证图结构创建后不可替换,符合函数式编程思想,减少意外修改;
  • 构造函数中预初始化所有顶点,这是经验之谈:避免后续addEdge()时因get(v)返回null而触发NPE,省去大量if判断;
  • getNeighbors()返回Collections.emptyList()而非null,彻底杜绝调用方的空指针风险——这是Java开发中极易被忽视的防御性编程习惯;
  • Edge类字段全final,保证不可变性,多线程下安全,且toString()便于调试时快速查看。

2.3 Python实现:用defaultdict(list)与namedtuple的轻量组合

Python的优势在于简洁,但简洁不等于随意。我见过太多用dict手动检查key是否存在,或者用list存边却忘记元组不可变性的写法。正确姿势是:from collections import defaultdict+from typing import NamedTuple。

defaultdict(list)自动处理缺失键:graph[v].append((u, w)),无需if v not in graph: graph[v] = [];NamedTuple替代普通tuple,让代码自解释:Edge(to=u, weight=w)比(u, w)直观十倍,且支持属性访问(edge.to),IDE还能自动补全。

from collections import defaultdict from typing import List, Tuple, NamedTuple, Optional class Edge(NamedTuple): to: int weight: int class Graph: def __init__(self, n: int): self.n = n # 顶点数 # 邻接表:defaultdict自动处理缺失键 self.adj_list: defaultdict[int, List[Edge]] = defaultdict(list) # 预初始化所有顶点,确保get_neighbors不会返回None for i in range(n): self.adj_list[i] # 触发default_factory def add_edge(self, v: int, u: int, w: int = 1) -> None: """添加有向边 v->u,权重w,默认为1(无权图)""" self.adj_list[v].append(Edge(to=u, weight=w)) def get_neighbors(self, v: int) -> List[Edge]: """获取顶点v的所有邻居,返回空列表而非None""" return self.adj_list[v] def to_matrix(self) -> List[List[float]]: """可选:转换为邻接矩阵,用于Floyd等算法""" INF = float('inf') matrix = [[INF] * self.n for _ in range(self.n)] for i in range(self.n): matrix[i][i] = 0 for v in self.adj_list: for edge in self.adj_list[v]: matrix[v][edge.to] = min(matrix[v][edge.to], edge.weight) return matrix

这里有个重要技巧:add_edge方法的w=1默认参数。这意味着同一套代码既能处理有权图(显式传w),也能无缝降级为无权图(不传w),避免为两种场景写两套逻辑。to_matrix()方法是预留的扩展接口——当需要运行Floyd-Warshall时,可以一键转换,而不用重构整个图结构。

2.4 实操避坑:顶点编号从0开始,还是从1开始?

这是新手最容易栽跟头的地方。LeetCode题目通常说“n个节点,编号1到n”,而教材示例常从0开始。我的建议是:统一用0-based索引,输入时减1,输出时加1。原因有三:

  1. 数组/列表天然0-based:Java的int[] dist = new int[n],Python的dist = [0] * n,索引0到n-1完美对应;
  2. 避免边界错误:for (int i = 1; i <= n; i++)容易写成i < n或i <= n+1,而for (int i = 0; i < n; i++)逻辑更干净;
  3. 库函数兼容:NetworkX、JGraphT等库内部都是0-based,提前适应。

所以,面对输入"1 2 3"这样的边,代码里应写addEdge(0, 1, 3);输出最短路径时,打印"Path: 1->2->3",即把内部0-based的[0,1,2]映射为[1,2,3]。这个转换逻辑必须封装在I/O层,绝不污染算法核心。

提示:在Graph类外单独写一个InputParser工具类,负责读取标准输入并转换索引。这样算法代码永远只和0-based数字打交道,心智负担降到最低。

3. 六大核心算法逐行解析:从原理到可运行代码

3.1 无向图连通性检测(DFS/BFS):不只是“是否连通”,更是“连通块划分”

连通性检测看似简单,却是图算法的基石。它的本质不是回答“是/否”,而是划分连通分量(Connected Components)。比如某课程设计系统中,200门课形成多个独立依赖环,管理员需要知道哪些课可以一起排期(同一连通块),哪些必须错开(不同连通块)。

Java实现(DFS递归版)
public class ConnectedComponents { private final Graph graph; private final boolean[] visited; private int componentId; private final int[] component; // component[i] = 连通块ID public ConnectedComponents(Graph graph) { this.graph = graph; this.visited = new boolean[graph.vertexCount]; this.component = new int[graph.vertexCount]; this.componentId = 0; } public int countComponents() { Arrays.fill(visited, false); Arrays.fill(component, -1); componentId = 0; for (int v = 0; v < graph.vertexCount; v++) { if (!visited[v]) { dfs(v); componentId++; // 每次dfs完成,新连通块 } } return componentId; } private void dfs(int v) { visited[v] = true; component[v] = componentId; for (Graph.Edge edge : graph.getNeighbors(v)) { int u = edge.to; if (!visited[u]) { dfs(u); } } } // 获取指定连通块的所有顶点 public List<Integer> getComponent(int id) { List<Integer> res = new ArrayList<>(); for (int v = 0; v < graph.vertexCount; v++) { if (component[v] == id) { res.add(v); } } return res; } }

关键点解析:

  • component[]数组是核心,它不仅记录是否访问,更记录每个顶点属于哪个连通块,这才是业务需要的输出;
  • dfs()方法不返回值,纯粹做标记,符合递归的“副作用”哲学;
  • countComponents()中Arrays.fill()重置状态,保证多次调用结果可靠——这是单元测试必须覆盖的点。
Python实现(BFS迭代版,更防栈溢出)
from collections import deque def connected_components_bfs(graph: Graph) -> List[List[int]]: """ 返回所有连通分量的列表,每个元素是顶点ID列表 使用BFS避免递归深度过大(对超大图更安全) """ n = graph.n visited = [False] * n components = [] for v in range(n): if not visited[v]: # BFS遍历新连通块 component = [] queue = deque([v]) visited[v] = True while queue: curr = queue.popleft() component.append(curr) for edge in graph.get_neighbors(curr): u = edge.to if not visited[u]: visited[u] = True queue.append(u) components.append(component) return components # 测试用例构造 def build_test_graph(): g = Graph(6) # 添加边形成两个连通块:[0,1,2] 和 [3,4,5] g.add_edge(0, 1) g.add_edge(1, 2) g.add_edge(3, 4) g.add_edge(4, 5) return g # 运行测试 if __name__ == "__main__": g = build_test_graph() comps = connected_components_bfs(g) print(f"Found {len(comps)} components:") for i, comp in enumerate(comps): print(f"Component {i}: {comp}") # 输出:Component 0: [0, 1, 2], Component 1: [3, 4, 5]

为什么用BFS?因为Python默认递归限制是1000层,而一个链状图(0->1->2->...->999)就会触发RecursionError。BFS用deque显式管理队列,内存可控,适合生产环境。

注意:BFS版本中queue.popleft()必须用deque,不能用list.pop(0)——后者是O(n)时间,会把BFS退化成O(n²)。这是Python性能陷阱中的经典案例。

3.2 有向图拓扑排序(Kahn算法):依赖管理的实时心跳

拓扑排序不是学术玩具,它是构建系统、CI/CD流水线、课程表生成器的引擎。其核心是检测环和生成线性序列。Kahn算法(基于入度的BFS)比DFS版本更直观,且天然支持增量更新——当新增一门课的依赖时,只需调整入度数组,无需重算全图。

Java实现(Kahn算法)
import java.util.*; public class TopologicalSort { private final Graph graph; private final int[] inDegree; // 每个顶点的入度 private final Queue<Integer> queue; // 存储入度为0的顶点 public TopologicalSort(Graph graph) { this.graph = graph; this.inDegree = new int[graph.vertexCount]; this.queue = new LinkedList<>(); // 计算初始入度 for (int v = 0; v < graph.vertexCount; v++) { for (Graph.Edge edge : graph.getNeighbors(v)) { inDegree[edge.to]++; } } // 将所有入度为0的顶点入队 for (int v = 0; v < graph.vertexCount; v++) { if (inDegree[v] == 0) { queue.offer(v); } } } public List<Integer> sort() { List<Integer> result = new ArrayList<>(); int[] inDegreeCopy = inDegree.clone(); // 保护原始数据 Queue<Integer> q = new LinkedList<>(queue); // 复制队列 while (!q.isEmpty()) { int v = q.poll(); result.add(v); // 遍历v的所有邻居,减少其入度 for (Graph.Edge edge : graph.getNeighbors(v)) { int u = edge.to; inDegreeCopy[u]--; if (inDegreeCopy[u] == 0) { q.offer(u); } } } // 检查是否有环:结果长度小于顶点数 if (result.size() != graph.vertexCount) { throw new IllegalStateException("Graph has cycle, topological sort impossible"); } return result; } }

关键设计点:

  • inDegree数组在构造时一次性计算,O(V+E)时间,后续排序复用,避免重复计算;
  • sort()方法内clone()入度数组,保证多次调用互不影响——这是API设计的基本素养;
  • 异常信息明确指出“has cycle”,比泛泛的RuntimeException更有诊断价值。
Python实现(带环检测与详细报告)
from collections import deque from typing import List, Tuple, Optional def topological_sort_kahn(graph: Graph) -> Tuple[bool, List[int], Optional[str]]: """ Kahn算法拓扑排序 返回:(是否成功, 排序结果, 错误信息) """ n = graph.n in_degree = [0] * n # 计算入度 for v in range(n): for edge in graph.get_neighbors(v): in_degree[edge.to] += 1 # 入度为0的队列 queue = deque([v for v in range(n) if in_degree[v] == 0]) result = [] # 记录处理顺序,用于调试 process_log = [] while queue: v = queue.popleft() result.append(v) process_log.append(f"Process {v}, remaining in-degree: {in_degree}") for edge in graph.get_neighbors(v): u = edge.to in_degree[u] -= 1 if in_degree[u] == 0: queue.append(u) if len(result) != n: # 找出剩余的环上节点,辅助调试 cyclic_nodes = [i for i in range(n) if in_degree[i] > 0] return False, [], f"Cycle detected involving nodes {cyclic_nodes}. In-degrees: {in_degree}" return True, result, None # 测试:构造有环图 def build_cyclic_graph(): g = Graph(4) g.add_edge(0, 1) g.add_edge(1, 2) g.add_edge(2, 0) # 形成环 0->1->2->0 g.add_edge(2, 3) return g # 运行 g = build_cyclic_graph() success, order, err = topological_sort_kahn(g) if success: print("Topo order:", [x+1 for x in order]) # 转换为1-based输出 else: print("Error:", err)

这个Python版本的亮点是错误诊断增强:当检测到环时,不仅报错,还列出环上所有节点和当前入度状态,极大缩短调试时间。实际项目中,这种“失败时提供最大信息量”的设计,能节省工程师数小时排查。

3.3 单源最短路径(Dijkstra):为什么优先队列是灵魂,以及如何避免过期条目

Dijkstra算法的精髓不在松弛操作,而在如何高效获取当前距离最小的未访问顶点。暴力扫描O(V)不可接受,二叉堆O(log V)是标配,但Python的heapq和Java的PriorityQueue都有一个致命陷阱:它们不支持更新已存在元素的优先级。当你发现更短路径时,只能插入新条目,导致队列中存在多个同一顶点的过期条目。

Java实现(标准PriorityQueue + visited剪枝)
import java.util.*; public class Dijkstra { private final Graph graph; private final int[] dist; // 最短距离 private final int[] parent; // 路径还原用 private final PriorityQueue<State> pq; public static class State implements Comparable<State> { public final int vertex; public final int distance; public State(int vertex, int distance) { this.vertex = vertex; this.distance = distance; } @Override public int compareTo(State other) { return Integer.compare(this.distance, other.distance); } } public Dijkstra(Graph graph) { this.graph = graph; this.dist = new int[graph.vertexCount]; this.parent = new int[graph.vertexCount]; this.pq = new PriorityQueue<>(); Arrays.fill(dist, Integer.MAX_VALUE); Arrays.fill(parent, -1); } public void compute(int start) { dist[start] = 0; pq.offer(new State(start, 0)); while (!pq.isEmpty()) { State curr = pq.poll(); int v = curr.vertex; // 关键剪枝:如果已找到更短路径,跳过此过期条目 if (curr.distance > dist[v]) { continue; } for (Graph.Edge edge : graph.getNeighbors(v)) { int u = edge.to; int newDist = dist[v] + edge.weight; if (newDist < dist[u]) { dist[u] = newDist; parent[u] = v; pq.offer(new State(u, newDist)); } } } } public List<Integer> getPath(int end) { List<Integer> path = new ArrayList<>(); for (int at = end; at != -1; at = parent[at]) { path.add(at); } Collections.reverse(path); return path; } }

curr.distance > dist[v]这一行是生命线。它意味着:虽然我们把这个顶点加入了队列,但后来发现了更短路径,dist[v]已被更新,当前条目已过期。跳过它,避免无效计算。

Python实现(heapq + index mapping,更省内存)
import heapq from typing import List, Tuple, Optional def dijkstra_heapq(graph: Graph, start: int) -> Tuple[List[int], List[Optional[int]]]: """ 使用heapq实现Dijkstra,返回(dist, parent)元组 通过dist数组本身作为“是否已确定最短距离”的标志 """ n = graph.n dist = [float('inf')] * n parent = [None] * n # heap: (distance, vertex) heap = [(0, start)] dist[start] = 0 while heap: d, v = heapq.heappop(heap) # 剪枝:d大于当前已知最短距离,跳过 if d > dist[v]: continue for edge in graph.get_neighbors(v): u = edge.to new_dist = dist[v] + edge.weight if new_dist < dist[u]: dist[u] = new_dist parent[u] = v heapq.heappush(heap, (new_dist, u)) return dist, parent def reconstruct_path(parent: List[Optional[int]], end: int) -> List[int]: """根据parent数组重建路径""" path = [] at = end while at is not None: path.append(at) at = parent[at] return path[::-1] # 反转得到start->end # 测试:带权重的图 def build_weighted_graph(): g = Graph(5) g.add_edge(0, 1, 10) g.add_edge(0, 4, 5) g.add_edge(1, 2, 1) g.add_edge(1, 4, 2) g.add_edge(2, 3, 4) g.add_edge(3, 2, 6) # 反向边 g.add_edge(4, 1, 3) g.add_edge(4, 2, 9) g.add_edge(4, 3, 2) return g # 运行 g = build_weighted_graph() dist, parent = dijkstra_heapq(g, 0) print("Distances from 0:", [f"{i}:{d:.0f}" for i, d in enumerate(dist)]) print("Path to 3:", reconstruct_path(parent, 3)) # 输出:Distances from 0: ['0:0', '1:8', '2:9', '3:11', '4:5'] # Path to 3: [0, 4, 3]

Python版本用float('inf')初始化距离,比Java的Integer.MAX_VALUE更符合数学直觉;reconstruct_path函数用None而非-1表示无父节点,类型更安全。heapq.heappush和heappop的调用位置严格对应算法步骤,无冗余。

3.4 全源最短路径(Floyd-Warshall):动态规划的优雅暴力

Floyd-Warshall是少有的O(V³)算法,但它胜在简洁、易实现、能处理负权边(无负环)。它的思想是:dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]),即考虑经过中间点k是否能缩短i到j的距离。三层循环,直白到令人感动。

Java实现(原地更新,空间最优)
public class FloydWarshall { private final int[][] dist; public FloydWarshall(Graph graph) { int n = graph.vertexCount; this.dist = new int[n][n]; // 初始化:对角线0,其余为INF final int INF = Integer.MAX_VALUE / 2; // 防止溢出 for (int i = 0; i < n; i++) { Arrays.fill(dist[i], INF); dist[i][i] = 0; } // 从邻接表填充初始距离 for (int v = 0; v < n; v++) { for (Graph.Edge edge : graph.getNeighbors(v)) { dist[v][edge.to] = Math.min(dist[v][edge.to], edge.weight); } } } public void compute() { int n = dist.length; for (int k = 0; k < n; k++) { for (int i = 0; i < n; i++) { // 剪枝:如果dist[i][k]是INF,跳过,避免溢出 if (dist[i][k] == Integer.MAX_VALUE / 2) continue; for (int j = 0; j < n; j++) { if (dist[k][j] != Integer.MAX_VALUE / 2) { dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]); } } } } // 检测负环:如果dist[i][i] < 0,则存在负环 for (int i = 0; i < n; i++) { if (dist[i][i] < 0) { throw new IllegalStateException("Negative cycle detected"); } } } public int getDistance(int i, int j) { return dist[i][j] == Integer.MAX_VALUE / 2 ? -1 : dist[i][j]; } }

注意INF设为Integer.MAX_VALUE / 2而非MAX_VALUE,这是防止dist[i][k] + dist[k][j]溢出成负数,导致错误的松弛。compute()中if (dist[i][k] == INF) continue是关键剪枝,将常数因子降低近一半。

Python实现(使用NumPy加速,适合大数据)
import numpy as np from typing import List, Tuple def floyd_warshall_numpy(graph: Graph) -> np.ndarray: """ 使用NumPy实现Floyd-Warshall,利用向量化加速 对于V>1000的图,比纯Python快10倍以上 """ n = graph.n # 初始化距离矩阵 INF = np.inf dist = np.full((n, n), INF) np.fill_diagonal(dist, 0) # 从邻接表填充 for v in range(n): for edge in graph.get_neighbors(v): dist[v, edge.to] = min(dist[v, edge.to], edge.weight) # Floyd-Warshall三重循环 for k in range(n): # 向量化:dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]) # 使用广播:dist[:, k] + dist[k, :] 生成 (n,n) 矩阵 new_dist = dist[:, k:k+1] + dist[k:k+1, :] dist = np.minimum(dist, new_dist) # 检测负环 if np.any(np.diag(dist) < 0): raise ValueError("Negative cycle detected") return dist # 测试 g = Graph(4) g.add_edge(0, 1, 1) g.add_edge(1, 2, 1) g.add_edge(2, 3, 1) g.add_edge(3, 0, -4) # 引入负环 try: d = floyd_warshall_numpy(g) print("Distance matrix:\n", d) except ValueError as e: print("Error:", e) # 输出 Error: Negative cycle detected

NumPy版本展示了技术选型的灵活性:当图规模增大,纯Python的三重循环会成为瓶颈,此时引入NumPy向量化,性能飞跃。dist[:, k:k+1] + dist[k:k+1, :]利用广播机制,一行代码替代两层循环,是科学计算的典型范式。

3.5 最小生成树(Prim与Kruskal):两种哲学,一个目标

最小生成树(MST)解决的是“如何用最少成本连接所有节点”。Prim算法像生长的菌丝,从一点出发,每次粘附最近的未访问节点;Kruskal则像拼图,把所有边排序,从小到大捡起,只要不形成环就拼上。两者时间复杂度相近,但适用场景不同:Prim适合稠密图(邻接矩阵),Kruskal适合稀疏图(邻接表),且Kruskal天然支持并行化。

Java实现(Prim算法,基于PriorityQueue)
public class PrimMST { private final Graph graph; private final int[] key; // key[v] = v到MST的最小边权 private final boolean[] inMST; private final int[] parent; // MST中v的父节点 private final PriorityQueue<EdgeWithKey> pq; public static class EdgeWithKey implements Comparable<EdgeWithKey> { public final int vertex; public final int key; public EdgeWithKey(int vertex, int key) { this.vertex = vertex; this.key = key; } @Override public int compareTo(EdgeWithKey other) { return Integer.compare(this.key, other.key); } } public PrimMST(Graph graph) { this.graph = graph; this.key = new int[graph.vertexCount]; this.inMST = new boolean[graph.vertexCount]; this.parent = new int[graph.vertexCount]; this.pq = new PriorityQueue<>(); Arrays.fill(key, Integer.MAX_VALUE); Arrays.fill(parent, -1); } public void compute(int start) { key[start] = 0; pq.offer(new EdgeWithKey(start, 0)); while (!pq.isEmpty()) { Edge
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 5:44:24

VS2022全版本平台工具集配置与兼容性实战

1. 项目概述&#xff1a;VS2022全版本平台工具集配置实战 在维护历史遗留项目时&#xff0c;我们常会遇到这样的困境&#xff1a;新购置的机器已经安装了最新的Visual Studio 2022&#xff0c;但需要编译十几年前用VC6.0开发的工程文件。传统做法往往需要同时安装多个版本的VS…

作者头像 李华
网站建设 2026/10/31 5:44:24

H.264分析工具:解析NALU、SPS/PPS与Slice语义的调试示波器

简介&#xff1a;这是一套面向音视频开发工程师、编解码学习者及多媒体技术研究者的H.264码流深度分析工具源码与配套资源&#xff0c;聚焦Windows平台下的H.264&#xff08;AVC&#xff09;编码结构解析、参数调试与常见运行问题排错。资源包含186个文件&#xff0c;涵盖118个…

作者头像 李华
网站建设 2026/10/31 5:43:39

微信小程序点餐系统开发实战:架构设计与核心实现

1. 项目背景与核心需求在餐饮行业数字化转型浪潮中&#xff0c;移动端点餐系统已成为提升运营效率的关键工具。传统纸质菜单存在更新不便、人力成本高、排队时间长等痛点&#xff0c;而基于微信小程序的解决方案凭借无需安装、即用即走的特性&#xff0c;成为餐饮商户的首选。本…

作者头像 李华
网站建设 2026/10/31 5:43:28

非递归全排列算法原理与工程实践指南

简介&#xff1a;本资源提供一种高效、稳定的N个数全排列非递归实现方案&#xff0c;面向算法学习者、C初学者及编程竞赛备赛人员&#xff0c;解决递归易致栈溢出、调试困难等实际痛点。代码采用纯迭代逻辑&#xff0c;通过元素交换与状态追踪完成全排列生成&#xff0c;兼顾可…

作者头像 李华
网站建设 2026/10/31 5:42:49

CSS Grid布局:二维响应式网页设计核心指南

1. 网格布局概述CSS Grid布局是当前最强大的二维布局系统。它彻底改变了我们构建网页布局的方式&#xff0c;将容器划分为行和列组成的网格单元&#xff0c;让我们能够精确控制元素在水平和垂直方向上的位置。与传统的浮动布局或Flexbox&#xff08;主要处理一维布局&#xff0…

作者头像 李华
网站建设 2026/10/31 5:42:33

手写数字识别:CNN实战与MNIST数据集解析

1. 项目概述&#xff1a;手写数字识别的深度学习实践手写数字识别是计算机视觉领域的经典入门项目&#xff0c;相当于编程界的"Hello World"。这个项目使用卷积神经网络(CNN)对MNIST数据集中的手写数字进行分类识别&#xff0c;能帮助初学者快速理解深度学习的基本流…

作者头像 李华