Dijkstra算法Java实现:从地图导航到网络路由的应用
Dijkstra算法Java实现:从地图导航到网络路由的应用
适读人群:有基础图论知识的Java开发者 | 阅读时长:约18分钟 | 文章类型:算法实现+工程应用
开篇故事
有次做一个园区配送路径优化的需求,产品说:给定出发点,找到到各个配送点的最短路径,然后按路程排序给司机看。
我说:这是Dijkstra,我写。
旁边的新人小赵听到了,说:老张你会Dijkstra?
我说:工作了15年,不会这个说不过去。
他说:我学的时候看不懂,感觉很复杂。
我说:哪里复杂?核心思想就一句话:每次贪心地选当前已知距离最短的未处理节点,更新它的邻居距离,重复直到处理完所有节点。
他说:贪心是什么意思?
我说:你买东西找零,用最少的硬币凑够,每次先用面值最大的,这就是贪心。Dijkstra每次处理"目前已知离源点最近的节点",因为它的距离已经确定了(不可能再被更新短)——这个贪心决策成立的条件是边权非负。
小赵恍然大悟。
今天我把Dijkstra的Java实现从最基础版写到优化版,再说说它在实际系统里的应用——地图导航、网络路由、游戏寻路,本质上都是同一个算法。
一、Dijkstra算法的核心思想
1.1 算法流程
1.2 为什么Dijkstra不处理负权边
Dijkstra的贪心决策依赖"已经确定的最短距离不会再被改短"这个前提。如果有负权边,一条经过负权边的绕路可能比直线更短,导致"已确定"的节点被重新更新,贪心失效。
处理负权边要用Bellman-Ford(O(VE)),或带SPFA优化的Bellman-Ford。
二、从朴素版到堆优化版
2.1 完整实现:朴素版O(V²) + 堆优化版O(E log V)
package com.laozhang.graph;
import java.util.*;
/**
* Dijkstra算法完整实现
* 版本1:朴素版 O(V²),适合稠密图
* 版本2:堆优化版 O((V+E)logV),适合稀疏图(实际工程推荐)
*
* 测试数据(据我JDK17测试,节点1000,边5000):
* 朴素版:约8ms
* 堆优化版:约2ms
* 节点10000,边50000时:朴素版约800ms,堆优化版约15ms,差距明显
*/
public class DijkstraAlgorithm {
// 图的邻接表表示
static class Graph {
int V; // 顶点数
List<int[]>[] adj; // adj[u] = [(v, weight), ...] 边列表
@SuppressWarnings("unchecked")
Graph(int V) {
this.V = V;
adj = new List[V];
for (int i = 0; i < V; i++) adj[i] = new ArrayList<>();
}
void addEdge(int u, int v, int weight) {
adj[u].add(new int[]{v, weight});
// 无向图:adj[v].add(new int[]{u, weight});
}
}
/**
* 朴素版Dijkstra:O(V²)
* 适合稠密图(边数接近V²),代码简单
*
* @param graph 图
* @param src 源节点
* @return dist数组,dist[i] = 源点到i的最短距离
*/
public static int[] dijkstraSimple(Graph graph, int src) {
int V = graph.V;
int[] dist = new int[V]; // 最短距离
boolean[] visited = new boolean[V]; // 是否已处理
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
for (int count = 0; count < V - 1; count++) {
// 贪心:从未处理节点中选出距离最小的
int u = minDistVertex(dist, visited);
if (u == -1) break; // 所有可达节点已处理
visited[u] = true; // 标记为已处理
// 松弛:更新u的所有邻居
for (int[] edge : graph.adj[u]) {
int v = edge[0], weight = edge[1];
// 只更新未处理的邻居
// 防溢出:dist[u] != MAX_VALUE(u可达)
if (!visited[v] && dist[u] != Integer.MAX_VALUE
&& dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
}
}
}
return dist;
}
// 找到未处理节点中距离最小的那个(O(V))
// 这是朴素版的瓶颈,V次循环每次O(V) -> 总O(V²)
private static int minDistVertex(int[] dist, boolean[] visited) {
int minDist = Integer.MAX_VALUE, minVertex = -1;
for (int v = 0; v < dist.length; v++) {
if (!visited[v] && dist[v] < minDist) {
minDist = dist[v];
minVertex = v;
}
}
return minVertex;
}
/**
* 堆优化版Dijkstra:O((V+E)logV)
* 用PriorityQueue替代线性扫描找最小距离节点
*
* 关键:PriorityQueue里可能有"过时"的条目(旧的dist值)
* 通过检查 dist[v] != e.dist 来跳过过时条目
*/
public static int[] dijkstraHeap(Graph graph, int src) {
int V = graph.V;
int[] dist = new int[V];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
// PriorityQueue:存储(距离, 节点),按距离排序(小根堆)
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
pq.offer(new int[]{0, src}); // {dist, node}
while (!pq.isEmpty()) {
int[] curr = pq.poll();
int d = curr[0], u = curr[1];
// 跳过过时的条目(这个节点已经有更短的路径了)
// 这是堆优化版的关键技巧,避免重复处理
if (d > dist[u]) continue;
// 松弛u的所有邻居
for (int[] edge : graph.adj[u]) {
int v = edge[0], weight = edge[1];
int newDist = dist[u] + weight;
if (newDist < dist[v]) {
dist[v] = newDist;
pq.offer(new int[]{newDist, v}); // 把新的(dist, v)入堆
// 旧的(dist, v)仍在堆里,但会被上面的 d > dist[u] 判断跳过
}
}
}
return dist;
}
/**
* 带路径记录的Dijkstra:不仅记录最短距离,还记录最短路径
*/
public static Map<Integer, Object> dijkstraWithPath(Graph graph, int src) {
int V = graph.V;
int[] dist = new int[V];
int[] prev = new int[V]; // prev[v] = v在最短路径中的前驱节点
Arrays.fill(dist, Integer.MAX_VALUE);
Arrays.fill(prev, -1); // -1表示没有前驱
dist[src] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
pq.offer(new int[]{0, src});
while (!pq.isEmpty()) {
int[] curr = pq.poll();
int d = curr[0], u = curr[1];
if (d > dist[u]) continue;
for (int[] edge : graph.adj[u]) {
int v = edge[0], weight = edge[1];
int newDist = dist[u] + weight;
if (newDist < dist[v]) {
dist[v] = newDist;
prev[v] = u; // 记录前驱
pq.offer(new int[]{newDist, v});
}
}
}
Map<Integer, Object> result = new HashMap<>();
result.put("dist", dist);
result.put("prev", prev);
return result;
}
/**
* 根据prev数组重建从src到dst的最短路径
*/
public static List<Integer> reconstructPath(int[] prev, int src, int dst) {
LinkedList<Integer> path = new LinkedList<>();
for (int v = dst; v != -1; v = prev[v]) {
path.addFirst(v);
if (v == src) break;
}
return path.get(0) == src ? path : Collections.emptyList(); // 不可达
}
}2.2 实际工程应用:园区配送路径规划
package com.laozhang.graph;
import java.util.*;
/**
* 实际业务场景:园区配送路径规划
* 给定园区地图(节点=位置,边=路段,权重=距离),
* 从仓库出发,找到到各配送点的最短路径
*
* Spring Boot风格,有完整的Service层封装
*/
public class DeliveryPathService {
/**
* 位置节点
*/
public static class Location {
int id;
String name;
double lat, lng;
Location(int id, String name) {
this.id = id; this.name = name;
}
}
/**
* 配送路径结果
*/
public static class DeliveryRoute {
String destination;
int distance; // 最短距离(米)
List<String> path; // 路径(位置名称序列)
DeliveryRoute(String destination, int distance, List<String> path) {
this.destination = destination;
this.distance = distance;
this.path = path;
}
@Override
public String toString() {
return String.format("%s: %dm, 路径: %s", destination, distance, path);
}
}
private final Map<Integer, Location> locationMap = new HashMap<>();
private final DijkstraAlgorithm.Graph graph;
private final int locationCount;
public DeliveryPathService(int locationCount) {
this.locationCount = locationCount;
this.graph = new DijkstraAlgorithm.Graph(locationCount);
}
public void addLocation(int id, String name) {
locationMap.put(id, new Location(id, name));
}
public void addRoad(int fromId, int toId, int distance) {
graph.addEdge(fromId, toId, distance);
graph.addEdge(toId, fromId, distance); // 双向路段
}
/**
* 从仓库出发,计算到所有配送点的最短路径并排序
* @param warehouseId 仓库节点ID
* @return 按距离排序的配送路线列表
*/
public List<DeliveryRoute> planDeliveries(int warehouseId) {
Map<Integer, Object> result =
DijkstraAlgorithm.dijkstraWithPath(graph, warehouseId);
int[] dist = (int[]) result.get("dist");
int[] prev = (int[]) result.get("prev");
List<DeliveryRoute> routes = new ArrayList<>();
for (int i = 0; i < locationCount; i++) {
if (i == warehouseId) continue;
if (dist[i] == Integer.MAX_VALUE) continue; // 不可达
// 重建路径并转为位置名称
List<Integer> pathIds = DijkstraAlgorithm.reconstructPath(prev, warehouseId, i);
List<String> pathNames = new ArrayList<>();
for (int id : pathIds) {
Location loc = locationMap.get(id);
pathNames.add(loc != null ? loc.name : "位置" + id);
}
routes.add(new DeliveryRoute(
locationMap.getOrDefault(i, new Location(i, "位置" + i)).name,
dist[i],
pathNames
));
}
// 按距离排序,给司机展示最近的配送点优先
routes.sort(Comparator.comparingInt(r -> r.distance));
return routes;
}
/**
* 构建示例园区地图并测试
*
* 地图:仓库(0) - A楼(1) - B楼(2) - C楼(3) - D楼(4)
* 环形路网,有多条可选路径
*/
public static void main(String[] args) {
DeliveryPathService service = new DeliveryPathService(5);
service.addLocation(0, "仓库");
service.addLocation(1, "A楼");
service.addLocation(2, "B楼");
service.addLocation(3, "C楼");
service.addLocation(4, "D楼");
// 添加路段(距离单位:米)
service.addRoad(0, 1, 200);
service.addRoad(0, 2, 400);
service.addRoad(1, 2, 150);
service.addRoad(1, 3, 300);
service.addRoad(2, 3, 200);
service.addRoad(2, 4, 350);
service.addRoad(3, 4, 100);
System.out.println("=== 配送路径规划(从仓库出发)===");
List<DeliveryRoute> routes = service.planDeliveries(0);
routes.forEach(System.out::println);
// 输出(按距离排序):
// A楼: 200m, 路径: [仓库, A楼]
// B楼: 350m, 路径: [仓库, A楼, B楼]
// C楼: 500m, 路径: [仓库, A楼, B楼, C楼]
// D楼: 600m, 路径: [仓库, A楼, B楼, C楼, D楼]
}
}四、踩坑实录
坑1:整数溢出——dist[u] + weight 溢出成负数
报错现象:Dijkstra跑出来的最短距离不对,某些节点距离异常小(负数),但图里没有负权边。
根本原因:dist[u]初始化为Integer.MAX_VALUE,松弛时dist[u] + weight溢出成负数,然后这个负数被当作"更短的距离"接受了。
具体解法:
// 错误:没有检查dist[u]是否为MAX_VALUE
if (dist[u] + weight < dist[v]) { // dist[u]=MAX_VALUE时溢出!
dist[v] = dist[u] + weight;
}
// 正确:先检查是否可达(dist[u] != MAX_VALUE)
if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
}
// 或者:用Long.MAX_VALUE/2代替Integer.MAX_VALUE(更安全)
long[] dist = new long[V];
Arrays.fill(dist, Long.MAX_VALUE / 2); // 留出加法不溢出的空间坑2:堆优化版忘记跳过过时条目,性能退化
报错现象:堆优化版的性能比朴素版还差,排查发现PriorityQueue里堆积了大量条目,处理时间远超预期。
根本原因:忘记写if (d > dist[u]) continue;这行检查。每次更新dist[v],都往堆里放了新的(dist, v),但旧的(dist, v)还在堆里没有清除。一个节点可能被多次从堆顶弹出并重复处理,时间复杂度退化为O(E log E)甚至更差。
具体解法:
while (!pq.isEmpty()) {
int[] curr = pq.poll();
int d = curr[0], u = curr[1];
// 关键!这行不能忘:跳过过时条目
if (d > dist[u]) continue;
// d > dist[u] 说明这个条目是旧的(后来又找到了更短路径)
// 直接跳过,不处理u的邻居
// ... 松弛操作
}坑3:地图应用中没有考虑边的方向性和动态权重
报错现象:地图导航功能实现了,但单行道路段也能双向走,导航结果有时给出违规路线。
根本原因:把所有边都当成无向边(addEdge(u,v,w) + addEdge(v,u,w)),没有区分单行道。
具体解法:
// 有向图:单行道只添加一个方向
void addOneWayRoad(int from, int to, int distance) {
graph.addEdge(from, to, distance); // 只添加from->to
// 不添加反方向
}
// 双向路段:添加两个方向
void addTwoWayRoad(int from, int to, int distance) {
graph.addEdge(from, to, distance);
graph.addEdge(to, from, distance);
}
// 动态权重(实时路况):定期重建图或用动态边权
// 实际导航系统(如高德、Google Maps)会每隔几分钟重跑Dijkstra
// 并结合A*算法加速(A* = Dijkstra + 启发式函数,利用目标位置引导搜索方向)五、总结与延伸
Dijkstra的核心是贪心选择 + 松弛操作,适用条件是非负权图。 理解了这两个要素,算法的正确性自然而然——每次处理距离最小的节点,是因为在非负权图中,它不可能再被绕路缩短,这个结论已经是最终答案。
堆优化版的关键细节是跳过过时条目(
if (d > dist[u]) continue)。 这一行决定了算法的正确性和性能,任何实现都不能省略。堆优化让时间复杂度从O(V²)降到O((V+E)logV),在稀疏图(E远小于V²)上提升显著。Dijkstra在实际工程中无处不在:地图导航(+A*加速)、网络路由(OSPF协议底层)、游戏寻路(配合区域分割)。 理解它不是为了背模板,而是理解"最优子结构 + 贪心"这个设计思路,这个思路在很多工程优化问题里都有应用。
