数据结构_图

1、图的定义

图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
在这里插入图片描述
在图的定义中,需要注意如下几个地方:
1)线性表中我们把数据元素叫作元素,树中将数据元素叫结点,在图中数据元素,我们称之为顶点(Vertex)。
2)线性表中可以没有数据元素,称为空表,树中可以没有结点,叫作空树,但在图结构中,不允许没有顶点。在定义中,若V是顶点的集合,则强调了顶点V有穷非空。
3)线性表中,相邻的数据元素之间具有线性关系,树结构中,相邻两层的结点具有层次关系,而图中,任意两个顶点之间都有可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。

(1)各种图的定义
无向边和无向图:
在这里插入图片描述
有向边和有向图:
在这里插入图片描述
在这里插入图片描述
简单图:
在这里插入图片描述
无向完全图:
在这里插入图片描述
有向完全图:
在这里插入图片描述
稀疏图和稠密图:
在这里插入图片描述
权和网:
在这里插入图片描述
子图:
在这里插入图片描述
在这里插入图片描述
(2)图的顶点与边之间的关系
无向图中邻接点、顶点相关联、顶点的度:
在这里插入图片描述
有向图中邻接点、顶点相关联、顶点的入度和出度:
在这里插入图片描述
无向图中顶点的路径:
在这里插入图片描述
在这里插入图片描述
有向图的路径:
在这里插入图片描述
树中根结点到任意结点的路径是唯一的,但树中顶点与顶点的路径确不是唯一的。
路径的长度:
在这里插入图片描述
回路或环、简单路径、简单回路或简单环:
在这里插入图片描述
在这里插入图片描述
(3)连通图相关术语
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
连通图的生成树:
在这里插入图片描述
在这里插入图片描述
有向树与生成森林:
在这里插入图片描述
在这里插入图片描述

2、图的抽象数据类型

图作为一种数据结构,它的抽象数据类型带有自己的特点,正因为它的复杂性,运用广泛,使得不同的应用需要不同的运算集合,构成不同的抽象数据操作。这里仅介绍图的基本操作。
在这里插入图片描述

3、图的存储结构

图的存储结构相较线性表与树来说更加复杂。首先,我们口头上说的“顶点的位置”或"邻接点的位置"只是一个相对的概念。其实从图的逻辑结构定义来看,图上任何一个顶点都可被看成第一个顶点,任一顶点的邻接点之间也不存在次序关系。如图7-4-1中的四张图,仔细观察发现,他们其实是同一个图,只不过顶点的位置不同,就造成了表象上不太一样的感觉。
在这里插入图片描述
也正由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系,也就是说,图不可能以简单的顺序存储结构来表示。而多重链表的方式,即以一个数据域和多个指针域组成的结点表示图中的一个顶点,尽管可以实现图结构,但其实在树中,我们也已经讨论过,这其实是有问题的。如果各个顶点的度相差很大,按度数最大的顶点设计来设计存储结构会造成存储单元的浪费,而若按每个结点自己的度数设计不同的顶点结构,又带来操作的不便。因此,对于图来说,如何对它实现物理存储是个难题,不过我们的前辈已经解决了。下面将介绍树的五种存储结构。

(1)邻接矩阵

考虑到图是由顶点和边或弧两部分组成。合在一起比较困难,那就很自然地考虑到分两个结构来分别存储。顶点不分大小、主次,所以用一个一维数组来存储是不错的选择。而边或弧由于是顶点与顶点之间的关系,一维搞不定,那就考虑用一个二维数组来存储。
图的邻接矩阵(Adjiacency Matrix)存储方式是用两个数组来表示图。一个一维数组来存储图中的顶点信息,一个二维数组(称为邻接矩阵)来存储图中的边或弧信息。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述图的邻接矩阵存储的结构,代码如下:

typedef char VertexType;/*顶点类型由用户定义*/
typedef int EdgeType;/*边上的权值类型由用户定义*/
#define MAXVEX 100 /*最大顶点数,应由用户定义*/
#define INFINITY 65535 /*用65535来代码∞*/
typedef struct
{
 VertexType vexs[MAXVEX];/*顶点表*/
 EdgeType arc[MAXVEX][MAXVEX];/*邻接矩阵,可看作边表*/
 int numVextexes, numEdge;/*图中当前的顶点数和边数*/
}MGraph;

在邻接矩阵存储结构中,构造一个图,其实就是给顶点表和边表输入 数据的过程。无向网图的创建代码如下:

/*建立无向网图的邻接矩阵表示*/
void CreateMGraph(MGraph* G)
{
    
    
 int i, j, k, w;
 printf("输入顶点数和边数:\n");
 scanf("%d,%d",&G->numVextexes,&G->numEdges);/*输入顶点数和边数*/
 for (i = 0; i < G->numVextexes; i++)/*输入顶点信息,建立顶点表*/
  scanf(&G->vexs[i]);
 for (i = 0; i < G->numVextexes; i++)
 {
    
    
  for (j = 0; j > G->numVextexes; j++)
  {
    
    
   G->arc[i][j] = INFINITY;/*邻接 矩阵初始化*/
  }
 }
 for (k = 0; k < G->numEdges; k++)/*读入numEdges条边,建立邻接矩阵*/
 {
    
    
  printf("输入边(Vi,Vj)上的上标i,下标j和权w");
  scanf("%d,%d,%d",&i,&j,&w);/*输入边(Vi,Vj)上的权*/
  G->arc[i][j] = w;
  G->arc[j][i] = G->arc[i][j];/*因为是无向图,矩阵对称*/
 }
}

从上述代码中可以看出,n个顶点和e条边的无向网图的创建,时间复杂度为O(n+n²+n),其中对邻接矩阵arc的初始化耗费了O(n²)的时间。

(2)邻接表

