第7章 图
图(Graph) 是由顶点的有穷非空集合和顶点之间边的集合组成, 通常表示为G(V,E), 其中, G表示一个图, V是图G中顶点的集合, E是图G中边的集合(注意有穷非空)
7.1 开场白
7.2 图的定义
-
三种结构的区分
-
线性表: 数据元素之间是被串起来的, 仅有线性关系, 每个数据元素只有一个直接前驱和一个直接后继
-
树:数据元素之间有着明显的层次关系, 并且每一层上的数据元素可能和下一层中多个元素相关, 但只能和上一层中一个元素相关
-
图:是一种较线性表和树更加复杂的数据结构, 结点之间的关系可以是任意的, 图中任意两个数据元素之间都可能相关
-
-
三种结构的注意事项
- 数据元素:线性表称为元素, 树称为结点, 图称为顶点
- 是否为空:线性表可有空表。 树可有空树。 图没有空图(图的定义是有穷非空)
- 元素关系:线性表中相邻的数据元素之间具有线性关系, 树中相邻两层的结点具有层次关系, 图中任意两个顶点之间都可能有关系, 顶点之间的逻辑关系用边来表示, 边集可以是空的。
7.2.1 各种图定义
-
无向边:若顶点vi到vj之间的边没有方向, 则称这条边为无向边 , 用无序偶对(vi,vj)来表示。 如果图中任意两个顶点之间的边都是无向边, 则称该图为无向图 。 下图是一个无向图,连接顶点A与D的边, 可以表示成无序对(A,D), 也可以写成(D,A)。该图可表示为 G1=(V1,{E1}), 其中顶点集合V1={A,B,C,D}; 边集合E1={(A,B),(B,C),(C,D),(D,A),(A,C)} ,
如果任意两个顶点之间都存在边, 则称该图为无向完全图,含有n个顶点的无向完全图有n(n-1)/2条边。
-
有向边: 若从顶点vi到vj的边有方向, 则称这条边为有向边, 也称为弧 。 用有序偶<vi,vj>来表示, vi称为弧尾 , vj称为弧头 。 如果图中任意两个顶点之间的边都是有向边, 则称该图为有向图。 下图是一个有向图。 连接顶点A到D的有向边就是弧, A是弧尾, D是弧头, <A,D>表示弧, 不能写成<D,A>。 该图可表示为G2=(V2,{E2}), 其中顶点集合V2={A,B,C,D}; 弧集合E2={<A,D>,<B,A>,<C,A>,<B,C>}。
如果任意两个顶点之间都存在方向互为相反的两条弧, 则称该图为有向完全图。 含有n个顶点的有向完全图有n×(n-1)条边,
- 有很少条边或弧的图称为稀疏图, 反之称为稠密图。 这里稀疏和稠密是模糊的概念,都是相对而言的。
- 有些图的边或弧具有与它相关的数字, 这种与图的边或弧相关的数叫做权,这种带权的图通常称为网。
- 子图:
7.2.2 图的顶点与边间关系
-
无向图G=(V,{E}):
- 如果边(v,v’)∈ E, 则称顶点v和v’互为邻接点。
- 边(v,v’)依附于顶点v和v’, 或边(v,v’)与顶点v和v’相关联。
- 顶点v的度是与v相关联的边的数目, 记为TD(v),图的边数其实就是各顶点度数和的一半。
-
有向图G=(V,{E}):
- 如果弧<v,v’>∈ E, 则称顶点v邻接到顶点v’, 顶点v’邻接自顶点v。
- 弧<v,v’>和顶点v,v’相关联
- 以顶点v为头的弧的数目称为v的入度, 记为ID(v); 以v为尾的弧的数目称为v的出度, 记为OD(v); 顶点v的度为TD(v)=ID(v)+OD(v)。 图的边数 = 所有顶点出度和 = 所有顶点入度和
-
无向图G=(V,{E})中从顶点v到顶点v’的路径是一个顶点序列
- 有向图中路径也是有向的,顶点序列应满足
- 路径的长度是路径上的边或弧的数目。 3图中的上方两条路径长度为2, 下方两条路径长度为3。4图中左侧路径长为2, 右侧路径长度为3
7.2.3 连通图相关术语
- 在无向图G中, 如果从顶点v到顶点v’有路径, 则称v和v’是连通的。
- 无向图中的极大连通子图称为连通分量。 注意连通分量的概念, 它强调:
- 要是子图;
- 子图要是连通的;
- 连通子图含有极大顶点数;
- 具有极大顶点数的连通子图包含依附于这些顶点的所有边
- 在有向图G中, 如果对于每一对vi、 vj∈ V、 vi≠vj, 从vi到vj和从vj到vi都存在路径, 则称G是强连通图。 有向图中的极大强连通子图称做有向图的强连通分量。
- 连通图的生成树定义
- 无向图:连通图的生成树是一个极小的连通子图, 它含有图中全部的n个顶点, 但只有足以构成一棵树的n-1条边(如果一个图有n个顶点和小于n-1条边,则是非连通图, 如果它多于n-1边条, 必定构成一个环,而且有n-1条边并不一定是生成树),生成树一定有n-1条边,但有n-1条边不一定是生成树。
- 有向图:只有一个顶点的入度为0, 其余顶点的入度均为1, 则是一个有向树,一个有向图由若干棵有向树构成生成森林。
7.2.4 图的定义与术语总结
- 图按照有无方向分为无向图和有向图。 无向图由顶点和边构成, 有向图由顶点和弧构成。 弧有弧尾和弧头之分。
- 图按照边或弧的多少分稀疏图和稠密图。 如果任意两个顶点之间都存在边叫完全图,有向的叫有向完全图。 若无重复的边或顶点到自身的边则叫简单图。
- 图中顶点之间有邻接点、 依附的概念。 无向图顶点的边数叫做度, 有向图顶点分为入度和出度。
- 图上的边或弧上带权则称为网。
- 图中顶点间存在路径, 两顶点存在路径则说明是连通的, 如果路径最终回到起始点则称为环, 当中不重复叫简单路径。 若任意两顶点都是连通的, 则图就是连通图, 有向则称强连通图。 图中有子图, 若子图极大连通则就是连通分量, 有向的则称强连通分量。
- 无向图中连通且n个顶点n-1条边叫生成树。 有向图中一顶点入度为0其余顶点入度为1的叫有向树。 一个有向图由若干棵有向树构成生成森林。
7.3 图的抽象数据类型
ADT 图(Graph)
Data
顶点的有穷非空集合和边的集合。
Operation
CreateGraph(*G, V, VR): 按照顶点集V和边弧集VR的定义构造图G。
DestroyGraph(*G): 图G存在则销毁。
LocateVex(G, u): 若图G中存在顶点u, 则返回图中的位置。
GetVex(G, v): 返回图G中顶点v的值。
PutVex(G, v, value): 将图G中顶点v赋值value。
FirstAdjVex(G, *v): 返回顶点v的一个邻接顶点, 若顶点在G中无邻接顶点返回空。
NextAdjVex(G, v, *w): 返回顶点v相对于顶点w的下一个邻接顶点,
若w是v的最后一个邻接点则返回“空”。
InsertVex(*G, v): 在图G中增添新顶点v。
DeleteVex(*G, v): 删除图G中顶点v及其相关的弧。
InsertArc(*G, v, w): 在图G中增添弧<v,w>, 若G是无向图, 还需要增添对称弧<w,v>。
DeleteArc(*G, v, w): 在图G中删除弧<v,w>, 若G是无向图, 则还删除对称弧<w,v>。
DFSTraverse(G): 对图G中进行深度优先遍历, 在遍历过程对每个顶点调用。
HFSTraverse(G): 对图G中进行广度优先遍历, 在遍历过程对每个顶点调用。
endADT
7.4 图的存储结构
7.4.1 邻接矩阵
- 一个一维数组存储图中顶点信息, 一个二维数组(称为邻接矩阵) 存储图中的边或弧的信息。
设图G有n个顶点, 则邻接矩阵是一个n×n的方阵, 定义为:
- 无向图
根据矩阵可知的信息(无向图的邻接矩阵是对称矩阵):
- 可判定任意两顶点是否有边。
- 顶点vi的度为在邻接矩阵中第i行(或第i列) 的元素之和。 比如顶点v1的度就是1+0+1+0=2。
- 求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍, arc[i] [j]为1就是邻接点。
- 有向图
根据矩阵可知的信息(有向图的邻接矩阵不是对称矩阵):
- 顶点v1的入度为1, 正好是第v1列各数之和。 顶点v1的出度为2, 即第v1行的各数之和。
- 判断顶点vi到vj是否存在弧, 只需要查找矩阵中arc[i] [j]是否为1即可。 要求vi的所有邻接点就是将矩阵第i行元素扫描一遍, 查找arc[i] [j]为1的顶点。
- 网(∞表示一个计算机允许的、 大于所有边上权值的值, 也就是一个不可能的极限值。 不能为0,原因在于权值wij大多数情况下是正值, 但个别时候可能就是0, 甚至有可能是负值)
- 构造方法
/* 顶点类型应由用户定义 */
typedef char VertexType;
/* 边上的权值类型应由用户定义 */
typedef int EdgeType;
/* 最大顶点数, 应由用户定义 */
#define MAXVEX 100
/* 用65535来代表∞ */
#define INFINITY 65535
typedef struct
{
/* 顶点表 */
VertexType vexs[MAXVEX];
/* 邻接矩阵, 可看作边表 */
EdgeType arc[MAXVEX][MAXVEX];
/* 图中当前的顶点数和边数 */
int numVertexes, numEdges;
} MGraph;
/* 建立无向网图的邻接矩阵表示 */
void CreateMGraph(MGraph *G)
{
int i, j, k, w;
printf("输入顶点数和边数:\n");
/* 输入顶点数和边数 */
scanf("%d,%d", &G->numVertexes, &G->numEdges);
/* 读入顶点信息, 建立顶点表 */
for (i = 0; i < G->numVertexes; i++)
scanf(&G->vexs[i]);
for (i = 0; i < G->numVertexes; i++)
for (j = 0; j <G->numVertexes; j++)
/* 邻接矩阵初始化 */
G->arc[i][j] = INFINITY;
/* 读入numEdges条边, 建立邻接矩阵 */
for (k = 0; k < G->numEdges; k++)
{
printf("输入边(vi,vj)上的下标i, 下标j和权w:\n");
/* 输入边(vi,vj)上的权w */
scanf("%d,%d,%d", &i, &j, &w);
G->arc[i][j] = w;
/* 因为是无向图, 矩阵对称 */
G->arc[j][i] = G->arc[i][j];
}
}
// 从代码中也可以得到, n个顶点和e条边的无向网图的创建, 时间复杂度为O(n+n2+e), 其中对邻接矩阵G.arc的初始化耗费了O(n2)的时间。
7.4.2 邻接表
-
数组与链表相结合的存储方法称为邻接表,减少对于边数相对顶点较少的图的存储空间的极大浪费
-
实现方法
- 图中顶点用数组存储, 每个数据元素还需要存储指向第一个邻接点的指针;
- 图中每个顶点vi的所有邻接点构成一个线性表, 由于邻接点的个数不定, 所以用单链表存储, 无向图称为顶点vi的边表, 有向图则称为顶点vi作为弧尾的出边表。
- 构造方法
/* 顶点类型应由用户定义 */
typedef char VertexType;
/* 边上的权值类型应由用户定义 */
typedef int EdgeType;
/* 边表结点 */
typedef struct EdgeNode
{
/* 邻接点域, 存储该顶点对应的下标 */
int adjvex;
/* 用于存储权值, 对于非网图可以不需要 */
EdgeType weight;
/* 链域, 指向下一个邻接点 */
struct EdgeNode *next;
} EdgeNode;
/* 顶点表结点 */
typedef struct VertexNode
{
/* 顶点域, 存储顶点信息 */
VertexType data;
/* 边表头指针 */
EdgeNode *firstedge;
} VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
/* 图中当前顶点数和边数 */
int numVertexes, numEdges;
} GraphAdjList;
/* 建立图的邻接表结构 */
void CreateALGraph(GraphAdjList *G)
{
int i, j, k;
EdgeNode *e;
printf("输入顶点数和边数:\n");
/* 输入顶点数和边数 */
scanf("%d,%d", &G->numVertexes, &G->numEdges);
/* 读入顶点信息, 建立顶点表 */
for (i = 0; i < G->numVertexes; i++)
{
/* 输入顶点信息 */
scanf(&G->adjList[i].data);
/* 将边表置为空表 */
G->adjList[i].firstedge = NULL;
}/
* 建立边表 */
for (k = 0; k < G->numEdges; k++)
{
printf("输入边(vi,vj)上的顶点序号:\n");
/* 输入边(vi,vj)上的顶点序号 */
scanf("%d,%d", &i, &j);
/* 向内存申请空间, */
/* 生成边表结点 */
e = (EdgeNode *)malloc(sizeof(EdgeNode));
/* 邻接序号为j */
e->adjvex = j;
/* 将e指针指向当前顶点指向的结点 */
e->next = G->adjList[i].firstedge;
/* 将当前顶点的指针指向e */
G->adjList[i].firstedge = e;
/* 向内存申请空间, */
/* 生成边表结点 */
e = (EdgeNode *)malloc(sizeof(EdgeNode));
/* 邻接序号为i */
e->adjvex = i;
/* 将e指针指向当前顶点指向的结点 */
e->next = G->adjList[j].firstedge;
/* 将当前顶点的指针指向e */
G->adjList[j].firstedge = e;
}
}
// 这里应用了头插法, 由于对于无向图, 一条边对应都是两个顶点, 所以在循环中, 一次就针对i和j分别进行了插入。 本算法的时间复杂度, 对于n个顶点e条边来说, 很容易得出是O(n+e)
7.4.3 十字链表
-
那么对于有向图来说, 邻接表的缺陷在于不能同时了解入度和出度。
-
顶点表结构与
-
顶点表结点:其中firstin表示入边表头指针, 指向该顶点的入边表中第一个结点, firstout表示出边表头指针, 指向该顶点的出边表中的第一个结点
-
边表结构:
其中tailvex是指弧起点在顶点表的下标, headvex是指弧终点在顶点表中的下标,headlink是指入边表指针域, 指向终点相同的下一条边, taillink是指边表指针域, 指向起点相同的下一条边。 如果是网, 还可以再增加一个weight域来存储权值。
-
7.4.4 邻接多重表
7.4.5 边集数组
7.5 图的遍历
7.5.1 深度优先遍历
- 深度优先遍历也称为深度优先搜索, 简称为DFS。
- 邻接矩阵代码
/* Boolean是布尔类型, 其值是TRUE或FALSE */
typedef int Boolean;
/* 访问标志的数组 */
Boolean visited[MAX];
/* 邻接矩阵的深度优先递归算法 */
void DFS(MGraph G, int i)
{
int j;
visited[i] = TRUE;
/* 打印顶点, 也可以其他操作 */
printf("%c ", G.vexs[i]);
for (j = 0; j < G.numVertexes; j++)
if (G.arc[i][j] == 1 && !visited[j])
/* 对为访问的邻接顶点递归调用 */
DFS(G, j);
}/
* 邻接矩阵的深度遍历操作 */
void DFSTraverse(MGraph G)
{
int i;
for (i = 0; i < G.numVertexes; i++)
/* 初始所有顶点状态都是未访问过状态 */
visited[i] = FALSE;
for (i = 0; i < G.numVertexes; i++)
/* 对未访问过的顶点调用DFS, 若是连通图, 只会执行一次 */
if (!visited[i])
DFS(G, i);
}
- 邻接表代码
/* 邻接表的深度优先递归算法 */
void DFS(GraphAdjList GL, int i)
{
EdgeNode *p;
visited[i] = TRUE;
/* 打印顶点, 也可以其他操作 */
printf("%c ", GL->adjList[i].data);
p = GL->adjList[i].firstedge;
while (p)
{
if (!visited[p->adjvex])
/* 对为访问的邻接顶点递归调用 */
DFS(GL, p->adjvex);
p = p->next;
}
}/
* 邻接表的深度遍历操作 */
void DFSTraverse(GraphAdjList GL)
{
int i;
for (i = 0; i < GL->numVertexes; i++)
/* 初始所有顶点状态都是未访问过状态 */
visited[i] = FALSE;
for (i = 0; i < GL->numVertexes; i++)
/* 对未访问过的顶点调用DFS, 若是连通图, 只会执行一次 */
if (!visited[i])
DFS(GL, i);
}
- 邻接矩阵二维数组, 要查找每个顶点的邻接点需要访问矩阵中的所有元素, 因此都需要O(n2)的时间
- 而邻接表做存储结构时, 找邻接点所需的时间取决于顶点和边的数量, 所以是O(n+e)。
- 显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高
7.5.2 广度优先遍历
- 广度优先遍历又称为广度优先搜索, 简称BFS。
- 邻接矩阵代码
/* 邻接矩阵的广度遍历算法 */
void BFSTraverse(MGraph G)
{
int i, j;
Queue Q;
for (i = 0; i < G.numVertexes; i++)
visited[i] = FALSE;
/* 初始化一辅助用的队列 */
InitQueue(&Q);
/* 对每一个顶点做循环 */
for (i = 0; i < G.numVertexes; i++)
{
/* 若是未访问过就处理 */
if (!visited[i])
{
/* 设置当前顶点访问过 */
visited[i]=TRUE;
/* 打印顶点, 也可以其他操作 */
printf("%c ", G.vexs[i]);
/* 将此顶点入队列 */
EnQueue(&Q,i);
/* 若当前队列不为空 */
while (!QueueEmpty(Q))
{
/* 将队中元素出队列, 赋值给i */
DeQueue(&Q, &i);
for (j = 0; j < G.numVertexes; j++)
{
/* 判断其他顶点若与当前顶点存在边且未访问过 */
if (G.arc[i][j] == 1 && !visited[j])
{
/* 将找到的此顶点标记为已访问 */
visited[j]=TRUE;
/* 打印顶点 */
printf("%c ", G.vexs[j]);
/* 将找到的此顶点入队列 */
EnQueue(&Q,j);
}
}
}
}
}
}
- 邻接表代码
/* 邻接表的广度遍历算法 */
void BFSTraverse(GraphAdjList GL)
{
int i;
EdgeNode *p;
Queue Q;
for (i = 0; i < GL->numVertexes; i++)
visited[i] = FALSE;
InitQueue(&Q);
for (i = 0; i < GL->numVertexes; i++)
{
if (!visited[i])
{
visited[i] = TRUE;
/* 打印顶点, 也可以其他操作 */
printf("%c ", GL->adjList[i].data);
EnQueue(&Q, i);
while (!QueueEmpty(Q))
{
DeQueue(&Q, &i);
/* 找到当前顶点边表链表头指针 */
p = GL->adjList[i].firstedge;
while (p)
{
/* 若此顶点未被访问 */
if (!visited[p->adjvex])
{
visited[p->adjvex] = TRUE;
printf("%c ", GL->adjList[p->adjvex].data);
/* 将此顶点入队列 */
EnQueue(&Q, p->adjvex);
}/
* 指针指向下一个邻接点 */
p = p->next;
}
}
}
}
}
7.6 最小生成树
定义:一个连通图的生成树是一个极小的连通子图, 它含有图中全部的顶点, 但只有足以构成一棵树的n-1条边,把构造连通网的最小代价生成树称为最小生成树。
7.6.1 普里姆(Prim) 算法
- Prim算法生成最小生成树(MAXVEX = 9,)
void MiniSpanTree_Prim(MGraph G)
{
int min, i, j, k;
int adjvex[MAXVEX]; /* 保存相关顶点下标 */
int lowcost[MAXVEX]; /* 保存相关顶点间边的权值 */
lowcost[0] = 0; /* 初始化第一个权值为0, 即v0加入生成树 ,lowcost的值为0, 在这里就是此下标的顶点已经加入生成树 */
adjvex[0] = 0; /* 初始化第一个顶点下标为0 */
for (i = 1; i < G.numVertexes; i++) /* 循环除下标为0外的全部顶点 */
{
lowcost[i] = G.arc[0][i]; /* 将v0顶点与之有边的权值存入数组 */
adjvex[i] = 0; /* 初始化都为v0的下标 */
}
for (i = 1; i < G.numVertexes; i++)
{
min = INFINITY; /* 初始化最小权值为∞,通常设置为不可能的大数字如32767、 65535等 */
j = 1; k = 0;
while (j < G.numVertexes) /* 循环全部顶点 */
{
if (lowcost[j] != 0 && lowcost[j] < min)
{ /* 如果权值不为0且权值小于min */
min = lowcost[j]; /* 则让当前权值成为最小值 */
k = j; /* 将当前最小值的下标存入k */
}
j++;
}
printf("(%d,%d)", adjvex[k], k); /* 打印当前顶点边中权值最小边 */
lowcost[k] = 0; /* 将当前顶点的权值设置为0, 表示此顶点已经完成任务 */
for (j = 1; j < G.numVertexes; j++) /* 循环所有顶点 */
{
if (lowcost[j] != 0 && G.arc[k][j] < lowcost[j])
{/* 若下标为k顶点各边权值小于此前这些顶点未被加入生成树权值 */
lowcost[j] = G.arc[k][j]; /* 将较小权值存入lowcost */
adjvex[j] = k; /* 将下标为k的顶点存入adjvex */
}
}
}
}
- 第4~5行, 创建两个一维数组lowcost和adjvex, 长度都为顶点个数9。
- 第6~7行,分别给这两个数组的第一个下标位赋值为0, adjvex[0]=0表示从顶点v0开始( 事实上, 最小生成树从哪个顶点开始计算都无所谓, 我们假定从v0开始) , lowcost[0]=0就表示v0已经被纳入到最小生成树中, 之后凡是lowcost数组中的值被设置为0就是表示此下标的顶点被纳入最小生成树。
- 第8~12行,读取邻接矩阵的第一行数据。 将数值赋值给lowcost数组, 所以此时lowcost数组值为
{0,10,65535,65535,65535,11,65535,65535,65535}, 而adjvex则全部为0,完成初始化的工作。 - 第13~36行, 整个循环过程就是构造最小生成树的过程。
- 第15~16行, 将min设置为了一个极大值65535, 它的目的是为了之后找到一定范围内的最小权值。 j是用来做顶点下标循环的变量, k是用来存储最小权值的顶点下标。
- 第17~25行, 循环中不断修改min为当前lowcost数组中最小值, 并用k保留此最小值的顶点下标。 经过循环后, min=10, k=1。(注意19行if判断的lowcost[j]!=0表示已经是生成树的顶点不参与最小权值的查找)
- 第26行, 因k=1, adjvex[1]=0, 所以打印结果为(0,1), 表示v0至v1边为最小生成树的第一条边。
- 第27行, 此时因k=1我们将lowcost[k]=0就是说顶点v1纳入到最小生成树中。 此时lowcost数组值为{0,0,65535,65535,65535,11,65535,65535,65535}。
- 第28~35行, j循环由1至8, 因k=1,查找邻接矩阵的第v1行的各个权值, 与lowcost的对应值比较, 若更小则修改lowcost值, 并将k值存入adjvex数组中。 因第v1行有18、 16、 12均比65535小, 所以最终lowcost数组的值为:{0,0,18,65535,65535,11,16,65535,12}。 adjvex数组的值为: {0,0,1,0,0,0,1,0,1}。 这里第30行if判断的lowcost[j]!=0也说明v0和v1已经是生成树的顶点不参与最小权值的比对了。
- 再次循环, 由第15行到第26行, 此时min=11, k=5, adjvex[5]=0。 因此打 印结构为(0,5)。 表示v0至v5边为最小生成树的第二条边。
11. 接下来执行到36行, lowcost数组的值为: {0,0,18,65535,26,0,16,65535,12}。adjvex数组的值为: {0,0,1,0,5,0,1,0,1}。 通过不断的转换, 得到最终结果。
算法核心:
假设N=(V,{E})是连通网, TE是N上最小生成树中边的集合。 算法从U={u0}(u0∈ V),TE={}开始。 重复执行下述操作: 在所有u∈ U,v∈ V-U的边(u,v)∈ E中找一条代价最小的边(u0,v0)并入集合TE, 同时v0并入U, 直至U=V为止。 此时TE中必有n-1条边, 则T=(V,{TE})为N的最小生成树。由算法代码中的循环嵌套可得知此算法的时间复杂度为O(n2)。
7.6.2 克鲁斯卡尔(Kruskal)算法
算法核心:
假设N=(V,{E})是连通网, 则令最小生成树的初始状态为只有n个顶点而无边的非连通图T={V,{}}, 图中每个顶点自成一个连通分量。 在E中选择代价最小的边, 若该边依附的顶点落在T中不同的连通分量上, 则将此边加入到T中, 否则舍去此边而选择下一条代价最小的边。 依次类推, 直至T中所有顶点都在同一连通分量上为止。此算法的Find函数由边数e决定, 时间复杂度为O(loge), 而外面有一个for循环e次。所以克鲁斯卡尔算法的时间复杂度为O(eloge)。
- 克鲁斯卡尔算法主要是针对边来展开, 边数少时效率会非常高, 所以对于稀疏图有很大的优势;
- 而普里姆算法对于稠密图, 即边数非常多的情况会更好一些。
7.7 最短路径
在网图和非网图中, 最短路径的含义是不同的(研究网图更有实际意义)。
- 非网图:两顶点之间经过的边数最少的路径;
- 网图:两顶点之间经过的边上权值之和最少的路径, 路径上第一个顶点是源点, 最后一个顶点是终点。
7.7.1 迪杰斯特拉(Dijkstra)算法
- 该算法通过一步步求出顶点之间的最短路径, 过程中都是基于已经求出的最短路径的基础上, 求得更远顶点的最短路径, 最终得到想要的结果。
#define MAXVEX 9
#define INFINITY 65535
typedef int Patharc[MAXVEX]; /* 用于存储最短路径下标的数组 */
typedef int ShortPathTable[MAXVEX]; /* 用于存储到各点最短路径的权值和 */
/* Dijkstra算法, 求有向网G的v0顶点到其余顶点v最短路径P[v]及带权长度D[v] */
/* P[v]的值为前驱顶点下标, D[v]表示v0到v的最短路径长度和。 */
void ShortestPath_Dijkstra(MGraph G, int v0, Patharc *P, ShortPathTable *D)
{
int v, w, k, min;
int final[MAXVEX]; /* final[w]=1表示求得顶点v0至vw的最短路径 */
for (v = 0; v < G.numVertexes; v++) /* 初始化数据 */
{
final[v] = 0; /* 全部顶点初始化为未知最短路径状态 */
(*D)[v] = G.arc[v0][v]; /* 将与v0点有连线的顶点加上权值 */
(*P)[v] = -1; /* 初始化路径数组P为-1 */
}
(*D)[v0] = 0; /* v0至v0路径为0 */
final[v0] = 1; /* v0至v0不需要求路径 */
for (v = 1; v < G.numVertexes; v++) /* 开始主循环, 每次求得v0到某个v顶点的最短路径 */
{
min=INFINITY; /* 当前所知离v0顶点的最近距离 */
for (w = 0; w < G.numVertexes; w++) /* 寻找离v0最近的顶点 */
{
if (!final[w] && (*D)[w] < min)
{
k=w;
min = (*D)[w]; /* w顶点离v0顶点更近 */
}
}
final[k] = 1; /* 将目前找到的最近的顶点置为1 */
for (w = 0; w < G.numVertexes; w++) /* 修正当前最短路径及距离 */
{
if (!final[w] && (min + G.arc[k][w] < (*D)[w]))
{ /* 如果经过v顶点的路径比现在这条路径的长度短的话,说明找到了更短的路径,修改D[w]和P[w] */
(*D)[w] = min + G.arc[k][w]; /* 修改当前路径长度 */
(*P)[w]=k;
}
}
}
}
- 第4行final数组是为了v0到某顶点是否已经求得最短路径的标记,如果v0到vw已经有结果, 则final[w]=1。
- 第5~10行, 是在对数据进行初始化的工作。 此时final数组值均为0, 表示所有的点都未求得最短路径。 D数组为{65535,1,5,65535,65535,65535,65535,65535,65535}。 因为v0与v1和v2的边权值为1和5。 P数组全为0, 表示目前没有路径。
- 第11行, 表示v0到v0自身, 权值和结果为0。 D数组为{0,1,5,65535,65535,65535,65535,65535,65535}。 第12行, 表示v0点算是已经求得最短路径, 因此final[0]=1。 此时final数组为{1,0,0,0,0,0,0,0,0}。 此时整个初始化工作完成。
- 第13~33行, 为主循环, 每次循环求得v0与一个顶点的最短路径。 因此v从1而不是0开始。
- 第15~23行, 先令min为65535的极大值, 通过w循环, 与D[w]比较找到最小值min=1, k=1。
- 第24行, 由k=1, 表示与v0最近的顶点是v1, 并且由D[1]=1, 知道此时v0到v1的最短距离是1。 因此将v1对应的final[1]设置为1。 此时final数组为{1,1,0,0,0,0,0,0,0}。
- 第25~32行是一循环, 此循环甚为关键。 它的目的是在刚才已经找到v0与v1的最短路径的基础上, 对v1与其他顶点的边进行计算, 得到v0与它们的当前最短距离,如下图所示。因为min=1, 所以本来D[2]=5, 现在v0→v1→v2=D[2]=min+3=4,v0→v1→v3=D[3]=min+7=8, v0→v1→v4=D[4]=min+5=6, 因此, D数组当前值为{0,1,4,8,6,65535,65535,65535,65535}。 而P[2]=1, P[3]=1, P[4]=1, 它表示的意思是v0到v2、 v3、 v4点的最短路径它们的前驱均是v1。 此时P数组值为:{0,0,1,1,1,0,0,0,0}。
- 重新开始循环, 此时v=2。 第15~23行, 对w循环, 注意因为final[0]=1和final[1]=1, 由第18行的!final[w]可知, v0与v1并不参与最小值的获取。 通过循环比较,找到最小值min=4, k=2。
- 第24行, 由k=2, 表示已经求出v0到v2的最短路径, 并且由D[2]=4, 知道最短距离是4。 因此将v2对应的final[2]设置为1, 此时final数组为: {1,1,1,0,0,0,0,0,0}。
- 第25~32行。 在刚才已经找到v0与v2的最短路径的基础上, 对v2与其他顶点的边, 进行计算, 得到v0与它们的当前最短距离, 如下图示。 因为min=4, 所以本来D[4]=6, 现在v0→v2→v4=D[4]=min+1=5, v0→v2→v5=D[5]=min+7=11, 因此, D数组当前值为: {0,1,4,8,5,11,65535,65535,65535}。 而原本P[4]=1, 此时P[4]=2,P[5]=2, 它表示v0到v4、 v5点的最短路径它们的前驱均是v2。 此时P数组值为:{0,0,1,1,2,2,0,0,0}。
- 重新开始循环, 此时v=3。 第15~23行, 通过对w循环比较找到最小值min=5,k=4。
- 第24行, 由k=4, 表示已经求出v0到v4的最短路径, 并且由D[4]=5, 知道最短距离是5。 因此将v4对应的final[4]设置为1。 此时final数组为: {1,1,1,0,1,0,0,0,0}。
- 第25~32行。 对v4与其他顶点的边进行计算, 得到v0与它们的当前最短距离, 如下图所示。 因为min=5, 所以本来D[3]=8, 现在v0→v4→v3=D[3]=min+2=7, 本来D[5]=11, 现在v0→v4→v5=D[5]=min+3=8, 另外v0→v4→v6=D[6]=min+6=11,v0→v4→v7=D[7]=min+9=14, 因此, D数组当前值为: {0,1,4,7,5,8,11,14,65535}。 而原本P[3]=1, 此时P[3]=4, 原本P[5]=2, 此时P[5]=4, 另外P[6]=4, P[7]=4, 它表示v0到v3、 v5、 v6、 v7点的最短路径它们的前驱均是v4。 此时P数组值为: {0,0,1,4,2,4,4,4,0}。
- 之后的循环就完全类似了。 得到最终的结果, 如下图所示。 此时final数组为: {1,1,1,1,1,1,1,1,1}, 它表示所有的顶点均完成了最短路径的查找工作。 此时D数组为: {0,1,4,7,5,8,10,12,16}, 它表示v0到各个顶点的最短路径数, 比如D[8]=1+3+1+2+3+2+4=16。 此时的P数组为: {0,0,1,4,2,4,3,6,7}, 这串数字可能略为难理解一些。 比如P[8]=7, 它的意思是v0到v8的最短路径, 顶点v8的前驱顶点是v7,再由P[7]=6表示v7的前驱是v6, P[6]=3, 表示v6的前驱是v3。 这样就可以得到, v0到v8的最短路径为v8←v7←v6←v3←v4←v2←v1←v0, 即v0→v1→v2→v4→v3→v6→v7→v8。
求源点到其他所有顶点的最短路径的时间复杂度是O(n2) ,求任一顶点到其余所有顶点的最短路径就是对每个顶点当作源点运行一次迪杰斯特拉算法, 等于在原有算法的基础上, 再来一次循环, 此时整个算法的时间复杂度就成了O(n3)
7.7.2 弗洛伊德(Floyd) 算法
实际问题
typedef int Pathmatirx[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];
/* Floyd算法, 求网图G中各顶点v到其余顶点w最短路径P[v][w]及带权长度D[v][w] */
void ShortestPath_Floyd(MGraph G, Pathmatirx *P, ShortPathTable *D)
{
int v, w, k;
for (v = 0; v < G.numVertexes; ++v) /* 初始化D与P */
{
for (w = 0; w < G.numVertexes; ++w)
{
(*D)[v][w] = G.matirx[v][w]; /* D[v][w]值即为对应点间的权值 */
(*P)[v][w] = w; /* 初始化P */
}
}
for (k = 0; k < G.numVertexes; ++k)
{
for (v = 0; v < G.numVertexes; ++v)
{
for (w = 0; w < G.numVertexes; ++w)
{
if ((*D)[v][w] > (*D)[v][k] + (*D)[k][w])
{/* 如果经过下标为k顶点路径比原两点间路径更短,将当前两点间权值设为更小的一个 */
(*D)[v][w] = (*D)[v][k] + (*D)[k][w];
(*P)[v][w] = (*P)[v][k]; /* 路径设置经过下标为k的顶点 */
}
}
}
}
}
- 第4~11行就是初始化了D和P, 使得它们成为下图的两个矩阵。 从矩阵也得到, v0→v1路径权值是1,v0→v2路径权值是5, v0→v3无边连线, 所以路径权值为极大值65535。
- 第12~25行, 是算法的主循环, 一共三层嵌套, k代表的就是中转顶点的下标。 v代表起始顶点, w代表结束顶点。
- 当k=0时, 即所有的顶点都经过v0中转, 计算是否有最短路径的变化。 可惜结果没有任何变化, 如图所示。
- 当k=1时, 也就是所有的顶点都经过v1中转。 此时, 当v=0时, 原本D[0] [2]=5, 现在由于D[0] [1]+D[1] [2]=4。 因此由代码的第20行, 二者取其最小值, 得到D[0] [2]=4,同理可得D[0] [3]=8、 D[0] [4]=6, 当v=2、3、4时, 也修改了一些数据, 请参考下图左图中虚线框数据。 由于这些最小权值的修正, 所以在路径矩阵P上, 也要作处理, 将它们都改为当前的P[v] [k]值, 见代码第21行。
- 接下来就是k=2一直到8结束, 表示针对每个顶点做中转的得到的计算结果,即D0是以D-1为基础,D1是以D0为基础, ……, D8是以D7为基础 ,它们是有联系的, 路径矩阵P也是如此。 最终当k=8时, 两矩阵数据如图下图所示。
至此, 我们的最短路径就算是完成了, 你可以看到矩阵第v0行的数值与迪杰斯特拉算法求得的D数组的数值是完全相同, 都是{0,1,4,7,5,8,10,12,16}。 而且这里是所有顶点到所有顶点的最短路径权值和都可以计算出。
那么如何由P这个路径数组得出具体的最短路径呢? 以v0到v8为例, 从上图的右图第v8列, P[0] [8]=1, 得到要经过顶点v1, 然后将1取代0得到P[1] [8]=2, 说明要经过v2, 然后将2取代1得到P[2] [8]=4, 说明要经过v4, 然后将4取代2得到P[4] [8]=3, 说明要经过v3, ……, 这样很容易就推导出最终的最短路径值为v0→v1→v2→v4→v3→v6→v7→v8。
- 代码简写
for (v = 0; v < G.numVertexes; ++v)
{
for (w = v + 1; w < G.numVertexes; w++)
{
printf("v%d-v%d weight: %d ", v, w, D[v][w]);
k = P[v][w]; /* 获得第一个路径顶点下标 */
printf(" path: %d", v); /* 打印源点 */
while (k != w) /* 如果路径顶点下标不是终点 */
{
printf(" -> %d", k); /* 打印路径顶点 */
k = P[k][w]; /* 获得下一个路径顶点下标 */
}
printf(" -> %d\n", w); /* 打印终点 */
}
printf("\n");
}
本质:二重循环初始化加一个三重循环权值修正
O(n3)时间复杂度