警告
本文最后更新于 2020-03-16,文中内容可能已过时。
链表是一种一对一的关系,树是一种一对多的关系,图则是一种多对多的关系。实际上,我们可以将链表和树都看作图的一部分。
1. 图的定义
用 V(Vertex) 表示顶点的集合,用 E(Edge) 表示边的集合,则图可以看作由一个非空的有限顶点集合 V 和一个有限边的集合 E 组成,记作G(V, E)。其中
- 边可以表示为顶点对:(v, w) ∈ E,其中 v, w ∈ V
- 无向边使用小括号包含两个顶点来表示,如上一条所示,有向边可以用 <v, w> 表示
- 不考虑重边和自回路(这样的图称为简单图,我们只考虑这种图)
一些概念解释如下
概念 | 解释 |
---|
弧(Arc) | 边的另一种称呼 |
无向图(Digraph) | 图中所有的边没有特定的指向 |
有向图(Undigraph) | 图中所有的边是有向的 |
完全图 | 任意两个顶点间都有边相连 |
权(Weight) | 与图的边有关的数,可能表示顶点的距离或花费 |
顶点的度(Degree) | 对无向图,顶点所连接的边的数量 |
顶点的入度(Indegree) | 对有向图,指向顶点的边的数量 |
顶点的出度(Outdegree) | 对有向图,从顶点出发的边的数量 |
路径(Path) | 从一个顶点到另一个顶点的顶点序列 |
路径长度 | 路径上边的数目 |
连通图 | 从一个顶点开始,可以到达图中任意一个其它顶点 |
非连通图 | 图中存在不可达的顶点 |
连通分量 | 对非连通图,它的极大连通子图称为连通分量 |
网 | 带权的连通图 |
关于图的操作集有很多,但最基本的如下
- Create():建立并返回空图
- InsertVertex(Graph G, Vertex V):将顶点 V 插入图 G
- InsertEdge(Graph G, Edge E):将边 E 插入图 G
- DFS(Graph G, Vertex V):从顶点 V 出发深度优先遍历图 G
- BFS(Graph G, Vertex V):从顶点 V 出发广度优先遍历图 G
- ShortestPath(Graph G, Vertex V, int Dist[]):计算图 G 中顶点 V 到任意其它顶点的最短路径
- MST(Graph G):计算图的最小生成树
2. 图的表示
图的表示有很多种方法,包括邻接矩阵、邻接表、十字链表和多重邻接表,最常用的是邻接矩阵和邻接表。
2.1 邻接矩阵
通过邻接矩阵$G[N] [N]$表示图,首先将 N 个顶点从0到 N-1 编号,然后按如下公式填入数值。即如果两个顶点有边连接,填入1,如果没有边,则填入 0
$$
G[N][N] = \begin{cases} 1& 若<v_i,v_j>是G中的边 \\ 0& 否则 \end{cases}
$$
下面是一个无向图的邻接矩阵表示
实际编程时,通常使用二维数组的形式存储。对于无向图而言,邻接矩阵是对称的,因此通过只存储下三角矩阵或上三角矩阵的形式,可以节省一半的存储空间(矩阵压缩)。无向图的度是对应行(或列)非0元素的个数。
对于有向图来讲,邻接矩阵并不是对称的,因此不能采用这种方式。有向图对应行非0元素的个数是「出度」,对应列非0元素的个数是「入读」。
以上我们谈到的都是无权图,如果是有权图,如果两个顶点有边连接,填入边的权值,如果没有边连接,为$\infty$
如果是稠密图(边很多),使用邻接矩阵比较合适。如果是稀疏图(点很多而边很少),存在大量的无效元素,使用邻接矩阵会浪费大量的存储空间。
邻接矩阵结构可以定义为
1
2
3
4
5
6
|
type Graph struct {
VNum,ENUM int // 顶点和边的个数
Vertex []int // 每个顶点的值
AdjMatrix [][]int // 邻接矩阵
}
|
2.2 邻接表
邻接表适用于稀疏图的情况。将所有顶点用一个指针数组$G[N]$表示,每个元素表示一个节点,其值指向该顶点所有相邻顶点构成的链表(顺序不重要,可以随意),一个有向图的邻接表示例如下
邻接表结构可以定义为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| type ENode struct {
V int //顶点编号
Weight int //边的权重(可选,无权图没有这个值)
NextEdge *ENode //指向下一个邻接点
}
type VNode struct {
data int //顶点信息
FirstEdge *Enode //指向第一个邻接点
}
type Graph struct {
VNum,ENum int //顶点和边的个数
AdjList []VNode //存顶点
}
|
邻接表方便寻找任一顶点的所有邻接点,可以节省存储空间,但对有向图无法计算顶点的出度,需要构造「逆邻接表」,上面有向图的逆邻接表如下
2.3 十字链表
十字链表可以看作将图的邻接表和逆邻接表结合的产物。和邻接表相同,图的顶点信息存在顶点数组中,数组元素有三个域:data域,存放与顶点相关的信息;FirstIn域,指向第一条指向它的弧;FirstOut域,指向一个单链表,单链表中存放所有该结点发出的弧。单链表的每个表结点对应一条弧,每个表结点有5个域:vtail和vhead分别是该弧两个顶点在图中的位置,weight存储弧的权重(可选),vtail指向同一弧尾的下一条弧,vhead指向同一弧头的下一条弧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| type ENode struct {
vtail,vhead int //弧尾和弧头顶点编号
Weight int //边的权重(可选,无权图没有这个值)
nexttail,nexthead *ENode //指向同弧尾和同弧头的弧结点
}
type VNode struct {
data int //顶点信息
FirstIn,FirstOut *Enode //指向第一个邻接点
}
type Graph struct {
VNum,ENum int //顶点和边的个数
AdjList []VNode //存顶点
}
|
一个十字链表如下图所示,A只有出弧没有入弧,所以第一个指针为nil,第二个指针指向弧<A, B>。弧结点<A, B>没有同弧尾的结点,即除了A没有其它结点指向B,所以第一个指针为nil,但同弧头的还有弧结点<A, C>。这里的同弧头和同弧尾都是相对于弧来说的,因此,对弧结点<A, C>,同弧尾的还有<B, C>,但同弧头的到此为止。
可以看出,基本结构是邻接表结构,只是添加了一个额外的指针域。
2.4 邻接多重表
邻接多重表是邻接表的一种改进,只适用于无向图。在此存储结构中,图的顶点信息存放在顶点数组中,数组元素有两个域:data域,存放与顶点相关的信息;FirstEdge域,指向一个单链表,此单链表存储所有依附于该顶点的边的信息。这些单链表的一个表结点对应一条边,表结点有4个域:vexi和vexj分别存放该边两个顶点在图中的位置;nexti 指向下一条依附于顶点vexi的边对应的表结点,nextj 指向下一条依附于顶点vexj的边对应的表结点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| type ENode struct {
vexi,vexj int //边的两个顶点
nexti,nextj *ENode //两个顶点所依附的下一条边
}
type VNode struct {
data int //顶点信息
FirstEdge *Enode //指向第一条边
}
type Graph struct {
VNum,ENum int //顶点和边的个数
AdjList []VNode //存顶点
}
|
一个示例如下
3. 图的构建
我们以邻接矩阵形式存储,定义图的结构体如下
1
2
3
4
5
| type Graph struct {
VNum int // the number of Vertices
ENum int // the numver of Edges
AdjMatrix [][]int // adjacency matrix
}
|
为了方便测试,不建立 CreateVertex() 和 CreateEdge() 函数,而是直接对结构体进行初始化从而创建图,创建了一个无向图和一个有向图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| func CreateUndirectedGraph() *Graph {
g := &Graph{}
g.VNum, g.ENum = 6, 6
for i := 0; i < 7; i++ {
//为了便于操作和理解,从下标为1开始
g.AdjMatrix = append(g.AdjMatrix, make([]int, 7))
}
g.AdjMatrix[1][2], g.AdjMatrix[1][3], g.AdjMatrix[1][4] = 1, 1, 1
g.AdjMatrix[2][1], g.AdjMatrix[2][5] = 1, 1
g.AdjMatrix[3][1], g.AdjMatrix[3][5] = 1, 1
g.AdjMatrix[4][1] = 1
g.AdjMatrix[5][2], g.AdjMatrix[5][3], g.AdjMatrix[5][6] = 1, 1, 1
g.AdjMatrix[6][5] = 1
return g
}
// 初始化一个图,顶点和边的数量、权值都预设好
func CreateDirectedGraph() *Graph {
g := &Graph{}
g.VNum, g.ENum = 7, 12
for i := 0; i < 8; i++ {
//为了便于操作和理解,从下标为1开始
g.AdjMatrix = append(g.AdjMatrix, make([]int, 8))
}
g.AdjMatrix[1][2], g.AdjMatrix[1][4] = 2, 1
g.AdjMatrix[2][4], g.AdjMatrix[2][5] = 3, 10
g.AdjMatrix[3][1], g.AdjMatrix[3][6] = 4, 5
g.AdjMatrix[4][3], g.AdjMatrix[4][5], g.AdjMatrix[4][6], g.AdjMatrix[4][7] = 2, 2, 8, 4
g.AdjMatrix[5][7] = 6
g.AdjMatrix[7][6] = 1
return g
}
|
4. 图的遍历
有深度优先(Depth First Search, DFS)和广度优先(Breadth First Search, BFS)两种,前者类似于树的先序遍历,后者类似于树的层次遍历。下面图的遍历算法以下图为例
4.1 深度优先遍历
递归解法的程序实现如下
1
2
3
4
5
6
7
8
9
10
| func DepthFirstSearch(g *Graph, vertex int, result []int) []int {
g.AdjMatrix[0][vertex] = 1
result = append(result, vertex)
for k, v := range g.AdjMatrix[vertex] {
if v > 0 && g.AdjMatrix[0][k] != 1 {
result = DepthFirstSearch(g, k, result)
}
}
return result
}
|
以结点0为入口,深度优先的遍历结果为[0 1 4 2 5 3]
4.2 广度优先遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| func BreadthFirstSearch(g *Graph, vertex int) []int {
result := []int{}
g.AdjMatrix[0][vertex] = 1
queue := list.New()
queue.PushBack(vertex)
for queue.Len() != 0 {
vertex := queue.Remove(queue.Front()).(int)
result = append(result, vertex)
for k, v := range g.AdjMatrix[vertex] {
if v > 0 && g.AdjMatrix[0][k] != 1 {
g.AdjMatrix[0][k] = 1
queue.PushBack(k)
}
}
}
return result
}
|
以结点0为入口,广度优先的遍历结果为[0 1 2 3 4 5]
5. 最短路径
最短路径问题可以抽象为:在网络中,求两个不同顶点之间的所有路径中,边的权值之和最小的那一条路径,这条路径就是两点之间的最短路径(Shortest Path)。其中,第一个顶点为源点(Source),最后一个顶点为终点(Destination)。
最短路径问题不是一个单独的问题,而是一系列问题的综合,包括
- 单源最短路径问题:从某固定源点出发,求其到所有其它顶点的最短路径
- 多源最短路径问题:求任意两顶点间的最短路径
最短路径使用的示例图如下
5.1 单源最短路径
在理解最短路径算法前有两个问题需要注意
- 无向图和有向图都适用,虽然多数示例是有向图
- 无权图是有权图的特例(权值为1),因此不单独介绍
- 图中不可以存在权值为负的边,否则 Dijkstra(迪杰斯特拉)算法不起作用
如第3条所述,单源最短路径的典型算法称为 Dijkstra(迪杰斯特拉)算法,算法的基本思想为以起始点为中心层层向外扩展,直到扩展到终点为止。因此,该算法和广度优先搜索有一定的相似性。
输入上面的示例图,Dijkstra算法的输出为:[0 2 3 1 3 6 5]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| func DijkstraShortestPath(g *Graph, vertex int) {
count := 1 // 已收录的顶点数目,用于控制循环
find := make([]bool, g.VNum+1) //标记已访问过的结点
prevVertex := make([]int, g.VNum+1) //当前节点的前驱结点
distance := make([]int, g.VNum+1) //当前结点的最短路径
//初始化
for i := 1; i <= g.VNum; i++ {
if g.AdjMatrix[vertex][i] > 0 {
distance[i] = g.AdjMatrix[vertex][i]
prevVertex[i] = vertex
} else {
distance[i] = MAX_INT
prevVertex[i] = -1
}
}
distance[vertex] = 0
find[vertex] = true
v, d := vertex, 0 //用来迭代顶点的变量和初始距离
for count < g.VNum {
d = MAX_INT
for i := 1; i <= g.VNum; i++ {
if !find[i] && distance[i] < d {
d = distance[i]
v = i
}
}
find[v] = true
count++
for k, t := range g.AdjMatrix[v] {
if !find[k] && t > 0 && distance[v]+t < distance[k] {
distance[k] = distance[v] + t
prevVertex[k] = v
}
}
}
fmt.Println(distance[1:])
}
|
5.2 多源最短路径
多源最短路径使用Floyd算法,这是一个经典的动态规划算法,核心思想是:从任意节点 i 到任意节点 j 的最短路径不外乎2种可能,1是直接从 i 到 j,2是从 i 经过若干个节点 k 到 j。所以,我们假设 Distance(i,j) 为节点u到节点v的最短路径的距离,对于每一个节点k,我们检查 Distance(i,k) + Distance(k,j) < Distance(i,j) 是否成立,如果成立,证明从 i 到 k 再到 j 的路径比 i 直接到 j 的路径短,我们便设置Distance(i,j) = Distance(i,k) + Distance(k,j),这样一来,当我们遍历完所有节点 k,Distance(i,j) 中记录的便是 i 到 j 的最短路径的距离。
整个过程可以描述为两个步骤
- 从任意一条单边路径开始,所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
- 对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短,如果是,更新它。
程序实现如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| func FloydShortestPath(g *Graph, vertex int) {
var D [][]int
var path [][]int
var i, j, k int
for i := 0; i < g.VNum; i++ {
D = append(D, make([]int, g.VNum))
path = append(path, make([]int, g.VNum))
}
for i = 1; i <= g.VNum; i++ {
for j = 1; j < g.VNum; j++ {
D[i][j] = g.AdjMatrix[i][j]
path[i][j] = -1
}
}
for k = 1; k <= g.VNum; k++ {
for i = 0; i < g.VNum; i++ {
if D[i][k]+D[k][i] < D[i][j] {
D[i][j] = D[i][k] + D[k][j]
path[i][j] = k
}
}
}
}
|
6. 拓扑排序
如果图中从 v 到 w 有一条有向路径,则 v 一定排在 w 之前。满足此条件的顶点序列称为一个拓扑序,获得一个拓扑序的过程就是拓扑排序。
一个最典型的例子是排课表,一个专业很多课程都有先修课,因此排课时必须考虑先修课的存在,以每门课程为结点,若课程间存在先修课关系则有边,这样构成的网络叫做AOV(Activity On Vertex)网,也是拓扑排序使用的网络。
拓扑排序用一句话描述就是「每次删除入度为0的顶点并输出它」,以下图为例,拓扑排序的结果为:V1,V2,V3,V4,V5。拓扑排序的结果是不唯一的。
拓扑排序必定是一个有向无环图(DAG),因此,该算法也可以用于判断一个图是否为有向无环图。程序实现如下,返回的result是拓扑排序结果,ve是关键路径需要用到的事件最早发生时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| func TopologicalSort(g *Graph) ([]int, []int) {
result := make([]int, 1) //拓扑排序的结果数组
ve := make([]int, g.VNum+1)
count := 0 //判断图中是否有环
//计算各结点的入度并存储
indegree := make([]int, g.VNum+1)
for i := 1; i <= g.VNum; i++ {
for j := 1; j <= g.VNum; j++ {
if g.AdjMatrix[i][j] > 0 {
indegree[j]++
}
}
}
queue := list.New()
//入度为0的结点入队
for i := 1; i <= g.VNum; i++ {
if indegree[i] == 0 {
queue.PushBack(i)
}
}
for queue.Len() != 0 {
vertex := queue.Remove(queue.Front()).(int)
result = append(result, vertex)
count++
for k, v := range g.AdjMatrix[vertex] {
if v > 0 {
indegree[k]--
if indegree[k] == 0 {
queue.PushBack(k)
}
if ve[vertex]+v > ve[k] {
ve[k] = ve[vertex] + v
}
}
if v == 0 {
if ve[vertex]+v > ve[k] {
ve[k] = ve[vertex] + v
}
}
}
}
if count != g.VNum {
fmt.Println("This is a DAG!")
return nil, nil
}
return result, ve
}
|
7. 关键路径
拓扑排序应用在AOV网络上,每个顶点表示一个活动或任务。如果每条边表示一个活动或任务,就是AOE(Activity On Edge)网络,多用在安排一个庞大生产流程的工序上,工序之间有先后关系。
如下图所示,在AOE网络中,事件 i 发生后,其后继活动 a(i,*) 都可以开始,但只有所有先导活动 a( *,j ) 都结束后,事件 j 才可以发生。
假设一个工程的 AOE 网如下,最常求的就是 a) 整个工程完工需要多长时间? b) 哪些活动影响工程进度?或求关键路径。图中的虚线表示事件有先后关系,但是这个活动不存在。
对事件(顶点)i,令最早发生时间为 ve(i),最晚发生时间为 vl(i);
对活动(边)a(i,j),令最早开始时间为 e(i,j),最晚开始时间为 l(i,j)。
那么整个工程的完工时间就是终点的最早发生时间;关键路径就是路径长度最长的路径。求关键路径的算法如下:
- 将所有顶点进行拓扑排序;
- 计算 ve(j), $ve(j) = max{ve() + a(,j)}$ ,其中*为任意前驱事件,有ve(1) = 0;
- 计算 vl(i), $vl(i) = min{vl() - a(i,)}$ ,其中*为任意后继事件,有vl(n) = ve(n);
- 计算 e(i,j) 和 l(i,j),$e(i,j) = ve(i)$,$l(i,j) = vl(j)-a(i,j)$
- 结论:工程总用时 ve(n),关键活动是 e(i,j) = l(i,j) 的活动 a(i,j)
如果只求工程总用时,那么只需要第1,2步。关于两个核心公式可以这样理解:事件 j 在所有前驱活动都完成后发生,所以其最早发生时间为 $ve(j) = max{ve() + a(,j)}$ ,即取决于最慢的前驱活动。另一方面,事件 i 发生后所有后继活动都可以开始了,所以其最晚发生时间为 $vl(i) = min{vl() - a(i,)}$,即不耽误最慢的后继活动。
简单理解的话,就是按照拓扑有序排列顶点,然后从前往后计算事件的最早发生时间得到总时间,再从后往前计算事件的最晚发生时间,最后计算活动的最早和最晚开始时间得到关键活动和关键路径。求上面示例图的关键路径过程如下表
事件 | 最早发生时间ve | 最晚发生时间vl | 活动 | 最早开始时间e | 最晚开始时间l |
---|
v1 | 0 | 0 | a(1,2) | 0 | 0 |
v2 | 6 | 6 | a(1,3) | 0 | 2 |
v3 | 4 | 6 | a(1,4) | 0 | 1 |
v4 | 5 | 6 | a(2,5) | 6 | 6 |
v5 | 7 | 7 | a(3,5) | 4 | 6 |
v6 | 7 | 7 | a(4,6) | 5 | 6 |
v7 | 12 | 13 | a(5,6) | 7 | 7 |
v8 | 11 | 11 | a(5,7) | 7 | 8 |
v9 | 15 | 15 | a(5,8) | 7 | 8 |
| | | a(6,8) | 7 | 7 |
| | | a(7,9) | 12 | 13 |
| | | a(8,9) | 11 | 11 |
最终得到工程完工需要时间为15,关键路径是 1,2,5,6,8,9
程序实现如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| func CriticalPath(g *Graph) (int, []int) {
path, ve := TopologicalSort(g)
if len(path) == 1 || path == nil {
return 0, nil
}
vl := make([]int, len(path))
for i := 1; i < len(vl); i++ {
vl[i] = MAX_INT
}
vl[len(ve)-1] = ve[len(ve)-1]
for i := len(vl) - 2; i > 0; i-- {
for k, v := range g.AdjMatrix[i] {
if v >= 0 && vl[k]-v < vl[i] {
vl[i] = vl[k] - v
}
}
}
result := []int{}
for i := 1; i < g.VNum+1; i++ {
for j := 1; j < g.VNum+1; j++ {
if g.AdjMatrix[i][j] >= 0 {
if ve[i] == vl[j]-g.AdjMatrix[i][j] {
result = append(result, i)
}
}
}
}
result = append(result, path[len(path)-1])
return ve[len(ve)-1], result
}
|
8. 最小生成树
生成树指包含全部顶点且树的 V-1 条边全部在图里的树,其中 V 为顶点数目。最小生成树(Minimum Spanning Tree)就是边的权重和最小的生成树。需要注意两点
- 向生成树中任加一条边都一定会构成回路
- 最小生成树存在等价于图连通
生成最小生成树最常见的有 Prim 和 Kruskal 两种算法,这两种都是贪心算法。
8.1 Kruskal算法
算法的核心思想用一句话描述就是「不构成环的情况下,每次选取最小的边」,最小边的选取可以使用最小堆,环的判断可以使用并查集。
代码实现如下,最小堆的实现使用了 标准库中的container/heap,usetFind是并查集的查找函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| func usetFind(x int, uset []int) int {
for x != uset[x] {
x = uset[x]
}
return x
}
func KruskalMiniSpanTree(g *Graph) (int, []int) {
var total int
result := []Edge{}
h := &Heap{}
heap.Init(h)
for i := 1; i < g.VNum+1; i++ {
for j := i; j < g.VNum+1; j++ {
if g.AdjMatrix[i][j] > 0 {
heap.Push(h, Edge{i, j, g.AdjMatrix[i][j]})
}
}
}
uset := make([]int, g.VNum+1) //用数组表示并查集
for i := 1; i < len(uset); i++ {
uset[i] = i
}
for h.Len() != 0 {
e := heap.Pop(h).(Edge)
if usetFind(e.from, uset) != usetFind(e.to, uset) {
result = append(result, e)
uset[uset[e.to]] = uset[e.from]
total += e.weight
}
}
return total, uset
}
|
8.2 Prim算法
记 V 是联通网的顶点集,U 是求得的生成树的顶点集,TE 是求得的生成树的边集。普利姆算法步骤如下
- 开始时,$U={v_0}, TE = \emptyset$
- 计算 U 到其余顶点 V-U 的最小代价,将该顶点纳入 U,边纳入TE
- 重复第二步直到 U=V
一个例子如下
代码实现如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| func PrimMiniSpanTree(g *Graph, start int) (int, []int) {
total := 0
parent := make([]int, g.VNum+1)
dist := make([]int, g.VNum+1)
parent[start] = -1
for i := 1; i < len(dist); i++ {
if i == start {
continue
}
if g.AdjMatrix[start][i] > 0 {
dist[i] = g.AdjMatrix[start][i]
} else {
dist[i] = MAX_INT
}
}
count := 1
vertex, mini := start, MAX_INT
for count < g.VNum {
mini = MAX_INT
for i := 1; i < len(dist); i++ {
if dist[i] != 0 && dist[i] < mini {
vertex, mini = i, dist[i]
}
}
total += dist[vertex]
dist[vertex] = 0
count++
for k, t := range g.AdjMatrix[vertex] {
if dist[k] != 0 && t > 0 && t < dist[k] {
dist[k] = t
parent[k] = vertex
}
}
}
return total, parent
}
|
8.3 两种算法比较
Kruskal的算法时间复杂度为$O(eloge)$,只和边的数目 e 有关,与顶点个数 n 无关,适用于稀疏图
Prim算法时间复杂度为$O(n^2)$,只和顶点个数 n 有关,与边的数目 e 无关,适用于稠密图