邻接矩阵是不错的一种图存储结构,但是我们也发现,对于边数相对顶点数较少的图,这种结构是存在存储空间的极大浪费的,比如要处理下图这样的稀疏有向图,邻接矩阵中除了arc[0][1]有权值外,没有其他弧,其实这些存储空间(即权值为∞的空间)都浪费掉了。
在这里插入图片描述
因此考虑另外一种存储方式。回忆我们在线性表时谈到,顺序存储结构就存在预先分配内存可能造成存储空间浪费的问题,于是引出了链式存储结构。同样地,我们也考虑对边或弧使用链式存储的方式来避免空间浪费。
再回忆我们在树中谈到存储结构时,讲到了一种孩子表示法,将结点存入数组,并对结点的孩子进行链式存储,不管有多少孩子,也不会存在空间浪费问题。这个思路同样适用于图的存储。我们把这种数组与链表相结合的存储方式称为邻接表(Adjacency List)。
邻接表的处理方法如下:
1)图中顶点用一个一维数组存储,当然,顶点可以用单链表来存储,只不过数组可以较容易地读取顶点信息,更加方便。另外,对于顶点数组,每个数据元素还需要存储指向第一个邻接点地指针,以便于查找该顶点的边信息。
2)图中的每个顶点Vi的邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点Vi的边表,有向图则称为顶点Vi作为弧尾的出边表。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
邻接表结构中结点定义代码如下:

/*邻接表存储结构中结点定义的代码*/
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 CreateMGraph(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));/*向内存申请空间,生成边表结点*/
  /*应用单链表中的头插法,G->adjList[i]相当于头结点,G->adjList[i].firstedge相当于头结点中的指针域next,指向第一个结点*/
  e->adjvex = j;/*邻接点序号j*/
  e->next = G->adjList[i].firstedge;/*将e指针指向当前顶点指向的结点*/
                                    /*该语句相当于单链表中新结点的指针域指向第一个结点*/
  G->adjList[i].firstedge = e;/*将当前顶点的指针指向e*/
                              /*该语句相当于单链表中更新第一个结点*/
  /*因为是无向图*/
  e = (EdgeNode*)malloc(sizeof(EdgeNode));/*向内存申请空间,生成边表结点*/
  e->adjvex = i;/*邻接点序号i*/
  e->next = G->adjList[j].firstedge;/*将e指针指向当前顶点指向的结点*/
  G->adjList[j].firstedge = e;/*将当前顶点的指针指向e*/
 }
}                           

对于n个顶点和e条边的图,该算法的时间复杂度为O(n+e)。
以图7-4-6为例,来说明该算法中边表的创建过程:
在这里插入图片描述
当k=0时,i=0,j=1时,创建V0边表的第一个结点,创建V1边表的第一个结点;
当k=1时,i=0,j=2时,创建V0边表的第二个结点,创建V2边表的第一个结点;
当k=2时,i=0,j=3时,创建V0边表的第三个结点,创建V3边表的第一个结点;
当k=3时,i=1,j=2时,创建V1边表的第二个结点,创建V2边表的第二个结点;
当k=3时,i=2,j=3时,创建V0边表的第三个结点,创建V3边表的第二个结点。

(3)十字链表

充分利用现有资源,正向思维、逆向思维、整合思维可以创造更大价值。
对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须遍历整个图才知道,反之,逆邻接表解决了入度却不了解出度的情况。如果将邻接表与逆邻接表结合起来,则会解决上述问题。这就是要讲的有向图的另一种存储方法:十字链表(Orthogonal List)。
在这里插入图片描述
在这里插入图片描述
上图中最下边的"taillink为空"应该是“headlink”为空。
在这里插入图片描述
十字链表的好处是因为它把邻接表和逆邻接表整合在了一起,这样既容易找到以Vi结尾的弧,也容易找到以Vi为头的弧,因而容易求得顶点的出度和入度。而且它除了结构复杂一点以外,其实创建图算法的时间复杂度和邻接表想同,因此,在有向图的应用中,十字链表是非常好的数据结构模型。

/*图的十字邻表结构*/
typedef struct OrthEdgeNode/*边表结点*/
{
    
    
 int tailvex;/*弧起点在顶点表的下标*/
 int headvex;/*弧终点在顶点表的下标*/
 EdgeType weight;/*用于存储权值,对于非网图可以不需要*/
 struct OrthEdgeNode* headlink;/*入边表指针域*/
 struct OrthEdgeNode* taillink;/*出边表指针域*/
}OrthEdgeNode;
typedef struct OrthVertexNode/*顶点表结点*/
{
    
    
 VertexType data;/*顶点域,存储顶点信息*/
 OrthEdgeNode* firstin;/*入边表头指针*/
 OrthEdgeNode* firstout;/*出边表头指针*/
}OrthVertexNode,OrthList[MAXVEX];
typedef struct/*图的十字邻表存储结构*/
{
    
    
 OrthList orthList;/*顶点数组*/
 int numVertexes, numEdges;/*图中当前顶点数和边数*/
}GraphOrthList;
/*建立图的十字邻表结构*/
void CreateOrthGraph(GraphOrthList* G)
{
    
    
 int i, j, k;
 OrthEdgeNode* e;/*边表结点*/
 printf("输入顶点数和边数:\n");
 scanf("%d,%d", &G->numVertexes, &G->numEdges);/*输入顶点数和边数*/
 /*建立顶点表*/
 for (i = 0; i < G->numVertexes; i++)/*输入顶点信息,建立顶点表*/
 {
    
    
  scanf(&G->orthList[i].data);/*输入顶点信息*/
  G->orthList[i].firstin = NULL;/*将入边表置为空表,因为此时还没指向任何存在内存上的东西*/
  G->orthList[i].firstout = NULL;/*将出边表置为空表,因为此时还没指向任何存在内存上的东西*/
 }
 /*建立边表*/
 for (k = 0; k < G->numEdges; k++)
 {
    
    
  printf("输入边<Vi,Vj>上的顶点序号:\n");
  scanf("%d,%d", &i, &j);/*输入边,<Vi,Vj>上的顶点序号*/
  e = (OrthEdgeNode*)malloc(sizeof(OrthEdgeNode));/*向内存申请空间,生成边表结点*/
  /*应用单链表中的头插法*/
  e->tailvex = i;
  e->headvex = j;
  e->taillink = G->orthList[i].firstout;
  e->headlink = G->orthList[j].firstin;
  G->orthList[i].firstout = e;/*出度*/
  G->orthList[j].firstin = e;/*入度*/ 
 }
}

