图的表示:邻接表 vs 邻接矩阵
| 表示方式 | 空间 | 查边是否存在 | 遍历所有邻居 | 适用场景 |
|---|---|---|---|---|
| 邻接矩阵 | O(V²) | O(1) | O(V) | 稠密图、Floyd 算法 |
| 邻接表 | O(V+E) | O(degree) | O(degree) | 稀疏图(大多数实际图) |
口诀稀疏图用邻接表,稠密图用邻接矩阵。现实中的图几乎都是稀疏的。
package main
import (
"container/heap"
"fmt"
"math"
)
// 图的表示(邻接表,加权有向图)
type Graph struct {
adj map[int][]Edge
}
type Edge struct{ to, weight int }
func NewGraph() *Graph {
return &Graph{adj: map[int][]Edge{}}
}
func (g *Graph) AddEdge(from, to, weight int) {
g.adj[from] = append(g.adj[from], Edge{to, weight})
}
// Dijkstra 最短路径:O((V+E) log V)
func (g *Graph) Dijkstra(src int) map[int]int {
dist := map[int]int{}
for v := range g.adj {
dist[v] = math.MaxInt64
}
dist[src] = 0
pq := &PQ{{0, src}}
heap.Init(pq)
for pq.Len() > 0 {
item := heap.Pop(pq).(PQItem)
u := item.node
if item.dist > dist[u] {
continue // 过时的记录
}
for _, e := range g.adj[u] {
if newDist := dist[u] + e.weight; newDist < dist[e.to] {
dist[e.to] = newDist
heap.Push(pq, PQItem{newDist, e.to})
}
}
}
return dist
}
type PQItem struct{ dist, node int }
type PQ []PQItem
func (pq PQ) Len() int { return len(pq) }
func (pq PQ) Less(i, j int) bool { return pq[i].dist < pq[j].dist }
func (pq PQ) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] }
func (pq *PQ) Push(x interface{}) { *pq = append(*pq, x.(PQItem)) }
func (pq *PQ) Pop() interface{} { old := *pq; n := len(old); x := old[n-1]; *pq = old[:n-1]; return x }
func main() {
g := NewGraph()
g.AddEdge(0, 1, 4); g.AddEdge(0, 2, 1)
g.AddEdge(2, 1, 2); g.AddEdge(1, 3, 1); g.AddEdge(2, 3, 5)
dist := g.Dijkstra(0)
fmt.Println("从节点 0 出发的最短路径:", dist)
// map[0:0 1:3 2:1 3:4]
}
拓扑排序:依赖解析的数学
package main
import "fmt"
// Kahn 算法拓扑排序(BFS 版)
// 应用:包管理依赖安装顺序、编译依赖、任务调度
func topoSort(n int, edges [][2]int) ([]int, bool) {
inDegree := make([]int, n)
adj := make([][]int, n)
for _, e := range edges {
adj[e[0]] = append(adj[e[0]], e[1])
inDegree[e[1]]++
}
queue := []int{}
for i, d := range inDegree {
if d == 0 {
queue = append(queue, i)
}
}
order := []int{}
for len(queue) > 0 {
u := queue[0]
queue = queue[1:]
order = append(order, u)
for _, v := range adj[u] {
inDegree[v]--
if inDegree[v] == 0 {
queue = append(queue, v)
}
}
}
if len(order) != n {
return nil, false // 有环,无法拓扑排序
}
return order, true
}
func main() {
// 包依赖:A→C,B→C,C→D(A、B 依赖 C,C 依赖 D)
edges := [][2]int{{0, 2}, {1, 2}, {2, 3}}
order, ok := topoSort(4, edges)
fmt.Println(ok, order) // true [0 1 2 3] 或 [1 0 2 3]
// 有环图
cyclic := [][2]int{{0, 1}, {1, 2}, {2, 0}}
_, ok2 := topoSort(3, cyclic)
fmt.Println("有环:", !ok2) // 有环: true
}
并查集:连通分量的利器
package main
import "fmt"
// 并查集(路径压缩 + 按秩合并)
type UnionFind struct {
parent, rank []int
}
func NewUF(n int) *UnionFind {
parent := make([]int, n)
for i := range parent { parent[i] = i }
return &UnionFind{parent, make([]int, n)}
}
func (uf *UnionFind) Find(x int) int {
if uf.parent[x] != x {
uf.parent[x] = uf.Find(uf.parent[x]) // 路径压缩
}
return uf.parent[x]
}
func (uf *UnionFind) Union(x, y int) {
rx, ry := uf.Find(x), uf.Find(y)
if rx == ry { return }
if uf.rank[rx] < uf.rank[ry] { rx, ry = ry, rx }
uf.parent[ry] = rx
if uf.rank[rx] == uf.rank[ry] { uf.rank[rx]++ }
}
func main() {
uf := NewUF(6)
uf.Union(0, 1); uf.Union(1, 2); uf.Union(3, 4)
fmt.Println(uf.Find(0) == uf.Find(2)) // true(同一连通分量)
fmt.Println(uf.Find(0) == uf.Find(3)) // false(不同分量)
}
✓推荐做法
- 无权最短路用 BFS,有权正边用 Dijkstra,有负边用 Bellman-Ford
- 依赖解析、任务调度先判断是否有环(拓扑排序可行性检查)
- 连通性查询用并查集,O(α(n)) ≈ O(1)
✗不推荐
- Dijkstra 用于有负权边的图——用 Bellman-Ford 或 SPFA
- 对稀疏图使用邻接矩阵——浪费内存
⚠常见误区
- BFS 找最短路时必须标记已访问节点,否则会死循环
判断标准:能用拓扑排序检测循环依赖,并解释 Dijkstra 为什么不能处理负权边 → 掌握本章。
图论是关系的数学——世界上所有的网络,都是图。
— 图论与应用