本章主要介绍图的概念,存储结构、图的遍历方法、计算最短路径、最小生成树的方法等,本章在考研中是重点内容。
图由结点(顶点)的有穷集合V和边的有穷集合E组成。边是顶点的有序偶对,若两个顶点之间存在一条边,就表示这两个顶点具有相邻关系。
有向图:边带有方向。边称为弧,含箭头的一端称为弧头,另一端称为弧尾。
邻接矩阵:表示顶点之间相邻关系的矩阵,图的顺序存储结构。设G = (V, E)是具有n个顶点的图,则G的邻接矩阵是具有如下定义的n阶方阵A:
A[i][j] = 1表示顶点i与顶点j邻接;
A[I][j] = 0表示顶点i与顶点j不邻接;
对于无向图:邻接矩阵是对称的,第i行或者第i列的元素之和即为顶点i的度;
对于有向图:第i行元素之和为i的出度,第j列元素之和为j的入度。
邻接表:图的链式存储结构。第一个结点存放有关顶点的信息,其余存放边的信息。
//代表边的结点
typedef struct ArcNode {
int adjvex;//该边指向的顶点
struct ArcNode *nextarc;//指向下一条边的指针
int info;//权重等
} ArcNode;
//代表顶点
typedef struct VNode {
char data;
ArcNode *firstarc;//指向第一条边的指针
} VNode;
//邻接表
typedef struct AGraph {
VNode adjlist[maxSize];
int n, e;
} AGraph;
【例子1】:邻接表
深度优先搜索遍历(DFS): 类似二叉树的先序遍历:首先访问出发点V,并将其标记为已访问过,然后选取与V邻接的未被访问的任意一个顶点W,并访问它;再选取与W邻接的未被访问的任意顶点并访问,依次重复进行。当一个顶点所有的邻接顶点都被访问过,则依次退回附近被访问过的顶点,若该顶点还有其他邻接顶点未被访问,则选取一个重复进行直到图中所有顶点都被访问。
int visit[maxSize];//标记顶点是否被访问过,初始化为0
/*
* @param g The graph to be visited.
* @param v The start point of the graph.
* Using DFS to visit graph.
*/
void DFS(AGraph *g, int v) {
ArcNode *p;
visit[v] = 1;//设置v号顶点(起点)被访问过
Visit(v);//代表对该结点进行访问操作
p = G->adjlist[v].firstarc;//p指向v的第一条边
while(p != null) {
//如果该顶点未被访问过
if (visit[p->adjvex] == 0) {
DFS(g, p->adjvex);
p = p->nextarc;
}
}
}
【例子2】:DFS
广度优先搜索遍历(BFS): 类似二叉树的层次遍历,需要用到一个队列实现:
1.任取图中一个顶点,访问后入队,标记为已访问;
2.队列不空的时候,循环执行:出队、依次检查出队顶点的所有邻接顶点。访问没有被访问过的所有邻接顶点并将其入队;
3.当队列为空的时候,跳出循环,广度优先搜索完成。
int visit[maxSize]//标记顶点是否被访问过,初始化为0
/*
* @param g The graph to be visited.
* @param v The start point of the graph.
* Using BFS to visit graph.
*/
void BFS(AGraph *g, int v) {
ArcNode *P;
Queue q;
int temp;//存储当前出队的顶点
visit[v] = 1;
Visit(v);
q.initial();
q.push(v);
while(!q.empty()) {
q.pop(temp);
p = G->adjlist[temp].firstarc;//p指向出队顶点的第一条边
//p所有邻接点中,未被访问的顶点入队
while(p != null) {
if (visit[p->adjvex] == 0) {
Visit(p->adjvex);
visit[p->adjvex] = 1;
q.push(p->adjvex);
}
p = p->nextarc;
}
}
}
【例子3】:BFS
利用邻接表存储图的时候,两种算法的时间复杂度是O(n+e)
利用邻接矩阵存储的时候,两种算法的时间复杂度是O(n^2)
一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。[1]这里我们介绍两种生成最小生成树的算法:普里姆算法和克鲁斯卡尔算法。这两个算法,在考研中比较要求思路和过程,对于代码的实现方式考察不是很多,所以重点要理解建树的过程,这里就不贴代码了。
普里姆算法: 从图中选出一个作为顶点,把它当作一棵树,然后从与这棵树相接的边中选择一条路径最短(权值最小)的边,并将这条边及其所连接的顶点也并入树中,以此类推,直到图中所有顶点都被并入树中为止。
【例子4】:普里姆算法求最小生成树
克鲁斯卡尔算法: 一句话来描述该算法:在不构成环的情况下,每次选权值最小边加入生成树。
【例子5】:克鲁斯卡尔算法求最小生成树
两种算法的比较
求一个顶点到其他各顶点的最短路径:迪杰斯特拉算法和弗洛伊德算法。
迪杰斯特拉算法: 求一个顶点到其他各顶点的最短路径。
(1)初始化:用起点V到该顶点W的直接边初始化为最短路径,否则设为∞;
(2)从未求得最短路径的终点中选择路径长度最小的终点U:即求得V到U的最短路径;
(3)修改最短路径:计算U的邻接点的最短路径,替代之;
(4)重复以上步骤,直到求得V到其余所有顶点的最短路径。
算法复杂度:O(n^2)
【例子6】:迪杰斯特拉算法求最短路径
dist[]:存储当前已找到的从起点V0到其他顶点Vi的最短路径长度。
path[]:保存从起点V0到其他顶点Vi最短路径中,Vi的前一个顶点。
由上述过程可知:从顶点0到顶点1~6**最短路径长度**分别为:4,5,6,10,9,16
弗洛伊德算法: 求图中任意一对顶点之间的最短路径。(考的概率不大;即使考,内容和形式也不难)
(1)设矩阵A,Path,初始化将图的邻接矩阵赋值给A,将Path全置为-1;
(2)以顶点K为中间顶点,J取0~n-1(n为图中顶点个数),对图中所有顶点对{i, j}进行如下检测和修改:
如果A[i][j] > A[i][k] + A[k][j],则将A[i][j]改为A[i][k] + A[k][j]的值,将Path[i][j]改为k。
/* 应该不会考白板默写代码,这里贴出来仅仅供大家进一步理解算法 */
void Floyed(AGraph g, int A[][maxSize], int Path[][maxSize]) {
int i, j, k;
for (i = 0; i < g.n; i++) {
for (j = 0; j < g.n; j++) {
A[i][j] = g.edges[i][j];
Path[i][j] = -1;
}
}
for (k = 0; k < g.n; k++) {
for (i = 0; i < g.n; i++) {
for (j = 0; j < g.n; j++) {
if (A[i][j] > A[i][k] + A[k][j]) {
A[i][j] = A[i][k] + A[k][j];
Path[i][j] = k;
}
}
}
}
}
AOV网:一种以顶点表示活动,以边表示活动的先后次序并且没有回路的有向图。
有向无环图(DAG),AOV网进行拓扑排序:将图中所有顶点排成一个线性序列,使得图中任意一对儿顶点U和V,若存在由U到V的路径,则在拓扑排序序列中一定是U出现在V的前面。若一个有向图能够被拓扑排序,则它一定是一个有向无环图。
即:每次删除入度为0的顶点并输出之。
**时间复杂度:**O(n+e),n代表顶点个数,e代表边的条数。
AOE网:边表示活动,边有权值,边代表活动持续时间。
关键路径:路径长度最长的路径。
【例子7】:拓扑排序例子
拓扑排序的结果不一定是唯一的,如:ACBDE也是以上DAG图的拓扑有序序列。
关键路径算法:
事件(顶点)i:最早发生事件ve(i),最晚发生时间vl(i)
活动(边)a(i, j):最早开始时间e(e, j),最晚开始时间l(i, j)
(1)按拓扑有序排列顶点:对顶点拓扑排序;
(2)计算ve(j):ve(1)=0,ve(j)=max{ve(*)+a(*, j)},其中*为任意前驱事件;
(3)计算vl(i):vl(n)=ve(n),vl(i)=min{vl(*)-a(i, *)},其中*为任意后继事件;
(4)计算e(i, j)和l(i, j):e(i, j)=ve(i),l(i, j)=vl(j)-a(i, j)
(5)工程总用时ve(n),关键活动是e(i, j)=l(i, j)的活动是a(i, j)
【例子8】:关键路径求法
工程完工需要15,关键路径是:1->2->5->6->8->9
1.严蔚敏《数据结构与算法分析》:清华大学出版社,2011