(4)邻接多重表

如果我们在无向图的应用中,关注的重点是顶点,那么邻接表是不错的选择,但如果我们更关注边的操作,比如对已访问过的边做标记,删除某一条边等操作,那就意味着,需要找到这条边的边表结点进行操作,这其实还是比较麻烦的。如下图,若要删除左图的(V0,V2)这条边,需要对邻接表结构中右边表的阴影两个结点进行删除操作,显然这是比较麻烦的。
在这里插入图片描述
因此,我们也仿照十字链表的方式,对边表结点的结构做一些改造,也许就可以避免刚才提到的问题。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
理解上图中的连线:例如下标域与指针域一一对应,同一结点中ivex和ilink对应,jvex和jlink对应,不同结点,看下标值。例如,顶点0的边表,其下标为0,如果是ivex为0,则ilink应该指向另一个结点中为0的下标域,,例如顶点V3的边界点的jvex为0,故顶点V0的ilink指向jvex。如果顶点V0还有其他边,则顶点V3的边界结点中的jlink继续指向下一结点中下标为0的下标域,可能是ivex,也可能是jvex。
邻接多重表与邻接表的差别仅仅是在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。这样对边的操作就方便多了,若要删除左图的(V0,V2)这条边,只需要将右图的6和9指向改为空即可。
邻接多重表的各种基本操作的实现与邻接表相似。

/*建立图的邻接表结构*/
void CreateMultiMGraph(GraphMultiAdjList* G)
{
    
    
 int i, j, k;
 MultiEdgeNode* 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 = (MultiEdgeNode*)malloc(sizeof(MultiEdgeNode));/*向内存申请空间,生成边表结点*/
  /*应用单链表中的头插法*/
  e->ivex = i;
  e->jvex = j;
  e->ilink = G->adjList[i].firstedge;
  e->jlink = G->adjList[j].firstedge;
  G->adjList[i].firstedge = e;/*顶点i的边表*/         
  G->adjList[j].firstedge = e;/*顶点j的边表*/
 }
}

(5)边集数组

边集数组是由两个一维数组组成。一个存储顶点信息;另一个存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权值(weight)组成,如下图。显然边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个数组,效率并不高。因此,它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。关于边集数组的应用将在库鲁斯卡尔算法中介绍。
在这里插入图片描述

4、图的遍历

图的遍历和树的遍历类似,我们希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程叫作图的遍历(Traversing Graph)。
图的遍历比较复杂,因为它的任一顶点都有可能和其余的所有顶点相邻接,极有可能存在沿着某条路径搜索之后,又回到原点,而有些顶点却还没有遍历到的情况。因此,我们需要在遍历过程中,把访问过的顶点打上标记,以避免访问多次而不自知。具体办法是设置一个访问数组visited[n],n是图中顶点的个数,初值为0,访问过后设置为1。
对于图的遍历来说,如何避免因回路陷入死循环,就需要科学地设计遍历方案,通常有两种遍历次序方案:深度优先遍历和广度优先遍历。

(1)深度优先遍历

深度优先遍历(Depth_First_Search),,也称为深度优先搜索,简称DFS。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如果用的是邻接矩阵的存储方式,则代码如下:

typedef int Boolean;/*Boolean是布尔类型,其值是TRUE或FALSE*/
#define TRUE 1;
#define FALSE 1;
#define MAX 100
Boolean visited[MAX];/*访问标志的数组*/
/*邻接矩阵的深度优先递归算法*/
void DFS(MGraph G,int i)
{
    
    
 int j;
 visited[i] = TRUE;
 printf("%c", G.vexs[i]);/*打印顶点,也可以其他操作*/
 for (j = 0; j < G.numVextexes; j++)
 {
    
    /*遍历顶点*/
  if (1 == G.arc[i][j] && !visited[j])
  {
    
    /*下标为i的顶点和下标为j的顶点相连且下标为j的顶点没有被标记过*/
   DFS(G,j);/*对为访问的邻接顶点递归调用*/
  }
 }
}
/*邻接矩阵的深度遍历操作*/
void DFSTraverse(MGraph G)
{
    
    
 int i;
 for (i = 0; i < G.numVextexes; i++)
  visited[i] = FALSE;/*初始所有顶点状态都是未访问过的状态*/
 for (i = 0; i < G.numVextexes; i++)
 {
    
    /*遍历顶点*/
  if(!visited[i])/*对未访问过的顶点调用DFS,若是连通图,则只会执行一次*/
      DFS(G,i);
 }
}
/*非递归方式*/
void MyDFSTraverse(MGraph G)
{
    
    
 int i;
 int j;
 int k;
 for (i = 0; i < G.numVextexes; i++)
  visited[i] = FALSE;/*初始所有顶点状态都是未访问过的状态*/
 for (i = 0; i < G.numVextexes; i++)
 {
    
    /*遍历顶点*/
  k = i;
  if (!visited[k])/*当前顶点未访问过*/
  {
    
    
   visited[k] = TRUE;/*将当前顶点置为访问过的状态*/
   printf("%c", G.vexs[k]);/*打印顶点,也可以其他操作*/
   for (j = 0; j < G.numVextexes; j++)
   {
    
    
    if (1 == G.arc[k][j] && !visited[j])
    {
    
    
     visited[j] = TRUE;/*将下标为j的顶点置为访问过的状态*/
     printf("%c", G.vexs[j]);/*打印下表为j的顶点,也可以其他操作*/
     k = j;/*继续遍历下标为j的邻接顶点*/
     j = -1;
    }
   }
  }
 }
}

