【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法

Dijkstra算法,中文叫狄克斯特拉算法,在地图中寻找两个地点之间的最短或者最快路径非常有用。狄克斯特拉算法是一个贪婪算法,也就是在处理过程中每一步都选择最佳路径。

例子

我们将通过一个有向图来演示一下狄克斯特拉算法的原理:

【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第1张图片

在狄克斯特拉算法中,我们首先要先选定一个起点,假设起点为 A。

  1. 第一条路径

从 A 出发,有三条路径,分别是:

  • A 到 B,权重为 1
  • A 到 D,权重为 9
  • A 到 C,权重为 2

我们将用一个表来记录寻找路径的过程,目前标的状态如下:

【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第2张图片

表格中的后面的四个 nil 意思是暂时没有顶点通往 E、F、G 和 H。前三个单元格下面的 A 是指通往 B、C 和 D 的上一个顶点,上面的数字是指通往 B、C 和 D 的总权重。

  1. 第二条路径

在狄克斯特拉算法中,每一步都要选择最优的路径。从第一步上看,权重最小的是从 A 到 B,所以我们就沿着 A 到 B 这条路走。B 只能通往 D,我们更新图和表格如下:

【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第3张图片
【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第4张图片

这一轮只有 B 通往 D,权重是 2,通往 D 的总权重是 4,因为从 A 到 B 到 D 为 1 + 2 = 3。在上一轮中,已经有一条路从 A 通往 D,权重为 9。在第二轮中我们找到了通往 D 的更近的路,所以我们在第二行中把通往 D 的上一个顶点更新为 3B。另外,因为从 A 通往 B 的权重是最小的,所以我们把它涂上背景色。

  1. 第三条路径

从上一步的表格看,下一个总权重最小的是 A 通往 C 的 2,所以沿着 C 走。有两条路:

  • C 到 D 的总权重为 3 + 8 = 11
  • C 到 F 的总权重为 3 + 5 = 8

C 到 D 的总权重比之前通往 D 的路大,所以不需要更新通往 D 的路。我们更新图和表格如下:

【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第5张图片
【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第6张图片
  1. 第四条路径

从上一步的表格看,下一个总权重最小的是 B 通往 D 的 3,所以沿着 D 走。有两条路:

  • D 到 G 的总权重为 3 + 9 = 12
  • D 到 E 的总权重为 3 + 7 = 10

更新图和表格如下:

【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第7张图片
【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第8张图片
  1. 第五条路径

从上一步的表格看,下一个总权重最小的是 C 通往 F 的 8,所以沿着 F 走。有一条路:

  • F 到 E 的总权重为 8 + 4 = 12

F 到 E 的总权重大于 D 到 E 的总权重,所以不需要更新通往 E 的路。更新图和表格如下:

【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第9张图片
【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第10张图片
  1. 第六条路径

从上一步的表格看,下一个总权重最小的是 D 通往 E 的 10,所以沿着 E 走。有三条路:

  • E 到 F 的总权重为 10 + 4 = 14,已经有更近的路到 F,所以忽略这条路
  • E 到 D 的总权重为 10 + 5 = 15,已经有更近的路到 D,所以忽略这条路
  • E 到 H 的总权重为 10 + 3 = 13

更新图和表格如下:

【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第11张图片
【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第12张图片
  1. 第七条路径

从上一步的表格看,下一个总权重最小的是 D 通往 G 的 12,但是 G 没有其他邻居,也就意味着我能找到了到达 G 的最小路径,更新图和表格如下:

【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第13张图片
【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第14张图片
  1. 第八条路径

从上一步的表格看,下一个总权重最小的是 E 通往 H 的 12,但是 H 没有其他邻居,也就意味着我能找到了到达 H 的最小路径,更新图和表格如下:

【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第15张图片
【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第16张图片

所有的顶点都已经遍历,完成了狄克斯特拉算法。我们可以从图中直接得到通往 E 的最小权重为 10。然后反方向往回看就能得到通往 E 的最小路径,E --> D --> B --> A。示意图如下:

【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法_第17张图片

实现

在实现过程中,我们要用到优先队列,这里我们用最小优先队列,这样每次从队列中取出的元素都是目前总权重最小的顶点。

首先我们定义一个枚举,用来区分顶点的类型:

enum Visit {
    case start // 顶点是起点
    case edge(Edge) // 顶点关联着通往它的边
}

下面看下狄克斯特拉算法的具体实现:

class Dijkstra {
    typealias Graph = AdjacencyList
    
