基础知识概述

在开始编码之前,我们需要理解一些基础的概念:

有向图:有向图中的每条边都是单向的,即从一个节点指向另一个节点,不能反向行驶。 权重:图中的边可以附带权重,表示从一个节点到另一个节点的代价(如距离或时间)。 最短路径:在一个有向图中,从起点到终点的路径中,边权重之和最小的路径称为最短路径。 在解决有向图中的最短路径问题时,最常用的算法是 Dijkstra 算法。

Dijkstra 算法简介

Dijkstra 算法是一种贪心算法,用于计算加权图中从一个源节点到其他所有节点的最短路径。它的工作原理如下:

初始化源节点的距离为 0,其他节点的距离为无穷大。 每次从未访问的节点中选择距离最小的节点,将其标记为“已访问”。 更新邻居节点的距离,如果通过当前节点的路径比之前记录的路径更短,则更新距离。 重复上述步骤,直到访问了所有节点或找到目标节点。

Golang 中的 Dijkstra 实现

接下来,我们将使用 Golang 编写一个 Dijkstra 算法,处理有向图的最短路径问题。

数据结构定义

首先,定义节点和图的结构:

package main

import (
    "container/heap"
    "fmt"
    "math"
)

// Edge 代表一个有向边,包含目标节点和边的权重
type Edge struct {
    target int
    weight int
}

// Graph 表示一个有向图
type Graph struct {
    adjList map[int][]Edge
}

// NewGraph 创建一个新的空图
func NewGraph() *Graph {
    return &Graph{adjList: make(map[int][]Edge)}
}

// AddEdge 添加一条有向边
func (g *Graph) AddEdge(source, target, weight int) {
    g.adjList[source] = append(g.adjList[source], Edge{target, weight})
}

实现优先队列

Dijkstra 算法需要不断选择当前距离最小的节点,因此我们使用优先队列来优化选择过程。Golang 的 container/heap 包可以帮助我们实现最小堆。


type Item struct {
    node     int
    priority int
}

// PriorityQueue 实现了一个优先队列
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
    return pq[i].priority < pq[j].priority
}
func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] }

func (pq *PriorityQueue) Push(x interface{}) {
    *pq = append(*pq, x.(*Item))
}

func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    *pq = old[0 : n-1]
    return item
}
    

Dijkstra 算法实现

有了上述数据结构,我们可以实现 Dijkstra 算法来寻找最短路径。


func (g *Graph) Dijkstra(start int) map[int]int {
    distances := make(map[int]int)
    for node := range g.adjList {
        distances[node] = math.MaxInt64
    }
    distances[start] = 0

    pq := &PriorityQueue{}
    heap.Init(pq)
    heap.Push(pq, &Item{node: start, priority: 0})

    for pq.Len() > 0 {
        current := heap.Pop(pq).(*Item)
        currentNode := current.node

        for _, edge := range g.adjList[currentNode] {
            newDist := distances[currentNode] + edge.weight
            if newDist < distances[edge.target] {
                distances[edge.target] = newDist
                heap.Push(pq, &Item{node: edge.target, priority: newDist})
            }
        }
    }

    return distances
}

示例代码:寻找最短路径

假设我们有以下有向图:

节点 1 到节点 2,权重 4 节点 1 到节点 3,权重 2 节点 2 到节点 3,权重 5 节点 2 到节点 4,权重 10 节点 3 到节点 4,权重 3 我们可以使用以下代码测试 Dijkstra 算法的效果。

func main() {
    g := NewGraph()
    g.AddEdge(1, 2, 4)
    g.AddEdge(1, 3, 2)
    g.AddEdge(2, 3, 5)
    g.AddEdge(2, 4, 10)
    g.AddEdge(3, 4, 3)

    startNode := 1
    distances := g.Dijkstra(startNode)

    fmt.Printf("从节点 %d 到其他节点的最短路径:\n", startNode)
    for node, distance := range distances {
        fmt.Printf("到节点 %d 的距离: %d\n", node, distance)
    }
}

运行该代码,输出应为:


从节点 1 到其他节点的最短路径:
到节点 1 的距离: 0
到节点 2 的距离: 4
到节点 3 的距离: 2
到节点 4 的距离: 5

完整代码

下面是完整的 Golang 代码,包含了上述所有部分:

package main

import (
    "container/heap"
    "fmt"
    "math"
)

// Edge 代表一个有向边,包含目标节点和边的权重
type Edge struct {
    target int
    weight int
}

// Graph 表示一个有向图
type Graph struct {
    adjList map[int][]Edge
}

// NewGraph 创建一个新的空图
func NewGraph() *Graph {
    return &Graph{adjList: make(map[int][]Edge)}
}

// AddEdge 添加一条有向边
func (g *Graph) AddEdge(source, target, weight int) {
    g.adjList[source] = append(g.adjList[source], Edge{target, weight})
}

// Item 代表优先队列中的一个项
type Item struct {
    node     int // 当前节点
    priority int // 节点到起点的距离(优先级)
}

// PriorityQueue 实现了一个优先队列
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
    return pq[i].priority < pq[j].priority // 小顶堆:优先级越小越靠前
}
func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] }

func (pq *PriorityQueue) Push(x interface{}) {
    *pq = append(*pq, x.(*Item))
}

func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    *pq = old[0 : n-1]
    return item
}

// Dijkstra 算法实现
func (g *Graph) Dijkstra(start int) map[int]int {
    // 初始化距离表,将所有节点的距离初始化为无穷大
    distances := make(map[int]int)
    for node := range g.adjList {
        distances[node] = math.MaxInt64
    }
    distances[start] = 0

    // 初始化优先队列,并将起点入队
    pq := &PriorityQueue{}
    heap.Init(pq)
    heap.Push(pq, &Item{node: start, priority: 0})

    for pq.Len() > 0 {
        current := heap.Pop(pq).(*Item)
        currentNode := current.node

        // 遍历当前节点的邻接节点,更新其距离
        for _, edge := range g.adjList[currentNode] {
            newDist := distances[currentNode] + edge.weight
            if newDist < distances[edge.target] {
                distances[edge.target] = newDist
                heap.Push(pq, &Item{node: edge.target, priority: newDist})
            }
        }
    }

    return distances
}

func main() {
    // 构建图
    g := NewGraph()
    g.AddEdge(1, 2, 4)
    g.AddEdge(1, 3, 2)
    g.AddEdge(2, 3, 5)
    g.AddEdge(2, 4, 10)
    g.AddEdge(3, 4, 3)

    // 使用 Dijkstra 算法寻找从起点 1 到其他节点的最短路径
    startNode := 1
    distances := g.Dijkstra(startNode)

    // 输出结果
    fmt.Printf("从节点 %d 到其他节点的最短路径:\n", startNode)
    for node, distance := range distances {
        fmt.Printf("到节点 %d 的距离: %d\n", node, distance)
    }
}

总结

通过 Golang 实现 Dijkstra 算法,我们可以高效地计算有向图中的最短路径。此算法在实际应用中有着重要的意义,例如在地图导航、网络路由优化等场景中都可以使用它来处理复杂的路径规划问题。

参考资料

算法导论 (Introduction to Algorithms) - Thomas H. Cormen 等著
Golang 官方文档