上述非递归方式并没有达到预期效果,因为只实现向前,而没有实现返回。
代码的执行过程,其实就是我们刚才迷宫找寻所有结点的过程。
如果图结构是邻接表结构,那么DFSTraverse函数的代码几乎是相同的,只是在递归函数中,因为将数组换成了链表而有不同,代码如下:

/*邻接表的深度优先遍历算法*/
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->next;/*上一级递归的p为空时调用,此时说明上一顶点不与其他顶点存在边*/
 }
}
/*邻接表的深度遍历操作*/
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);
 }
}

对比两个存储结构的深度优先遍历代码,对于n个顶点和e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点的邻接点需要访问矩阵中的所有元素,因此都需要O(n²)的时间。而邻接表做存储结构时,找邻接点所需的时间取决于顶点和边的数量,所以是O(n+e)。显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。
对于有向图而言,由于它只是对通道存在可行或不可行,算法上没有变化,完全是通用的(注:是指对于邻接表这种存储结构,按入度或出度遍历)。

(2)广度优先遍历

广度优先遍历(Breath_First_Search),又称为广度优先搜素,简称BFS。
在这里插入图片描述
如果说图的深度优先遍历类似于树的前序遍历,那么图的广度优先遍历则类似于树的层次遍历。
在这里插入图片描述
在这里插入图片描述
邻接矩阵结构的广度优先遍历算法:

/*邻接矩阵的广度遍历算法*/
void BFSTraverse(MGraph G)
{
    
    
 int i, j;
 Queue Q;
 for (i = 0; i < G.numVextexes; i++)
  visited[i] = FALSE;/*初始化标记数组*/
 InitQueue(&Q);/*初始化一辅助用的队列*/
 for (i = 0; i < G.numVextexes; i++)
 {
    
    /*对每一个顶点做循环*/ 
  /*避免图为非连通图,如果是连通图,如图7-5-3,则i为0时就能遍历所有结点*/
  if (!visited[i])
  {
    
    /*若是未被访问过就做处理*/
   visited[i] = TRUE;/*标记该顶点*/
   printf("%c",G.vexs[i]);/*打印顶点,也可以是其他操作*/
   EnQueue(&Q,i);/*将此顶点的下标入队列*/ 
        /*如果为连通图,则该顶点为起点,如图7-5-3中的顶点A*/
   while (!QueueEmpty(Q))/*若当前队列不为空*/
   {
    
    
    DeQueue(&Q,i);/*将队中元素出队列,赋值给i*/
                  /*队列的先进先出保证了先访问本层所有顶点再访问下一层顶点,*/
                  /*因为本层顶点先入队*/
    for (j = 0; j < G.numVextexes; j++)
    {
    
    
     /*判断其他顶点若与当前顶点存在边且未被访问过*/
     if (1 == G.arc[i][j] & !visited[j])
     {
    
    
      visited[j] = TRUE;/*将找到的此顶点标记为访问*/
      printf("%c",G.vexs[j]);/*打印此顶点*/
      EnQueue(&Q,j);/*将找到的此顶点入队列*//*将顶点i的所有边顶点入队*/
     }
    }
   }
  }
 }
}

邻接表的广度优先遍历,代码与邻接矩阵差异不大,如下:

/*邻接表的广度遍历算法*/
void BFS(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]);
   EnQueue(&Q,i);
   while (!QueueEmpty(Q))
   {
    
    
    DeQueue(&Q,i);
    p = GL.adjList[i].firstedge;/*获得当前顶点边表链表头指针*/
    while (p)
    {
    
    
     if (!visited[p->adjvex])
     {
    
    
      visited[p->adjvex] = FALSE;
      printf("%c", GL.adjList[p->adjvex].data);
      EnQueue(&Q,i);    
     }
     p = p->next;/*指针指向下一邻接点*/ 
        /*与while循环一起等价于邻接矩阵中的for循环,*/
        /*只不过邻接表中每个顶点的循环次数不定*/
    }
   }
  }
 }
}

对比图的深度优先遍历和广度优先遍历算法,可以发现,它们在时间复杂度上是一样的,不同之处仅仅在于对顶点的访问顺序不同。可见两者在全图遍历上是没有优劣之分的,只是视不同的情况选择不同的算法。
不过如果图顶点和边非常多,不能在短时间内遍历完成,遍历的目的是为了寻找合适的顶点,,那么选择哪种遍历就要仔细斟酌了。深度优先更适合目标比较明确,以找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。

5、最小生成树

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
找连通图得最小生成树,经典的有两种算法,普里姆算法和克鲁斯卡尔算法。

(1)普里姆(Prim)算法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
个人理解的普里姆算法:
在具有n个顶点的连通图中找权值最小的n-1条边构成最小生成树,也就是在n✖n邻接矩阵中找除对角线外边不同的n-1个最小元素。具体方法如下:在n个顶点中找一个顶点作为起点,以图7-6-3中的图为例,这里假设以V0为顶点,在与V0有边的顶点中找权值最小的顶点,即在邻接矩阵的V0行中找数值最小的元素,也即在程序中的lowcost数组中找数值最小的元素,结果为10,即边(V0,V1);比较V1行的元素和lowcost数组,符合条件则更新lowcost数组中的元素值,并用adjvex数组记录更新的元素在邻接矩阵中的位置;继续在lowcost数组中找最小权值的元素……重复以上操作,直到找到最小权值的n-1条边。
lowcost数组的作用:保存访问过的顶点的对应位置处的最小权值边,从所有这些最小权值中找出数值最小的元素,即可得到下一条最小权值边和下一个作为查找依据的顶点。
adjvex数组的作用:保存lowcost数组中更新的元素在邻接矩阵中的位置,通过该位置可知道构成最小权值边的两顶点。
添加个人理解的普里姆算法代码:

