图搜索算法(上):Dijkstra算法原理、实现与优缺点

说到路径规划,Dijkstra算法是绕不开的基石。我个人觉得,搞懂它,后面学A*、D*这些高级算法会轻松很多。说白了,Dijkstra就是解决“从A点到B点,哪条路最短”这个问题的最经典方法。

算法核心思想:贪心 + 动态规划

Dijkstra算法的本质,其实就两句话:每次找离起点最近的那个未处理节点,然后用它去更新邻居的距离。嗯,听起来很简单对吧?

我刚开始学的时候,总觉得这玩意儿跟“广度优先搜索”有点像。区别在哪呢?BFS用队列,一层层往外扩;Dijkstra用优先队列,谁距离小谁先出。你想想看,如果每条边的权重都是1,那Dijkstra就退化成BFS了。

核心前提:Dijkstra要求图中不能有负权边。为什么?因为一旦有负权边,已经确定的最短距离可能被后续的负权边“反超”,算法就失效了。

算法步骤详解

咱们一步步来拆解。假设有一个图,起点是S,终点是T。

  1. 初始化:所有节点的距离设为无穷大,起点S的距离设为0。创建一个优先队列,把S放进去。
  2. 循环处理:从优先队列里取出距离最小的节点u。
  3. 松弛操作:遍历u的所有邻居v。如果dist[u] + weight(u,v) < dist[v],就更新dist[v],并把v加入优先队列。
  4. 重复:直到优先队列为空,或者取出了目标节点T。

这里有个细节:每个节点可能被多次加入优先队列,但只有第一次取出时,它的距离才是最终确定的。我在项目中遇到过有人没注意这点,导致算法死循环——嗯,调试了一下午才发现。

代码实现:Python版

我个人习惯用Python写原型,清晰又好改。下面是一个标准实现:

import heapq

def dijkstra(graph, start, end):
    # graph: 邻接表,{node: [(neighbor, weight), ...]}
    INF = float('inf')
    dist = {node: INF for node in graph}
    dist[start] = 0
    pq = [(0, start)]  # (距离, 节点)
    visited = set()
    
    while pq:
        current_dist, u = heapq.heappop(pq)
        
        if u in visited:
            continue
        visited.add(u)
        
        if u == end:
            return current_dist
        
        for v, w in graph[u]:
            if v in visited:
                continue
            new_dist = current_dist + w
            if new_dist < dist[v]:
                dist[v] = new_dist
                heapq.heappush(pq, (new_dist, v))
    
    return INF  # 不可达

避坑指南:我曾经在实现时忘了加visited集合,结果同一个节点被反复处理,性能直接崩了。记住:优先队列里取出的节点,如果已经访问过,直接跳过。

时间复杂度分析

这个得说清楚。用二叉堆实现的话:

操作 复杂度 说明
插入/更新 O(log V) 每个节点最多入堆一次
取出最小 O(log V) 每次取一个
总复杂度 O((V+E) log V) V是节点数,E是边数

如果图比较稠密(E接近V²),用斐波那契堆可以降到O(V log V + E)。但说实话,工程上很少用斐波那契堆——常数太大,实现也复杂。二叉堆够用了。

优缺点分析

优点:

  • 保证找到最短路径(前提是无负权边)
  • 实现简单,代码量少
  • 适用于静态图(拓扑不变的情况)

缺点:

  • 没有方向性,会探索很多无关区域。你想想看,如果目标在起点东边,它还是会往西边搜
  • 在大规模图上,内存消耗大(需要存所有节点的距离)
  • 无法处理动态变化的环境(比如突然出现的障碍物)

注意:在自动驾驶的全局路径规划中,Dijkstra很少直接使用。因为道路网络动辄几万个节点,跑一次要几百毫秒,太慢了。但它是理解A*算法的基础——A*就是在Dijkstra基础上加了启发式函数。

核心逻辑流程图

下面这张图展示了Dijkstra算法的核心流程,我特意画得简洁一些:

初始化 优先队列 为空? 是 返回结果 否 取出距离 最小节点u u已访问? 是 否 标记 已访问 遍历邻居 松弛操作

这张图把核心循环画清楚了。你注意看那个“已访问”的判断——这是很多人容易漏掉的。我刚开始写的时候,没加这个判断,结果在环形图上跑出了死循环。

实际应用中的坑

最后分享几个实战经验:

  • 地图太大怎么办? 可以用分层Dijkstra。先把地图分成区块,在区块级别做规划,再在区块内部细化。我在做园区物流机器人时用过这招,效果不错。
  • 实时性要求高? 可以设置一个最大搜索半径。比如只搜500米内的节点,超出就放弃。虽然可能找不到最优解,但至少能快速给出一个可行解。
  • 路径平滑问题:Dijkstra找出来的路径可能有很多折角。我建议后面加一个平滑处理,比如用B样条曲线拟合一下。

一句话总结:Dijkstra是图搜索的“Hello World”,简单、可靠、但不够聪明。理解它,你就能理解更高级的启发式搜索算法。

可可精品资料库