    let graph: Graph
    
    init(graph: Graph) {
        self.graph = graph
    }
    
    /// 找出从某个顶点开始的所有路径
    func paths(from start: Vertex) -> [Vertex: Visit] {
        // 用一个字典来记录每一步的数据,
        // key 是顶点,value 是顶点类型或者是关联着通往这个顶点的边
        var paths: [Vertex: Visit] = [start: .start]
        // 创建最小优先队列,`order` 闭包队列中元素排序的条件,总权重最小的优先
        var priorityQueue = PriorityQueue>(order: {
            self.distance(to: $0, with: paths) <
                self.distance(to: $1, with: paths)
        })
        priorityQueue.enqueue(start)
        
        while let vertex = priorityQueue.dequeue() { // 取出队列当前最小权重的顶点
            for edge in graph.edges(from: vertex) { // 遍历从这个顶点出发的边
                guard let weight = edge.weight else {
                    continue
                }
                // 如果边的终点不在字典中,
                // 或者从当前顶点出发达到边的终点总权重小于之前的路径,
                // 更新路径,并把邻居加入到队列中
                if paths[edge.destination] == nil ||
                    distance(to: vertex, with: paths) + weight <
                    distance(to: edge.destination, with: paths) {
                    
                    paths[edge.destination] = .edge(edge)
                    priorityQueue.enqueue(edge.destination)
                }
            }
        }
        
        return paths
    }
    
    /// 根据记录着每一步数据的字典,找到到达某个终点的最小路径,
    // 返回由边组成的有序数组
    func shortestPath(to destination: Vertex,
                      with paths: [Vertex: Visit]) -> [Edge] {
        var vertex = destination
        var path: [Edge] = []
        while let visit = paths[vertex], case .edge(let edge) = visit {
            path = [edge] + path
            vertex = edge.source
        }
        return path
    }
    
    // MARK: - Private
    
    // 根据记录着每一步数据的字典中的数据,计算到达某一个终点的总权重
    private func distance(to destination: Vertex,
                          with paths: [Vertex: Visit]) -> Double {
        let path = shortestPath(to: destination, with: paths)
        return path.compactMap { $0.weight }
                   .reduce(0, +)
    }
}

测试

let graph = AdjacencyList()

let a = graph.createVertex(value: "A")
let b = graph.createVertex(value: "B")
let c = graph.createVertex(value: "C")
let d = graph.createVertex(value: "D")
let e = graph.createVertex(value: "E")
let f = graph.createVertex(value: "F")
let g = graph.createVertex(value: "G")
let h = graph.createVertex(value: "H")

graph.addDirectedEdge(from: a, to: b, weight: 1)
graph.addDirectedEdge(from: a, to: d, weight: 9)
graph.addDirectedEdge(from: a, to: c, weight: 2)
graph.addDirectedEdge(from: b, to: d, weight: 2)
graph.addDirectedEdge(from: c, to: d, weight: 8)
graph.addDirectedEdge(from: c, to: f, weight: 5)
graph.addDirectedEdge(from: d, to: g, weight: 9)
graph.addDirectedEdge(from: d, to: e, weight: 7)
graph.addDirectedEdge(from: e, to: d, weight: 5)
graph.addUndirectedEdge(between: e, and: f, weight: 4)
graph.addDirectedEdge(from: e, to: h, weight: 3)

let dijkstra = Dijkstra(graph: graph)
let pathsFromA = dijkstra.paths(from: a)
let path = dijkstra.shortestPath(to: e, with: pathsFromA)
for edge in path {
    print("\(edge.source) -- \(edge.weight ?? 0) -- > \(edge.destination)")
}

// 结果
0: A -- 1.0 --> 1: B
1: B -- 2.0 --> 3: D
3: D -- 7.0 --> 4: E

首先用邻接表创建本篇文章例子的图,最终通过 Dijkstra 找到从 A 通往 E 的最小路径为:A --> B --> D --> E

性能分析

迪克斯特拉算法的时间复杂度,主要取决于优先队列中元素的移除和插入。

优先队列中元素的移除和插总的时间是 O(log V)。在算法的实现中,我们还要遍历所有的顶点,时间为 O(E)。所以迪克斯特拉算法总的时间复杂度为 O(E log V)

完整代码 >>

参考资料

Data Structures and Algorithms in Swift --- raywenderlich.com,如果想看原版书籍,请点击链接购买。

欢迎加入我管理的Swift开发群:536353151

你可能感兴趣的:(【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法)