/*普里姆算法生成最小生成树*/
/*以图7-6-3为例*/
void MiniSpanTree_Prim(MGraph G)
{
    
    
 int min, i, j,k;
 int adjvex[MAXVEX];/*保存相关顶点下标*/
                    /*在该数组中,假设adjvex[i]=j,则顶点Vi和顶点Vj在最小生成树中,*/
        /*且连接Vi和Vj的边符合最小权值要求。*/
        /*如果j不为0,那么其含义是矩阵中第j行的第i个元素对应的边符合最小权值。*/
                    /*如果j为0,那么表示第0行的第j个元素对应的边符合最小权值,*/
                    /*且下标为j的顶点在最小生成树中只有一条边。*/
                    /*遍历该下标及其对应数值可得G的最小生成树。*/
 int lowcost[MAXVEX];/*保存相关顶点间边的权值。*/
                     /*保存每一次循环后的最小权值。*/
 lowcost[0] = 0;/*初始化第一个权值为0,即V0加入生成树*/
                /*lowcost的值为0,在这里就是此下标的顶点已经加入生成树*/
 adjvex[0] = 0; /*初始化第一个顶点下标为0*/
 /*该for循环起初始化作用,因为lowcost中有初值才能比较*/
 for (i = 0; i < G.numVextexes; i++)
 {
    
    /*循环除下标为0外的全部顶点*/
  lowcost[i] = G.arc[0][i];/*将V0顶点与之有边的权值加入lowcost数组*/
  adjvex[i] = 0;/*初始化都为V0的下标*/
 }
 for (i = 1; i < G.numVextexes; i++)
 {
    
    /*矩阵列*/
  min = INFINITY;/*初始化最小权值为∞*/
                 /*通常设置为不可能的大数字,如32767和65535等*/
  j = 1; 
  k = 0;
  while (j < G.numVextexes)
  {
    
    /*循环全部顶点*/ 
   /*1==i时,lowcost数组中为与顶点V0有边的顶点的权值,该循环的最终结果为k=1,min=10*/
   if (0 != lowcost[j] && lowcost[j]<min)
   {
    
    /*如果权值不为0且权值小于min,因为0是顶点与其自身*/
    min = lowcost[j];/*则让当前权值成为最小值*/
    k = j;/*将当前最小值的下标存入k*/
   }
   j++;
  }
  printf("%d,%d",adjvex[k],k);/*打印当前顶点中权值最小边*/
  lowcost[k] = 0;/*将当前顶点的权值置为0,表示此顶点已完成任务*/
                 /*接下来获得与顶点Vk有边的其他顶点的权值中最小的权值*/
                 /*避免无向图重复纳入边*/
  for (j = 1; j < G.numVextexes; j++)
  {
    
    /*循环所有顶点*/ /*矩阵行*/
   if (0 != lowcost[j] && G.arc[k][j] < lowcost[j])
   {
    
    /*若下标为k的顶点各边权值小于(即条件G.arc[k][j] < lowcost[j])
    /*此前未被加入生成树(即条件0 != lowcost[j])的权值。*/
    lowcost[j] = G.arc[k][j];/*将较小权值存入lowcost。*/
                              /*更新权值。*/
    adjvex[j] = k;/*将下标为k的顶点存入adjvex*/
   }
   /*如果退出for中不更新权值,则下一次的while循环继续从未更新的Lowcost数组*/
   /*中获得最小值。此时的low数组中的元素值是否可能都为0呢,答案是不可能的,*/
   /*因为G为连通图,不可能存在一个顶点与所有顶点都不存在边的情况。*/
  }
  /*当lowcost中所有元素都为0时,即所有顶点都已放入最下生成树中,上面的for循环不起作用。*/
  /*既然如此,为什么不将for循环放到while循环的上面?*/
  /*因为while循环得到当前lowcost数组中最小权值的顶点,*/
  /*而for循环根据得到的顶点继续更新lowcost数组中的权值。*/
 }
}

在这里插入图片描述
上述V即为P。
由算法代码中的循环嵌套可得知此算法的时间复杂度为O(n²)。

(2)克鲁斯卡尔算法(Kruskal)

现在我们来换一种思考方式,普里姆算法是以某一顶点为起点,逐步找各顶点上最小权值的边来构建最生成树的。这就像我们如果去参观某个展会,例如世博会,你从一个入口进去,然后找你所在位置周边的场馆中你最感兴趣的场馆观光,看完后再用同样的办法看下一个。但也可以选择事先计划好,进园后直接去你最兴趣的场馆观光。
同样的思路,我们可以直接就以边为目标去构建,因为权值就在边上,直接去找最小权值的边来构建生成树也是很自然的想法,只不过构建时要考虑是否会形成环路而已。此时我们就用到了图的存储结构中的边集数组结构。以下是edge边集数组结构的定义代码:

/*对边集数组Edge结构的定义代码*/
typedef struct
{
    
    
 int begin;
 int end;
 int weight;
}Edge;

将图7-6-3的邻接矩阵通过程序转化为图7-6-7的右图的边集数组,并且对它们按权值从小到大排序。
在这里插入图片描述
于是克鲁斯卡尔算法代码如下,
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

