大话数据结构第七章笔记(图)

本文详细介绍了图数据结构,包括无向图和有向图的定义,以及无向完全图、有向完全图的概念。还探讨了图的存储结构,如邻接矩阵、邻接表、十字链表、邻接多重表和边集数组的特点和应用场景。此外,文章还讨论了连通图、强连通图、生成树、最短路径算法(迪杰斯特拉和弗洛伊德算法)以及拓扑排序和关键路径的概念。
摘要由CSDN通过智能技术生成

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={<A,D>,<B,A>,<C,A>,<B,C>} A是弧头,D是弧尾 。无向边用()表示,有向边用<>表示
在这里插入图片描述
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-12,图1不是连通图,图2,图3都是子图且都连通,但图4不是连通分量,因为他只有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;     //指针指向下一个邻接点
                    }
            }
        }
    }
}

深度优先遍历:更适合目标比较明确,已找到目标为主要目的,类似于前序遍历
广度优先遍历:更适合在不断扩大遍历范围时找到相对最优解情况 ,类似于层序遍历

7.6.1Prim算法:
在这里插入图片描述
Prim算法生成最小生成树


/*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;

在这里插入图片描述
in表示入度域,下面的邻接表就是表示上图
在这里插入图片描述
拓扑排序的代码:

//拓扑排序
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是否为关键活动

在这里插入图片描述
下列代码求etv的过程,与拓扑排序类似

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("<v%d,v%d> 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点开始写,就是这意思,有点牵强,理解即可)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值