1.图的定义G(V,E)其中G表示一个图,V表示图G中顶点集,E表示图G中边的集合
2.线性表中数据元素叫元素,树中数据元素叫结点,在图中数据元素称为顶点
3.线性表中没有数据元素称为空表,树中没有数据元素称为空树,在图的结构中,不允许没有顶点
4.在线性表中相邻的数据元素之间具有线性关系,树结构中,相邻两层的结点具有层次关系,在图中,任意两个顶点可能有关系,顶点之间的逻辑关系用边来表示。
7.2.1各种图的定义:
1.如下图 7-2-2是无向图G1=(V1,{E1}),其中顶点集合V1={A,B,C,D} 边集合={(A,B),(B,C),(C,D),(D,A),(A,C)}其中边之间用()来表示
2.如下图7-2-3,是有向图G2=(V2,{E2}), 顶点集合V2={A,B,C,D} 弧集合E2={,,
3.无向完全图:任意两顶点之间都存在边。含有n个顶点的无向完全图有n*(n-1)/2条边
4.有向完全图:任意两定点之间存在方向相反互为相反的两条弧,含有n个顶点的有向完全图有n*(n-1)条边
5.无向图:边数就是各顶点度数之和的一半
6.有向图:某顶点的度数=出度数+入度数;所有顶点的入度数之和=所有顶点的出度数之和=边数;
7.路径长度:路径上的边或弧的数目,图7-2-9中上方两条路径长度为2,下放两条路径长度为3;图7-2-10中左侧路径长为2,右侧路径长度为3
8.回路,简单路径,简单回路:
能从出发点绕了一圈又回到出发点,称为回路或环;
序列中顶点不重复出现的路径称为简单路径。
除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路。
如图7-2-11中,左侧的回路是简单回路,右侧的环是不是简单回路,因为c重复了。
9.若无重复的边或顶点到自身的边则叫简单图,如下图,就不是简单图:
7.2.3连通图:任意两点之间都是连通的
无向图中的极大连通子图称为连通分量:
7.2.3.2强连通图:有向+连通图
图中有子图,若子图极大连通就是连通分量,有向的就是强连通分量,如下图中图1不是强连通图,图2是强连通图,同样图2是图1的强连通分量
7.2.3.3连通图的生成树:包含图中全部的n个顶点,但只有足以构成一棵树的n-1条边,如图2.图3;如果有n个顶点,但是小于n-1条边则是非连通图;如果边多于n-1,则构成环,如图2,3任意给两个顶点之间一条边,就构成环;不过有n-1条边并不一定是生成树,如图4。
7.2.3.4
如果一个有向图恰有一个顶点的入度为0,其余顶点的入度为1,则是一颗有向树。
一个有向图的生成森林由若干课有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧,如下图中,图1是有向图, 去掉弧之后,分解为两个有向树,如图2 图3,这两颗就是图1的有向图的生成森林。
7.3图的抽象数据类型:
7.4图的五种存储结构:
一.邻接矩阵:
1.图的邻接矩阵存储方式用两个数组来表示图,一个一维数组存储图中顶点信息,一个二维数组存储图中的边或弧的信息。
2.无向图的邻接矩阵的特点:
a.矩阵是关于主对角线对称的
b.Vi顶点的度=第i行或者第i列上的元素之和
c.Vi的所有邻接点就是将矩阵中第i行元素扫描一遍,数组元素为1就是邻接点
d.主队角线上元素为0
3.有向图的邻接矩阵的特点:
a.不是对称矩阵
b.主队角线上元素为0
c.顶点Vi的入度为第Vi列各数之和;出度数为第Vi行各数之和
d.判断Vi到Vj是否存在弧,只需要查找矩阵中元素是否为1
e.要求Vi的所有邻接点就是将第i行元素扫描一遍,查找矩阵中元素为1的点
4.对于边上的权值而言,如果不存在则为无穷;自身到自身的度为矩阵元素为0,
5.图的邻接矩阵存储的结构:
typedef char VertexType; //顶点类型应由用户定义
typedef int EdgeType; //边上的权值类型应由用户定义
#define MAXVEX 100 //最大顶点数
#define INFINITY 65535 //用65535来表示无穷
typedef struct
{
VertexType vexs[MAXVEX]; //顶点表
EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵,
int numVertexes, numEdges; //图中当前顶点数和边数
}MGraph;
6.构造一个无向图
//建立无向图的邻接矩阵
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; //邻接矩阵初始化
for(k=0; k<G->numEdges; k++)
{
printf("输入边(Vi,Vj)上的下标i,下标j和权w:\n");
scanf("%d,%d,%d",&i,&j,&w); //输入边(Vi,Vj)上的权W
G->arc[i][j]=w;
G->arc[i][j]=G->arc[i][j]; //因为是无向图,矩阵对称
}
//时间复杂度为o(n+n^2+e):n个顶点,e条边 最终为o(n^2)
}
二.邻接表:
1.顶点用一维数组来存放,另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针;每个顶点的所有邻接点构成一个线性表,用单链表存储
2.第Vi个顶点的度,就去查这个顶点的边表中结点的个数
3.要判断Vi与Vj之间是否存在边,只需要测试顶点Vi的边表中是否存在结点Vj的下标j就行
4.若要求顶点的所有邻接点,就对此顶点的边表进行遍历,得到的就是邻接点。见下图7-4-6
5.有向图的逆邻接表,就是看谁往这个点里干,如下图7-4-7(2)
6.有向图的邻接表,如下图7-4-7(1)
7.带权值的邻接表,如下图7-4-8
邻接表结点定义的代码:
typedef char VertexType; //顶点类型由用户定义
typedef int EdgeType; //边上的权值类型由用户定义
typedef struct EdgeType //边表结点
{
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");
scanf("%d,%d",&i,&j); //输入边(vi,vj)上的顶点序号
e=(EdgeNode *)malloc(sizeof(EdgeNode)); //向内存申请空间 , 生成边表结点
e->adjvex=j; //邻接序号为j
e->next=G->adjList[i].firstedge; //头插法每次将新的结点连接到顶点表结点的后面的结点
G->adjList[i].firstedge=e; //把e这个结点在放回头结点
e=(EdgeNode *)malloc(sizeof(EdgeNode)); //向内存申请空间 , 生成边表结点
e->adjvex=i; //邻接序号为j
e->next=G->adjList[j].firstedge; //头插法每次将新的结点连接到顶点表结点的后面的结点
G->adjList[j].firstedge=e; //把e这个结点在放回头结点
}
}
时间复杂度为o(n+e)=o(n) //n个顶点,e条边
第三种:十字链表:就是将邻接表和逆邻接表相结合方便了解结点的出度和入度
1.重新定义顶点表结点结构,如下图,
firstin 表示入边表头指针,指向该顶点的入边表中第一个结点
firstout 表示出边表指针,指向该顶点的出边表中的第一个结点
2.重新定义边表结点结构,如下图:
tailvex是指弧起点在顶点表的下标
headvex是指弧终点在顶点表中的下标
headlink是指边表指针域,指向终点相同的下一条边
taillink 是指边表指针域,指向起点相同的下一条边
v0中firstout指向它自己的边表结点,后面那个边表结点中03表示从0开始走到3;v0中fistrtin表示V1走向它,他犯贱再走向v1,v0再也没有可走的下一个结点,所以taillink为NULL;v0走向v3,没有人与他一样再走向v3所以headlink为NULL
v1中firstou指向自己的边表结点,后面10 。12分别表示从v1走向V0,从v1走向V2;所以v1第一个边表结点中taillink指向下一个,由于1走向0,2也走向0,所以v1中第一个边表结点走到v2中第一个边表结点;同样V2走向V1,v1犯贱它的fisttin指向V2中第2个边表结点
v2也是如此,重点强调由于v2走向v0,没有人再走向v0所以v2中第一个边表结点的healink为空
第四种:邻接多重表
1.如果要删除边,则对于下图的邻接表而言太复杂对于结点而言改动太多
2.重新定义如下结构:
ivex和jvex表示与某条边依附的两个顶点在顶点表中的下标。
ilink指向依附顶点ivex的下一条边。
jlink指向依附顶点jvex的下一条边
1-4表示连接它自己的边表结点
5表示依附于v0的下一条边:v0–v3 6表示依附于v0的下一条边:v0–v2 7表示依附于v1的下一条边:v1–v0
8表示依附于v2的下一条边:v2–v3 9表示依附于v2的下一条边:v2–v0 10表示依附于v2的下一条边:v3–v2
第五种:边集数组:
如下图,就能看懂
7.5.1深度优先遍历:
1.简称DFS,说白了就是遍历图中每个顶点然后,所有路径都走一遍。遍历过的顶点做好标记就不在遍历
邻接矩阵的DFS代码如下:
typedef int Boolean; //Boolean是布尔类型,其值是TRUE或FALSE
Boolean visited[MAX]; //访问标志的数组
//邻接矩阵的深度优先递归算法
void DFS(MGraph G, int i)
{
int j;
visited[i]=TRUE;
printf("%c",G.vexs[i]); //vexs【】表示顶点数组
for(j=0; j<G.numVertexes; j++)
if(G.arc[i][j]==1 && !visited[j]) //arc表示边表数组,1表示两个点之间有值
DFS(G,j); //对访问的邻接顶点递归调用
}
//邻接矩阵的深度遍历操作
void DFSTraverse(MGraph G)
{
int i;
for(i=0; i<G.numVeretexes; i++)
visited[i]=False; //初始所有顶点都是未访问过的状态
for(i=0; i<G.numVertexes; i++)
if(!visited[i]) //对未访问过的顶点调用DFS
DFS(G,i);
}
时间复杂度是o(n^2)
邻接表的深度优先递归算法:
void DFS(GraphAdjList GL, int i)
{
EdgeNode *p;
visited[i]=TRUE;
printf("%c ", GL->adjList[i].data); //打印顶点,adjLIst表示顶点结构数组,结构成员有data和fistredge指针
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++)
if(!visited[i]) //对访问过的顶点调用DFS
DFS(GL,i);
}
时间复杂度是o(n+e);
邻接矩阵的广度遍历算法:简称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.vex[i]); //打印顶点,也可以其他从操作
EnQueue(&Q,i); //将此顶点入队列
while(!QueueEmpty(Q)) //若当前队列不为空
{
DeQueue (&Q, &i); //将队中元素出队列,赋值给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); //将队中元素出队列,赋值给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; //指针指向下一个邻接点
}
}
}
}
}
深度优先遍历:更适合目标比较明确,已找到目标为主要目的,类似于前序遍历
广度优先遍历:更适合在不断扩大遍历范围时找到相对最优解情况 ,类似于层序遍历
/*Prim算法生成最小生成树*/
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; //初始化最小权值为无穷
j=1;k=0; //j用来做顶点下标循环的变量,k是用来存储最小权值的顶点下标、
//20--29表示找出最小值min和对应的数组下标k
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); //打印当前顶点边中权值最小边,比如打印结果为(0,1),表示V0至v1遍为最小生成树的第一条边
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
}
}
}
}
以下便是Prim算法的过程,最终构成n-1条边,遍历完所有顶点,以某顶点为起点,逐步找各顶点上权值最小的边构建最小生成树
Krskal算法:求最小生成树
1.将邻接矩阵转化为边集数组:
如下图
/*Kruskal算法*/
void MiniSpanTree_Kruskal(MGraph G) //生成最小生成树
{
int i,n,m;
Edge edges[MAXEDGE]; //定义边集数组
int parent[MAXEDGE]; //定义一数组用来判断边与边是否形成环路
//此处省略将邻接矩阵G转化为边集数组edges并按权值由小到大排列的代码
for(i=0; i<G.numVertexes; i++)
parent[i]=0; //初始化数组为0
for(i=0; i<G.numEdges; i++) //循环每一条边
{
n=Find(parent,edges[i].begin);
m=Find(parent,edges[i].end);
if(n!=m)
{
parent[n]=m; //将此边的结尾顶点放入下表为起点的parent中
//表示此顶点已经在生成树集合中
print("(%d,%d)",edges[i].begin,edges[i].end,edges[i].weight);
}
}
}
int Find(int *parent, int f) //查找连线顶点的尾部下标
{
while(parent[f]>0)
f=parent[f];
return f;
}
算法中find函数有e决定,时间复杂度为o(loge),而外面有一个for循序e次,所以时间复杂度为o(eloge)
具体执行过程如下:
在E中选择最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此加入到T中,否则舍去此边而选择下一条代价最小的边
k式算法主要针对边来展开,边数少效率非常高,适合稀疏图。p式算法对于稠密图,边数非常多情况会更好
锋说:就是每次找最小的边,不要形成回路,然后最终能将图中所有顶点连接起来
总结:p式算法就是以某个顶点开始找与这个点相关的最小 的边,每次这样寻找;而k式算法就是直接上来找最小的边不形成回路,最终将整个点连起来,他俩都是去找连通图的最小生成树
7.7迪杰斯特拉算法:求最短路径
#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]=0; //初始化路径数组p为0
}
(*D)[v0]=0; //v0至v0的路径为0
final[v0]=1; //v0至v0不需要求路径
//开始主循环,每次求得V0到某个顶点v的最短路径
for(v=1; v<G.numVertexes; v++)
{
min=INFNITY; //当前所知离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++) //修改当前最短路径及距离
{
//如果经过v顶点的路径比现在这条的路径长度短的话
if(!final[w] && (min+G.arc[k][w]<(*D)[W]))
{
//说明找到了更短的路径,修改D[W]和p[W]
(*D)[w]=min+G.arc[k][w]; //修改当前路径长度
(*p)[w]=k;
}
}
}
}
时间复杂度:o(n^2)
数组D,数组P含义:上图中比如到v3的最短路径长度是7,比如到v5的最短路径长度是8。。
数组p表示V0----->V8的最短路径中,P[8]=7表示V8的前驱节点为V7;P[4]=2表示V4的前驱节点是2.。。
如果想要知道任意顶点开始到某结点的最短路径,那就没到一个结点就用一次迪杰斯特拉算法,这 样的复杂度为o(n^3)
锋说:其实迪杰斯特拉算法目的是计算从某个点到某个点的最短路径,思想就是每次找到最短的路径,把他放入到已经找到的路径中,很漂亮的思想
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 ShorttestPath_Floyd(MGraph G, Pathmatirx *P, ShortPathTable *D)
{
int v,w,k;
for(v=0; v<G.numVertexes; ++w)
{
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) //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的顶点
}
}
}
}
} 时间复杂度o(n^3)
最短路径的显示代码
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]; //获得下一个路径顶点下标,p数组已经设置好了
}
printf(" -> %d\n",w); //打印终点
}
printf("\n");
}
上面代码图示:
矩阵D表示从某个点到某个点的最短路径长度
p矩阵的含义不用知道,只要知道怎么通过此矩阵找到最短路径
以v0–v8
首先p[0][8]=1,表示经过v1
再看v[1][8]=2,表示经过v2
再看v[2][8]=4, 表示经过v4.。。。。以此类推
锋说:迪杰斯特拉算法和弗洛伊德算法的相同与不同:
相同点:
1.都是求某个点到某个点的最短路径
2.核心思路都是不断的找最短的路径归并到已经找到的路径集合中
不同点:
1.D通过min+下一次的路径和直接到达路径长度比较;而F通过拐点k来表示i–>k—>j要比i—>j路径要短
2.最终得到的数组D都是表示vi到vj的最短路径,而p不同,其中D中p只能表示以v0为起点,到其余各点的最短路径;比如要想知道v2,v3…到其余各点的最短路径,只能再次将v2,v3当做源头再次使用D式算法。但是F式算法中p可以知道所有顶点到所有顶点的最短路径
7.8.2拓扑排序:判断工程能否顺利进行
在一个有向图中,就像拍电影有先后顺序,这样的网图称为AOV图,如下所示:
下图中的一个拓扑序列是:v0v1v2v3v4v5v6v7v8v9v10v11v12v13v14v15v16
而且AOV图不存在回路,所以在输出时如果顶点数少了一个,则说明不是回路,如果顶点被全部输出,则说明它不存在回路。
对AOV网进行拓扑排序的基本思路:
从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧,继续重复此步骤,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止。
在拓扑排序算法中,涉及的结构代码如下,使用邻接表来构造:
```cpp
typedef struct EdgeNode //边表结点
{
int adjvex; //邻接点域,存储该结点对应的下标
EdgeType weight; //用于存储权值,对于非网图可以不需要
struct EdgeNode *next; //链域,指向下一个邻接点
}EdgeNode;
typedef struct VertexNode //顶点表结点
{
int in;
VertexType data; //顶点域,存储顶点信息
EdgeNode *firstedge; //边表头指针
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes,numEdges; //图中当前顶点数和边数
}graphAdjList,*GraphAdjList;
//拓扑排序
Status TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i,k,gettop;
int top=0; //用于栈指针下标
int count=0; //用于统计输出顶点的个数
int *stack; //建栈存储入度为0的顶点
stack=(int *)malloc(GL->numVertexes * sizeof(int));
for(i=0; i<GL->numVertexes; i++)
if(GL->adjList[i].in==0)
stack[++top]=i; //将入度为0的顶点入栈
while(top!=0) //表示栈指针为0时循环结束,也就是再也没有入度为0的顶点了
{
gettop=stack[top--]; //出栈
printf("%d -> ",GL->adjList[gettop].data); //打印此顶点
count++; //统计输出顶点数
for(e=GL->adjList[gettop].firstedge; e; e=e->next) //此循环就是边表的循环
{
//对此顶点弧表遍历
k=e->adjvex;
if(!(--GL->adjList[k].in)) //将k号顶点的入度域减1
stack[++top]=k; //如果为0则入栈
}
}
if(count < GL->numVertexes) //如果count小于此顶点数,说明存在环
return ERROR;
else
return OK;
}
时间复杂度:
第一个for循环,一共n个顶点,时间复杂度为o(n)
下面while,从下图6可以看出,最后删除边都没有了,所以时间复杂度为o(e)
最终时间复杂度为o(n+e)
具体过程如下:
先从v3开始
打印输出v3
依次类推,如下所示:
最终拓扑排序的结果为3->1->2->6->0->4->5->8->7->12->9->10->13->11,当然这个拓扑排序结果不唯一
7.9关键路径:求完成工程最短的时间问题,利用求出关键路径算法,求出完成工程的最短时间和关键活动有哪些?
1.关键路径用AOE表示,AOV表示活动之间以相互制约关系,而AOE是在AOV的基础上来分析完成整个活动所需的最短时间,如下图所示:
其中每个AOE网图都有一个源点和汇点
路径长度:路径上各个活动所持续的时间之和称为路径长度
从源点到汇点的具有最大长度的路径叫关键路径,关键路径上的活动叫关键活动
7.9.1关键路径的算法原理:
放学回家到睡觉,一共4个小时。写作业两个小时,最早开始时间是一回来,可以理解为0。最晚开始时间为2个小时之后,可以理解为2。当最早开始时间和最晚开始时间不相同时,表示有空闲时间。当买了很多的习题时,时间全部被占满,则最早和最晚开始时间为都是0,因此他就是关键活动。
1.事件的最早发生时间etv:即顶点vk的最早发生时间
2.事件的最晚发生时间ltv:即顶点vk的最晚发生时间,如果超过此时间,就会延误整个工期
3.活动最早开工时间ete:即弧ak的最早发生时间
4.活动最晚开工时间lte:即弧ak的最晚发生时间
通过1,2求得3,4,然后根据ete[k]与lte[k]是否相等来判断ak是否为关键活动
int *etv, *ltv; //时间最早发生时间和最迟发生时间数组
int *stack2; //用于存储拓扑排序的栈
int top2; //用于stack2的的指针
//拓扑排序,用于关键路径计算
Status TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i,k,gettop;
int top=0; //用于栈指针下标
int count=0; //用于统计输出顶点的个数
int *stack; //建栈存储入度为0的顶点
stack=(int *)malloc(GL->numVertexes * sizeof(int));
for(i=0; i<GL->numVertexes; i++)
if(GL->adjList[i].in==0)
stack[++top]=i; //将入度为0的顶点入栈
top2=0; //初始化为0
etv=(int *)malloc(GL->numVertexes*sizeof(int)); //事件最早发生的时间
for(i=0; i<GL->numVertexes; i++)
etv[i]=0; //初始化为0
stack2=(int *)malloc(GL->numVertexes*sizeof(int)); //初始化
while(top!=0) //表示栈指针为0时循环结束,也就是再也没有入度为0的顶点了
{
gettop=stack[top--]; //出栈
count++; //统计输出顶点数
stack2[++top2]=gettop; //将弹出的顶点序号压入拓扑序列的栈
for(e=GL->adjList[gettop].firstedge; e; e=e->next) //此循环就是边表的循环
{
//对此顶点弧表遍历
k=e->adjvex;
if(!(--GL->adjList[k].in)) //将k号顶点的入度域减1
stack[++top]=k; //如果为0则入栈
if((etv[gettop]+e->weight)>etv[k]) //求各顶点事件最早发生时间
etv[k]=etv[gettop]+e->weight;
}
}
if(count < GL->numVertexes) //如果count小于此顶点数,说明存在环
return ERROR;
else
return OK;
}
上面代码的例子如下图:
对于v3来说,4+8>3+5,所以v3=12。以此类推
关键路径的算法代码:
//求关键路径,GL为有向图,输出GL的各项关键活动
void CriticalPath(GraphAdjList GL)
{
EdgNode *e;
int i,gettop,k,j;
int ete,lte; //声明活动最早发生时间和最迟发生时间
TopologicalSort(GL); //求拓扑序列,计算数组etv和stack2
ltv=(int *)malloc(GL->numverteces*sizeof(int)); //事件最晚发生时间
for(i=0; i<GL->numVertexes; i++)
ltv[i]=etv[GL->numVertexes-1]; //初始化ltv
while(top2!=0) //计算ltv
{
gettop=stack2[top2--]; //将拓扑序列出栈,后进先出
for(e=GL->adjList[gettop].firstedge; e; e=e->next)
{
//求各顶点最迟发生时间ltv
k=e->adjvex;
if(ltv[k]-e->weight<ltv[gettop]) //求各顶点事件最晚发生时间
ltv[gettop]=ltv[k]-e->weight;
}
}
for(j=0; j<GL->numVertexes; j++) //求ete,lte和关键活动
{
for(e=GL-adjList[j].firstedge; e; e=e>next)
{
k=e->adjvex;
ete=etv[j]; //活动最早发生时间
lte=ltv[k]-e->weight; //活动最迟发生时间
if(ete=lte) //两者相等即在关键路径上
printf(" length: %d , " ,GL->adjList[j].data,GL->adjList[k].data,e->weight);
}
}
}
上述代码所求etv和ltv如下所示+关键路径如下所示:
总结:
etv通过拓扑排序算出最早发生的时间,每次找权值较大的
ltv相当于从后向前算出最晚发生时间,每次找权值较小的
ete=etv[j]表示当事件发生时,最早开工时间(当然必须事件发生,才开工)
lte=ltv[k]-e->weight表示事件最晚发生的时间-要完成工作的时间(比如23点睡觉,写作业2h,可以23点才开始写但要睡觉所以最晚开工时间是21点,最晚21点开始写,就是这意思,有点牵强,理解即可)