/*Kruskal算法生成最小生成树*/
void MinSpanTree_Kruskal(MGraph G)/*生成最小生成树*/
{
    
    
 int i, n, m;
 Edge edges[MAXEDGE];/*定义边集数组*/
 int parent[MAXVEX];/*定义一数组用来判断边与边是否形成回路*/
 /*将邻接矩阵G转换为边集数组edges并按权由小到大排序*/
 /*将邻接矩阵转换为边集数组*/
 int j;
 int k = 0;
 for (i = 0; i < G.numVextexes; i++)
 {
    
    
  for (j = i+1; j < G.numVextexes; j++)
  {
    
    /*避免重复*/
   if (INFINITY != G.arc[i][j])
   {
    
    /*有边*/
    edges[k].begin =i;
    edges[k].end = j;
    edges[k].weight = G.arc[i][j];
   }
  }
 }
 /*边集数组中的权由小到大排序*/
 for (i = 0; i < G.numEdges; i++)
 {
    
    
  int min = i;
  for (j = i + 1; j < G.numEdges; j++)
  {
    
    
   if (edges[min].weight > edges[j].weight)
   {
    
    
    min = j;
   }
  }
  if (min != i)
  {
    
    
   int beginTemp = edges[i].begin;
   int endTemp = edges[i].end;
   int weightTemp = edges[i].weight;
   edges[i].begin = edges[min].begin;
   edges[i].end = edges[min].end;
   edges[i].weight = edges[min].weight;
  }
 }
 for (i = 0; i < G.numVextexes; 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)/*加入n与m不等,说明此边没有与现有生成树形成环路*/
  {
    
    
   parent[n] = m;/*将此边的结尾顶点放入下标为起点的parent中。*/
                 /*表示此顶点已经在生成树集合中。*/
   /*打印边集数组中的元素*/
   printf("(%d,%d) %d",edges[i].begin,edges[i].end,edges->weight);
  }
 }
}
/*查找连线顶点的尾部下标*/
int Find(int* parent, int f)
{
    
    
 while (parent[f])
  f = parent[f];
 return f;
}

个人理解:
新边的加入是否使现有的边集合形成回路的判断依据:n是否等于m。因为n等于m相等意味着新边加入使得新边集合变成环,不符合最小生成树的定义。
n等于m时会使边集合变成环:因为n等于m时,从一个顶点出发经过边集合中不同的边会回到该顶点。
parent数组的作用:判断新加入的顶经过现有边集合后是否回到该顶点。parent数组之所以能起到该作用是因为该数组中的下标和其元素值要么对应一条边的两个顶点的下标,要么是两个顶点经过现有边之后各自的终止顶点。

假设N=(V,{E})是连通网,则令最小生成树的初始状态为只有n个顶点而无边的初始的非连通图,T=(V,{})。图中每个顶点自成一个连通分量。在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍弃此边而选择下一条代价最小的边。依次类推,直至T中所有顶点都在同一连通分量上为止。
此算法的Find函数由边数e决定,时间复杂度为O(loge),而外面有一个for循环e次所以克鲁斯卡尔的时间复杂度为O(eloge)。
对比两个算法,克鲁斯卡尔主要针对边来展开,边数少时效率会非常高,所以对稀疏图有很大的优势;而普里姆算法对于稠密图,即边数非常多的情况会更好一些。

6、最短路径

在网图和非网图中,最短路径的含义是不同的。由于非网图它没有边上的权值,所谓最短路径,就是两顶点之间经过的边数最少的路径;而对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点。显然,我们研究网图更有实际意义,就地图来说,距离就是两顶点间的权值之和。而非网图完全可以理解为所有的边的权值都为
1的网。
这里有两种求最短路径的算法,第一种是从某个源点到其余各顶点的最短路径的问题。

(1)迪杰斯特拉(Dijkstra)算法

这是一个按路径长度递增的次序产生最短路径的算法。它的思路大体是这样的。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从以上例子可以发现,迪杰斯特拉算法并不是一下子求出V0到V8的最短路径,而是一步步求出它们之间顶点的最短路径,过程中都是基于已经求出的最短路径的基础之上,求得更远顶点的最短路径,最终得到想要的结果。
该算法的代码如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

/*迪杰斯特拉算法*/
typedef int pathmatirx[MAXVEX];/*用于存储最短路径下标的数组*/
typedef int ShortPathTable[MAXVEX];/*用于存储到各点最短路径的权值和*/
/*求有向网G的V0顶点到其余顶点V的最短路径P[V]及带权长度D[V]*/
/*P[V]的值为前驱顶点下标,D[V]表示V0到V的最短路径长度和*/
/*由P(V)的下标和其元素值可得最短路径*/
void ShortestPath_Dijkatra(MGraph G,int v0,pathmatirx* P,ShortPathTable* D)
{
    
    
 int v, w, k, min;
 int final[MAXVEX];/*final[w]=1表示求得V0到W的最短路径*/
 for (v = 0; v < G.numVextexes; v++)/*初始化数组*/
 {
    
    
  final[v] = 0;
  (*P)[v] = 0;/*初始化路径数组P为0*/
  (*D)[v] = G.arc[v0][v];/*将与v0有连线的顶点加上权值*/
 }
 (*D)[v0] = 0;/*v0至v0路径为0*/
 final[v0] = 1;/*v0至v0不需要求路径*/
 /*开始主循环,每次求得v0到某个顶点v的最短路径*/
 for (v = 1; v < G.numVextexes; v++)
 {
    
    
  min = INFINITY;/*当前所知离v0顶点的最近距离*/
  for (w = 0; w < G.numVextexes; w++)/*寻找离v0最近的顶点*/
  {
    
    
   if (!final[w] && (*D)[w] < min)
   {
    
    
    k = w;
    min = (*D)[w];/*w顶点离v0顶点更近*/
   }
  }
  final[k] = 1;/*将目前找到的最近的顶点(k顶点)置为1*/
  for (w = 0; w < G.numVextexes; 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;
   }
  }
 }
}

(2)佛洛依德(Floyd)算法

在这里插入图片描述
在这里插入图片描述
在最短路径中,V2的前驱是V0。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

/*佛洛依德算法*/
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, int v0, pathmatirx* P, ShortPathTable* D)
{
    
    
 int v, w, k;
 for (v = 0; v < G.numVextexes; v++)/*初始化D与P*/
 {
    
    
  for (v = 0; v < G.numVextexes; v++)
  {
    
    
   (*D)[v][w] = G.arc[v][w];/*D[v][w]的值即为对应点间的权值*/
   (*P)[v][w] = w;/*初始化P*/
  }
 }
 for (k = 0; k < G.numVextexes; k++)
 {
    
    
  for (v = 0; v < G.numVextexes; v++)
  {
    
    
   for (w = 0; w < G.numVextexes; w++)
   {
    
    
    if ((*D)[v][w] > ((*D)[v][k] + (*D)[v][k]))
    {
    
    /*如果经过下标为k顶点路径比原顶底间路径更短*/
     /*将当前两点间权值设为更小的一个*/
     (*D)[v][w] = (*D)[v][k] + (*D)[v][k];
     (*P)[v][w] = (*P)[v][k];/*路径设置经过下标为k的顶点*/
    }
   }
  }
 }
 /*最短路径的显示代码*/
 for (v = 0; v < G.numVextexes; v++)
 {
    
    
  for (w = v + 1; w < G.numVextexes; w++)
  {
    
    
   printf("v%d-w%d weight",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",w);/*打印终点*/
  }
  printf("\n");
 }
}

再次回过头看看佛洛依德算法,它的代码简介到就是一个二重循环初始化加一个三重循环权值修正,就完成了所有顶点到所有顶点的最短路径计算,时间复杂度为O(n)。如果需要求所有顶点到所有顶点的最短路径问题时,佛洛依德算法应该是不错的选择。
虽然上面求最短路径的两个算法都是以无向图举例,但它们对有向图依然有效,因为两者的差异仅仅是邻接矩阵是否对称而已。

7、拓扑排序

上述介绍了两个有环的图的应用,现在介绍无环图的应用。无环,即是图中没有回路的意思。

(1)拓扑排序介绍

在这里插入图片描述
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网(Activity On Vertex Network)。AOV网中的弧表示活动之间存在某种制约关系。比如演职人员确定了,场地也联系好了,才可以开始进场拍摄。另外就是AOV网中不能存在回路。刚才已经举例了,让某个活动的开始要以自己完成作为先决条件,显然是不可以的。
在这里插入图片描述
设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列V1,V2,……,Vn,满足若从顶点Vi到Vj有一条路径,则在顶点序列中,顶点Vi在顶点Vj之前。则我们称这样的顶点序列为一个拓扑序列。
在这里插入图片描述
所谓拓扑排序,其实就是对一个有向图构造拓扑序列的过程。构造时会有两个结果,如果此网的全部顶点被输出,说明它是不存在环(回路)的AOV网;如果输出顶点数少了,哪怕是少了一个,也说明这个网存在环(回路),不是AOV网。
一个不存在回路的AOV网,我们可以将它应用在各种各样的工程或项目的流程图中,满足各种应用场景的需要,所以实现拓扑排序的算法就很有价值。

(2)拓扑排序算法

对AOV网进行拓扑排序的基本思路是:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧,继续重复此步骤,直至输出所有顶点或者AOV网中不存在入度为0的顶点为止。
首先我们需要确定一下这个图需要使用的数据结构。前面求最小生成树和最短路径时,我们用的都是邻接矩阵,但由于拓扑排序的过程中,需要删除顶点,显然用邻接表会更加方便。因此我们需要为AOV网建立一个邻接表。考虑到在算法过程中始终都要查找入度为0的顶点,我们在原来顶点表结构中,增加一个入度域in,结构如下标,其中in就是入度的数字。
在这里插入图片描述
在这里插入图片描述
在拓扑排序算法中,涉及的结构代码如下:

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;

在算法中,我们还需要辅助的数据结构——栈,用来存储处理过程中入度为0的顶点,目的是为了避免每个查找时都要去遍历顶点表找与没有入度为0的顶点。
拓扑排序算法代码如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

/*拓扑排序,若GL无回路,则输出拓扑排序序列,并返回OK,若有回路则返回ERROR*/
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 (0 == GL->adjList[i].in)/*如果入度为0*/
   stack[++top] = i;/*将入度为0的顶点入栈*/
 while (0 != top)
 {
    
    /*存在入度为0的顶点*/
  gettop = stack[--top];/*出栈*/
  printf("%d->",GL->adjList[gettop].data);
  count++;/*统计输出顶点数*/
  for (e = GL->adjList[gettop].firstedge; e; e->next)
  {
    
    /*对此顶点弧表遍历*/
   k = e->adjvex;
   if (0 != --GL->adjList[k].in)/*将k号顶点邻接点的入度减1(删除弧后入度减1)*/
    stack[++top] = k;/*若为0,则入栈,以便于下次循环输出*/
  }
 }
 if (count < GL->numVertexes)/*如果count小于顶点数,说明存在环*/
  return ERROR;
 else
  return OK;
}

8、关键路径

在这里插入图片描述
在这里插入图片描述
因此,我们如果要对一个流程图获得最短时间,就必须要分析它们的拓扑关系,并且找到当中最关键的流程,这个流程的就是最短时间。
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE网(Activity On Edge Network)。把AOE网中没有入边的顶点称为起点或源点,没有出边的顶点称为终点或汇点。由于一个工程,总有一个开始,,一个结束,所以正常情况下,AOE网只有一个源点一个汇点。
在这里插入图片描述
既然AOE网是表示工程流程的,所以它就具有明显的工程特性。如在某点所代表的工程事件发生之后,从该顶点出发的各活动才能开始。只有在进入某顶点的各活动都已经结束,该点所代表的事件才能发生。
尽管AOV网和AOE网都是用来对工程建模的,但它们还是有很大的不同,主要体现在AOV网是顶点表示活动的网,它只描述活动之间的制约关系,而AOE网是边表示活动的网,边上的权值表示活动持续的时间,如下图所示两图的对比。因此,AOE网是建立在活动之间制约关系没有矛盾的基础之上,再来分析完成整个工程至少需要多少时间,或者为缩短完成工程所需时间,应当加快哪些活动等问题。
在这里插入图片描述

把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径称为关键路径,在关键路径上的活动称为关键活动。
在这里插入图片描述
在这里插入图片描述

(1)关键路径算法原理

在这里插入图片描述
在这里插入图片描述
也就是说,只要找到活动的最早开始时间和最晚开始时间,并且比较它们。如果它们相等,则意味着此活动是关键活动,活动间的路径为关键路径。如果不等,则就不是。
在这里插入图片描述

(2)关键路径算法

在这里插入图片描述
在这里插入图片描述

求时间的最早发生时间etv的过程,就是从头至尾找拓扑序列的过程,因此,在求关键路径之前,需要先调用一次拓扑序列算法的代码来计算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));
 top2 = 0;/*初始化为0*/
 stack2= (int*)malloc(GL->numVertexes * sizeof(int));/*事件最早发生时间*/
 etv= (int*)malloc(GL->numVertexes * sizeof(int));
 for (i = 0; i < GL->numVertexes; i++)
  etv[i] = 0;/*初始化为0*/
 for (i = 0; i < GL->numVertexes; i++)
  if (0 == GL->adjList[i].in)/*如果入度为0*/
   stack[++top] = i;/*将入度为0的顶点入栈*/
 while (0 != top)
 {
    
    /*存在入度为0的顶点*/
  gettop = stack[--top];/*出栈*/
  //printf("%d->", GL->adjList[gettop].data);
  stack2[++top2] = gettop;/*将弹出的顶点序号压入拓扑序列的栈*/
  count++;/*统计输出顶点数*/
  for (e = GL->adjList[gettop].firstedge; e; e->next)
  {
    
    /*对此顶点弧表遍历*/
   k = e->adjvex;
   if (0 != --GL->adjList[k].in)/*将k号顶点邻接点的入度减1(删除弧后入度减1)*/
    stack[++top] = k;/*若为0,则入栈,以便于下次循环输出*/
   if ((etv[gettop] + e->weight > etv[k]))/*以顶点k为弧尾的顶点到顶点k的最长时间,因为要等前一个事件和活动发生完之后,才可以发生本事件*/
    etv[k] = etv[gettop] + e->weight;
  }
 }
 if (count < GL->numVertexes)/*如果count小于顶点数,说明存在环*/
  return ERROR;
 else
  return OK;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

/*求关键路径,GL为有向图,输出GL的各项关键活动*/
void CriticalPath(GraphAdjList GL)
{
    
    
 EdgeNode *e;
 int i, gettop, k, j;
 int ete, lte;/*活动最早发生时间和最迟发生时间变量*/
 TopologicalSort(GL);/*求拓扑序列,计算数组etv和stack2的值*/
 ltv = (int*)malloc(GL->numVertexes * sizeof(int));
 for (i = 0; i < GL->numVertexes; i++)
  ltv[i] = etv[GL->numVertexes - 1];/*初始化ltv,等于拓扑序列中最后一个事件的最早发生时间
            因为最后一个事件是汇点,没有出度,也即不受下一事件和活动的制约,
            所以其最早完成时间等于最晚完成时间*/
 while (0!=top2)/*计算ltv*/
 {
    
    
  gettop = stack2[top2--];/*将拓扑序列出栈,后进先出*/
  for (e = GL->adjList[gettop].firstedge; e; e->next)
  {
    
    /*求各顶点事件的最迟发生时间ltv值*/
   k = e->adjvex;
   if ((ltv[k] - e->weight) < ltv[gettop])/*事件gettop是弧尾,事件k是弧头,故先发生事件gettop,再发生事件k。*/
    ltv[gettop] = ltv[k] - e->weight;  /*因为完成活动<gettop,k>需要时间e->weight,
               所以为了不耽误完成事件k,事件gettop不能过晚完成。画图即可理解。*/
  }
  for (j = 0; j < GL->numVertexes; j++)
  {
    
    /*求ete、lte和关键活动*/
   for (e = GL->adjList[j].firstedge; e; e->next)
   {
    
    
    k = e->adjvex;
    ete = etv[j];/*活动最早发生时间,即活动的开始事件的最早发生时间,该事件开始,则该活动开始*/
    lte = ltv[k] - e->weight;/*活动最迟发生时间,因为是弧<Vj,Vk>,活动完成至少需要时间e->weight*/
    if (ete == lte)/*两者相等即在关键路径上*/
     printf("<v%d,v%d length:%d",
      GL->adjList[j].firstedge,GL->adjList[k].data,e->weight);
   }
  }
 }

注:这里的事件或活动开始时间指的是时间,如几点或第几天,而完成活动的时间是时间段。
一个事件的最早发生时间要在其前面用时最长的事件发生完成之后才发生,即取其中最大值;一个事件的最晚发生时间要保证不耽误后面的事件发生,即取其中最小值;一个活动的最早发生时间即是该活动的尾事件完成的完成时间;一个活动的最晚发生时间要不耽误下一事件的发生,即要考虑下一事件的最晚完成时间和完成该活动需要的时间。

在这里插入图片描述
之所以是最小生成树是因为需要把所有省份都考虑进去,然后用最小的代价旅行这些省份。

猜你喜欢

转载自blog.csdn.net/fazstyle/article/details/105917376