数据结构7---图

news2024/11/26 20:30:59

一、定义

对于图的定义,我们需要明确几个注意的地方:一线性表中我们把数据元素叫元素,树中叫结点,在途中数据元素我们则称之为顶点(Vertex)。

对于图的定义,我们需要明确几个注意的地方:

  • 线性表中我们把数据元素叫元素,树中叫结点,在图中数据元素我们则称之为顶点(Vertex)。
  • 线性表可以没有数据元素,称为空表,树中可以没有结点,叫做空树,而图结构在咱国内大部分的教材中强调顶点集合V要有穷非空。
  • 线性表中,相邻的数据元素之间具有线性关系,树结构中,相邻两层的结点具有层次关系,而图结构中,任意两个顶点之间都可能有关系,顶点之闻的逻辑关系用边来表示,边集可以是空的。

1、各种名词的定义:

无向边:

若顶点Vi到V之间的边没有方向,则称这条边为无向边(Edge),用无序偶(Vi,Vj)来表示

有向边:

若从顶点Vi到Vj的边有方向,则称这条边为有向边,也成为弧(Arc),用有序偶<Vi,Vj>来表示,Vi称为弧尾,Vj称为弧头。

简单图:

在图结构中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。

无向完全图:

在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n*(n-1)/2条边。

有向完全图:

在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n*(n-1)条边。

稀疏图和稠密图:

这里的稀疏和稠密是模糊的概念,都是相对而言的,通常认为边或弧数小于n*logn (n是顶点的个数)的图称为稀疏图,反之称为稠密图。

有些图的边或弧带有与它相关的数字,这种与图的边司弧相关的数叫做权(weight),带权的图通常称为网(Network)。

子图:

假设有两个图G1=(V1,E1)和G2=(V2,E2),如果V2cV1,E2EE1,则称G2为G1的子图(Subgraph)。

2、顶点与边的关系

邻接点:

对于无向图G=(V,E),如果边(V1,V2)\epsilonE,则称顶点V1和V2互为邻接点(Adjacent), 即V1和V2相邻接。边(V1,V2)依附(incident)于顶点V1和V2,或者说边(V1,V2)与顶点V1和V2相关联。

度:

顶点V的度(Degree)是和V相关联的边的数目,记为TD(V),如下图,顶点A与B互为邻接点,边(A,B)依附于顶点A与B上,顶点A的度为3。

邻接到顶点:

对于有向图G=(V,E),如果有<V1,V2>\epsilonE,则称顶点V1邻接到顶点V2,顶点V2邻接自顶点V1。

度,入度与出度:

以顶点V为头的弧的数目称为V的入度(InDegree),记为ID(V),以V为尾的弧的数目称为V的出度(OutDegree),记为OD(V),因此顶点V的度为TD(V)=ID(V)+OD(V).

路径:

无向图G=(V,E)中从顶点V1到顶点V2的路径(Path)。

如果G是有向图,则路径也是有向的。
下图用红线列举顶点B到顶点D的两种路径,而顶点A到顶点B就不存在路径啦:

路径的长度:

路径的长度是路径上的边或弧的数目。

回路或环:

第一个顶点到最后一个顶点相同的路径称为回路或环(cycle)。

序列中顶点不重复出现的路径称为简单路径,除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。

3、连通图

在无向图G中,如果从顶点V1到顶点V2有路径,则称V1和V2是连通的,如果对于图中任意两个顶点Vi和Vj都是连通的,则称G是连通图(ConnectedGraph)

连通分量:

无向图中的极大连通子图称为连通分量。注意以下概念:

  1. 首先要是子图,并且子图是要连通的;
  2. 连通子图含有极大顶点数;
  3. 具有极大顶点数的连通子图包含依附于这些顶点的所右边

 上图的图1是一个无向非连通图。但是它有两个连通分量,即图2和图3.而图4尽管是图1的子图,但它却不满足连通子图的极大顶点数。因此它不是图1的无向图的连通分量。

强连通图:

在有向图G中,如果对于每一对Vi到Vj都存在路径,则称G是强连通图。

有向图中的极大强连通子图称为有向图的强连通分量。

连通图的生成树:

所谓的一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。

右图就是一个生成树

有向树:

如果一个有向图恰有一个顶点入度为0,其余顶点的入度均为1,则是一棵有向树。

二、图的存储结构

因为任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系(内存物理位置是线性的,图的元素关系是平面的)。

下面有几种存储结构:

(一)邻接矩阵

1、邻接矩阵(无向图)

考虑到图是由顶点和边或弧两部分组成,合在一起比较困难,那就很自然地考虑到分为两个结构来分别存储。

顶点因为不区分大小、主次,所以用一个一维数组来存储是狠不错的选择。

而边或弧由于是顶点与顶点之间的关系,一维数组肯定就搞不定了,那我们不妨考虑用一个二维数组来存储。

图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。

我们可以设置两个数组,顶点数组为vertex[4]={vO, V1,V2,V3},边数组arc[4][41为对称矩阵(0表示不存在顶点间的边,1表示顶点间存在边)。

对称矩阵:所谓对称矩阵就是n阶矩阵的元满足a[i][j]=a[j][i](0<=i,j<=n)。即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的。

2、邻接矩阵(有向图)

无向图的边构成了一个对称矩阵,貌似浪费了一半的空间,那如果是有向图来存放,会不会把资源都利用得很好呢?

可见顶点数组vertex[4]={v0, V1,V2,V3},弧数组arc[4][4]也是一个矩阵,但因为是有向图,所以这个矩阵并不对称,例如由V1到V0有弧,得到arc[1][0],而V0到V1没有弧,因此arc[0][1]=0。

另外有向图是有讲究的,要考虑入度和出度,顶点V1的入度为1,正好是第V1列的各数之和,顶点V1的出度为2,正好是第V2行的各数之和。

3、邻接矩阵(网)

在图的术语中,我们提到了网这个概念,事实上也就是每条边上带有权的图就叫网。

这里“\infty”表示一个计算机允许的、大于所有边上权值的值。

#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 100
//无向图的邻接矩阵
typedef struct
{
	char data[MAXSIZE];//顶点数组
	int arc[MAXSIZE][MAXSIZE];//邻接矩阵
	int vexnum, edgenum;//当前的定点数和边数
 
}MGraph;
//图的邻接矩阵的创建
void CreateMgrapg(MGraph *p)
{
	int i, j, k;
	printf("输入定点数和边数:");
	scanf("%d %d", &p->vexnum,&p->edgenum);//输入顶点数和边数
	printf("输入所有顶点的信息:");
	for (i=0; i<p->vexnum;i++)
    {
		scanf(" %c",&p->data[i]);
	}
	for (i = 0;i < p->vexnum;i++){
		for (j = 0;j<p->vexnum;j++){
			p->arc[i][j] = 0;
		}
	}
	for (k=0; k < p->edgenum; k++)
	{
		printf("输入第%d边的两个顶点的序号,格式(i j):",k+1);
		scanf("%d %d", &i, &j);
		p->arc[i][j] = 1;
		p->arc[j][i] = 1;//去掉这个可以表示有向图
	}
}
void print(MGraph g)
{
	int i,j;
	for (i = 0; i < g.vexnum; i++)
	{
		for (j = 0; j < g.vexnum; j++)
		{
			printf("%-3d", g.arc[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	MGraph g;
	CreateMgrapg(&g);
	print(g);
	return 0;
}

(二)邻接表

1、邻接表(无向图)

数组与链表结合一起来存储,这种方式在图结构也适用,我们称为邻接表(AdjacencyList)。

邻接表的处理方法是这样:

  1. 图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以较容易地读取顶点信息,更加方便。
  2. 图中每个顶点Vi的所有邻接点构成一个线性表,由于邻接点的个数不确定,所以我们选择用单链表来存储。

2、邻接表(有向图)

若是有向图,邻接表结构也是类似的,我们先来看把顶点当弧尾建立的邻接表,这样很容易就可以得到每个顶点的出度:

但也有时为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表:

此时我们很容易就可以算出某个顶点的入度或出度是多少,判断两顶点是否存在弧也很容易实现。

3、邻接表(网)

对于带权值的网图,可以在边表结点定义中再增加一个数据域来存储权值即可:

#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100
//有向图邻接表的存储
typedef char VertexType;
typedef struct node{//定义边表结点
	int adjvex;//邻接点域
	struct node *next;//指向下一个邻接点域的指针域
}EdgeNode;
typedef struct vexnode{//定义顶点表结点
	VertexType data;//顶点域
	EdgeNode *firstedge;//指向第一条边结点
}VHeadeNode;
typedef struct{
	VHeadeNode adjlist[MAXSIZE];/*邻接表头结点数组*/
	int n,e;//顶点数,边数
}AdjList;
 
//有向图邻接表的创建
void GreateAGraph(AdjList *g){
	int i,j,k;
	printf("请输入图的顶点数和边数:");
	scanf("%d %d",&g->n,&g->e);
	printf("请输入图的所有顶点信息:");
	for(i=0;i<g->n;i++){
		scanf(" %c",&(g->adjlist[i].data));//给图的每个结点的顶点域赋值
		g->adjlist[i].firstedge = NULL;//首先点的边表头指针都设为空
	}
	EdgeNode *p;
	for(k=0;k<g->e;k++){
        printf("请输入第%d条边对应的两个顶点的序号,格式(i j):",k+1);
		scanf("%d %d",&i,&j);
		p = (EdgeNode*)malloc(sizeof(EdgeNode));
		p->adjvex = j;
		p->next = g->adjlist[i].firstedge;//头插法
		g->adjlist[i].firstedge = p;
	}
}
//邻接表的输出
void printadj(AdjList *g){
	int i;
	EdgeNode *p;
	for(i=0;i<g->n;i++){
		printf("%2d [%c]",i,g->adjlist[i].data);
		p = g->adjlist[i].firstedge;
		while(p!=NULL){
			printf("-->[%d]",p->adjvex);
			p = p->next;
		}
		printf("\n");
	}
}
int main() {
	AdjList G;
	GreateAGraph(&G);//创建有向图
	printadj(&G);
	return 0;
}

(三)十字链表(有向图)

结构

即:弧起点的下标     弧终点的下标     入度的指针      出度的指针

蓝线:出度         红线:入度  

十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到以Vi为尾的弧,也容易找到以Vi为头的弧,因而容易求得项点的出度和入度。

(四)邻接多重表

结构

其中iVex和jVex是与某条边依附的两个顶点在顶点表中的下标。iLink指向依附顶点iVex的下一条边,jLink指向依附顶点jVex的下一条边。

也就是说在邻接多重表里边,边表存放的是一条边,而不是一个顶点。

(五)边集数组

边集数组是由两个一维数组构成,一个是存储顶点的信息,另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。

三、图的遍历

谈到图的遍历,那就复杂多了,因为它的任一顶点都可以和其余的所有顶点相邻接,因此极有可能存在重复走过某个顶点或漏了某个顶点的遍历过程。

对于图的遍历,如果要避免以上情况,那就需要科学地设计遍历方案,通常有两种遍历次序方案:化们是深度优先遍历(DFS)和广度优先遍历(BFS)。

(一)深度优先遍历

深度优先遍历(DepthFirstSearch),也有称为深度优先搜索,简称为DFS。

我们可以约定右手原则:在没有碰到重复顶点的情况下,分叉路口始终是向右手边走,每路过一个顶点就做一个记号。

这里规定的就是去小的,当然也可以规定先去大的

那么如何实现呢?

1、邻接矩阵(递归)

#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100 //最大顶点数
#define MAX_INT 32767//设置最大值
typedef struct{
    char vexs[MAXSIZE];//这里的数据类型根据实际情况而定
    int arcs[MAXSIZE][MAXSIZE];//这里的数据类型根据实际情况而定
    int vexnum, arcnum;//图的当前顶点数和边数
}Graph;
int get_index(char* arr,char m)
{
    int i = 0;
    for(i = 0; i < MAXSIZE; i++)
    {
        if(arr[i] == m)return i;
    }
    return -1;
}
 
void CreatGraph(Graph* G)
{
    int i,j = 0;
    printf("请输入顶点和边的数量:>");
    scanf("%d%d",&G->vexnum,&G->arcnum);//把输入的值保存到图结构体中
    for(i = 0; i < G->vexnum; i++)//初始化邻接矩阵
    {
        for(j = 0; j < G->vexnum; j++)
            {
                G->arcs[i][j] = 0;
            }
 
    }
 
    for(i = 0; i < G->vexnum; i++)
    {
        printf("请输入每个顶点的值:>");
        getchar();//清除键盘缓存区的回车键
        scanf("%c", &G->vexs[i]);//把输入的值保存到顶点数组当中
    }
    for(i = 0; i < G->arcnum; i++)//有几条边,循环几次
    {
        char m,n;//用来接收两个顶点
        int j,k;//接收顶点的下标
        printf("请依次输入每一条边,格式为:ab >");
        getchar();
        scanf("%c%c",&m,&n);
        j = get_index(G->vexs,m);//得到输入的m的值,在顶点数组中的下标
        k = get_index(G->vexs,n);//得到输入的n的值,在顶点数组中的下标
        G->arcs[j][k] = 1;//给邻接矩阵对应的位置赋权值
        G->arcs[k][j] = 1;//因为是无向网,所以是对称的,给对称点赋相同值
    }
 
 
}
//深度遍历创建的无向图
void DepthSearch(Graph G, int v, int*visited)//参数为创建的图,输入的值在数组中的下标,判断是否被访问过的数组
{
    int i = 0;
    visited[v] = 1;
    printf("%c", G.vexs[v]);
    for(i = 0; i < G.vexnum; i++)//遍历二维数组v行的每一列
    {
        if((G.arcs[v][i] == 1)&&(visited[i]!=1))//如果有边相连,而且该顶点还没有被访问过
            DepthSearch(G, i,visited);//递归搜索该顶点
    }
}
int main()
{
    Graph G;
    CreatGraph(&G);
    char input;
    int visited[MAXSIZE] = {0};//创建数组,用来判断某一个顶点是否被访问过
    printf("请输入搜索的起始点:>");
    getchar();
    scanf("%c",&input);
    DepthSearch(G, get_index(G.vexs, input),visited);
    return 0;
 
}

2、邻接表

代码与上面邻接矩阵也差不多,只是在条件判断时有所不同,这里不需要判断表结点是不是顶点的边,只需要判断表结点是不是空.递归具体代码如下:

#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100 //最大顶点数
#define MAX_INT 32767//设置最大值
typedef struct TableNode{//表结点
    int index;//结点在数组中的下标
    struct TableNode* nextarc;
    int info;//权值
 
}TNode;
 
typedef struct{//头结点
    char data;
    TNode* firstarc;
}HNode;
 
typedef struct{//整个无向网的结构体
    HNode* head;
    int vexnum,arcnum;
}Gragh;
int get_index(HNode* arr,char m)
{
    int i = 0;
    for(i = 0; i < MAXSIZE; i++)
    {
        if(arr[i].data == m)return i;
    }
    return -1;
}
 
void CreatGraph(Gragh* G)
{
    int i = 0;
    printf("请输入顶点和边的数量:>");
    scanf("%d%d",&G->vexnum,&G->arcnum);//把输入的值保存到图结构体中
 
 
    for(i = 0; i < G->vexnum; i++)
    {
        printf("请输入每个顶点的值:>");
        getchar();//清除键盘缓存区的回车键
        scanf("%c", &G->head[i].data);//把输入的值保存到顶点数组当中
        G->head[i].firstarc = NULL;
    }
    for(i = 0; i < G->arcnum; i++)//有几条边,循环几次
    {
        char m,n;//用来接收两个顶点
        int j,k;//接收顶点的下标
        printf("请依次输入每一条边,格式为:ab >");
        getchar();
        scanf("%c%c",&m,&n);
        j = get_index(G->head,m);//得到输入的m的值,在顶点数组中的下标
        k = get_index(G->head,n);//得到输入的n的值,在顶点数组中的下标
        TNode* P1 = malloc(sizeof(TNode));
        P1->index = k;
        P1->nextarc = G->head[j].firstarc;
        G->head[j].firstarc = P1;
        //因为是无向图,所以还要建立一条反向的边
        TNode* P2 = malloc(sizeof(TNode));
        P2->index = j;
        P2->nextarc = G->head[k].firstarc;
        G->head[k].firstarc = P2;
    }
 
 
}
//深度遍历创建的无向图
void DepthSearch(Gragh G, int v, int*visited)//参数为创建的图,输入的值在数组中的下标,判断是否被访问过的数组
{
    visited[v] = 1;
    printf("%c", G.head[v].data);
    TNode* P = G.head[v].firstarc;
    while(P)
    {
        if(!visited[P->index])DepthSearch(G, P->index, visited);//如果表结点不为空且判断数组值不为1,则递归搜索该结点
        P = P->nextarc;//指针指向该结点的下一个结点
    }
 
}
int main()
{
    Gragh G;
    CreatGraph(&G);
    char input;
    int visited[MAXSIZE] = {0};//创建数组,用来判断某一个顶点是否被访问过
    printf("请输入搜索的起始点:>");
    getchar();
    scanf("%c",&input);
    DepthSearch(G, get_index(G.head, input),visited);
    return 0;
 
}

非递归


void DFS(Graph g,int v){//采用非递归的方式实现图的深度优先遍历
    InitStack(&s);//创建并初始化栈s
    PUSH(&s,v);//待访问结点入栈
    while(!IsEmpty(s)){//栈非空栈顶元素出栈并访问,同时让其所有未被访问的邻结点入栈
        POP(&s,&v);
        visit(v);
        visited[v]=1;
        int w=FirstAdjVex(g,v);
        while(w!=-1){
            if(!visited[w]){
                PUSH(&s,w);
            }
            w=NextAdjVertex(g,v,w);
        }

时间效率

实例(马踏棋盘算法(骑士周游问题))

国际象棋的棋盘为8*8的方格棋盘,现将“马”放在任意指定的方格中,按照“马”走棋的规则将“马”进行移动。要求每个方格只能进入一次,最终使得“马”走遍棋盘64个方格。

编写代码,实现马踏棋盘的操作,要求用1~64来标注“马”移动的路径(看演示)。

对于在n*n的棋盘上,当n>=5且为偶数的时候,以任意点作点都有解。

回溯法

一之前我们谈过回溯法,还是那句话,指导思想很简单,就是一条路走到黑,碰壁了再回来一条路走到黑......一般和递归可以很好的搭配使用,还有深度优先搜索(DFS) 。

哈密尔顿路径:

一图G中的哈密尔顿路径指的是经过图G中每个顶点,且只经过一次的一条轨迹。如果这条轨迹是一条闭合的路径(从起点出发不重复地遍历所有点后仍能回到起始点),那么这条路径称为哈密尔顿回路

62
40
当前位置
51
73

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define X 6
#define Y 6

int chess[X][Y];

//找到基于(x,y)位置下一个可走的位置

int nextxy(int* x, int* y, int count)//传入x,y的地址,可以像全局变量一样修改变量的值。
{
	switch (count)
	{
	case 0:
		if (*x + 2 <= X - 1 && *y - 1 >= 0 && chess[*x + 2][*y - 1] == 0)
		{
			*x += 2;
			*y -= 1;
			return 1;
		}
		break;
	case 1:
		if (*x + 2 <= X - 1 && *y + 1 <= Y - 1 && chess[*x + 2][*y + 1] == 0)
		{
			*x += 2;
			*y += 1;
			return 1;
		}
		break;
	case 2:
		if (*x + 1 <= X - 1 && *y + 2 <= Y - 1 && chess[*x + 1][*y + 2] == 0)
		{
			*x += 1;
			*y += 2;
			return 1;
		}
		break;
	case 3:
		if (*x - 1 >= 0 && *y + 2 <= Y - 1 && chess[*x - 1][*y + 2] == 0)
		{
			*x -= 1;
			*y += 2;
			return 1;
		}
		break;
	case 4:
		if (*x - 2 >= 0 && *y + 1 <= Y - 1 && chess[*x - 2][*y + 1] == 0)
		{
			*x -= 2;
			*y += 1;
			return 1;
		}
		break;
	case 5:
		if (*x - 2 >= 0 && *y - 1 >= 0 && chess[*x - 2][*y - 1] == 0)
		{
			*x -= 2;
			*y -= 1;
			return 1;
		}
		break;
	case 6:
		if (*x - 1 >= 0 && *y - 2 >= 0 && chess[*x - 1][*y - 2] == 0)
		{
			*x -= 1;
			*y -= 2;
			return 1;
		}
		break;
	case 7:
		if (*x + 1 <= X - 1 && *y - 2 >= 0 && chess[*x + 1][*y - 2] == 0)
		{
			*x += 1;
			*y -= 2;
			return 1;
		}
		break;
	}
	return 0;
}

void print() {
	int i, j;
	for (i = 0; i < X; i++) {
		for (j = 0; j < Y; j++) {
			printf("%2d\t", chess[i][j]);
		}
		printf("\n");
	}
	printf("\n");
}


//DFS 深度优先遍历棋盘
//(x,y)为位置坐标
//tag是标记变量,每走一步,tag + 1
int TravelChessBoard(int x, int y, int tag) {
	int x1 = x, y1 = y, flag = 0, count = 0;
	chess[x][y] = tag;
	if (X * Y == tag) {
		print();
		return 1;
	}
	//找到马的下一个可走坐标(x1,y1),如果找到flag = 1,否则为0
	flag = nextxy(&x1, &y1, count);
	while (0 == flag && count < 7) {
		count++;
		flag = nextxy(&x1, &y1, count);
	}

	while (flag) {
		if (TravelChessBoard(x1, y1, tag + 1)) {
			return 1;
		}
		//继续找到马的下一步可走的坐标(x1,y1)...如果找到flag = 1,否则为0
		//前面的八种情况都不行
		x1 = x;//重新赋值是因为上面寻找的时候nextadress已经对x1,y1做改变了
		y1 = y;//退回到前一格(传入nextadress的一格)(回溯1)
		count++;
		flag = nextxy(&x1, &y1, count);
		while (0 == flag && count < 7) {
			count++;
			flag = nextxy(&x1, &y1, count);
		}

	}
	if (0 == flag) {
		chess[x][y] = 0;
		return 0;
	}
}


int main() {
	int i, j;
	clock_t start, finish;
	start = clock();

	for (i = 0; i < X; i++) {
		for (j = 0; j < Y; j++) {
			chess[i][j] = 0;
		}
	}
	if ( TravelChessBoard(2, 0, 1)==0) {
		printf("fail\n");
	}
	finish = clock();
	printf("\n本次计算一共花费:%f秒\n\n", (double)(finish - start) / CLOCKS_PER_SEC);

	return 0;
}

emmm,吃饭去了,回溯的理解,DFS把8个情况可能都走一遍

(二)广度优先遍历

广度优先遍历(BreadthFirstSearch),又称为广度优先搜索,简称BFS。

1、邻接表

#include<stdlib.h>
#include<stdio.h>
#include<malloc.h>
#include<string.h>
//图的邻接表类型定义
typedef char VertexType[4];
typedef char InfoPtr;
typedef int VRType;
#define MaxSize 50   //最大顶点个数 
typedef enum { DG, DN, UN, UG } GraphKind; //图的类型:有向图、有向网、无向图和无向网
typedef struct ArcNode {//边表结点的类型定义 
	int adjvex;// 弧(边)指向的顶点的位置
	InfoPtr* info;//与弧(边)相关的信息 
	struct ArcNode* nextarc;//指示下一个与该顶点相邻接的顶点 
} ArcNode;
typedef struct VNode { //表头结点的类型定义 
	VertexType data;//用于存储顶点
	ArcNode* firstarc;//指示第一个与该顶点邻接的顶点 
}VNode, AdjList[MaxSize];
typedef struct {	//图的类型定义 
	AdjList vertex;
	int vexnum, arcnum;//图的顶点的数目与弧的数目 
	GraphKind kind;//图的类型 
}AdjGraph;
//函数声明
int LocateVertex(AdjGraph G, VertexType v);
void CreateGraph(AdjGraph* G);
void DisplayGraph(AdjGraph G);
void DestroyGraph(AdjGraph* G);
//深度 
void DFSTraverse(AdjGraph G);
void DFS(AdjGraph G, int v);
int FirstAdjVertex(AdjGraph G, VertexType v);
int NextAdjVertex(AdjGraph G, VertexType v, VertexType w);

void BFSTraverse(AdjGraph G);
void CreateGraph(AdjGraph* G) {
	int i, j, k;
	VertexType v1, v2;//定义两个顶点v1和v2
	ArcNode* p;
	printf("请输入图的顶点数,边数(逗号分割):");
	scanf("%d,%d", &(*G).vexnum, &(*G).arcnum);
	printf("请输入%d个顶点的值:\n", G->vexnum);
	for (i = 0; i < G->vexnum; i++) {
		scanf("%s", G->vertex[i].data);
		G->vertex[i].firstarc = NULL;
	}
	printf("请输入弧尾和弧头(以空格作为间隔);\n");
	for (k = 0; k < G->arcnum; k++) {
		scanf("%s%s", v1, v2);
		i = LocateVertex(*G, v1);
		j = LocateVertex(*G, v2);
		//j为入边,i为出边 创建邻接表
		p = (ArcNode*)malloc(sizeof(ArcNode));
		p->adjvex = j;//邻接点域 
		p->info = NULL;//数据域 
		p->nextarc = G->vertex[i].firstarc;//指针域
		G->vertex[i].firstarc = p;
		//i为入边,j为出边 创建邻接表
		p = (ArcNode*)malloc(sizeof(ArcNode));
		p->adjvex = i;//邻接点域 
		p->info = NULL;//数据域 
		p->nextarc = G->vertex[i].firstarc;//指针域
		G->vertex[j].firstarc = p;
	}
	(*G).kind = UG;
}
//查找 
int LocateVertex(AdjGraph G, VertexType v) {
	int i;
	for (i = 0; i < G.vexnum; ++i) {
		if (strcmp(G.vertex[i].data, v) == 0) {
			return i;
		}
	}
	return -1;
}
//销毁无向图 
void DestroyGraph(AdjGraph* G) {
	int i;
	ArcNode* p, * q;
	for (i = 0; i < (*G).vexnum; ++i) {//释放图中的边表结点 
		p = G->vertex[i].firstarc;//p指向边表的第一个结点 
		if (p != NULL) {//如果边表不为空,则释放边表的结点 
			q = p->nextarc;
			free(p);
			p = q;
		}
	}
	(*G).vexnum = 0;//将顶点的数目为0 
	(*G).arcnum = 0;//将边的数目为0 
}
//打印 
void DisplayGraph(AdjGraph G) {
	int i;
	ArcNode* p;
	printf("%d个顶点:\n", G.vexnum);
	for (i = 0; i < G.vexnum; i++) {
		printf("%s", G.vertex[i].data);
	}
	printf("\n%d条边:\n", 2 * G.arcnum);
	for (i = 0; i < G.vexnum; i++) {
		p = G.vertex[i].firstarc;//将p指向边表的第一个结点
		while (p) {
			printf("%s->%s  ", G.vertex[i].data, G.vertex[p->adjvex].data);
			p = p->nextarc;
		}
		printf("\n");
	}
}
int visited[MaxSize];
//打印深度优先遍历的数据 
void Visit(VertexType v) {
	printf("%s ", v);
}
//深度 
void DFSTraverse(AdjGraph G) {
	int v;
	for (v = 0; v < G.vexnum; v++) {
		visited[v] = 0;		//访问标志数组初始化为未被访问 
	}
	printf("\n深度优先遍历序列:\n");
	for (v = 0; v < G.vexnum; v++) {
		if (!visited[v]) {
			DFS(G, v);	//对未访问的顶点v进行深度优先遍历 
		}
	}
	printf("\n");
}
//从顶点v出发递归深度优先遍历 
void DFS(AdjGraph G, int v) {
	int w;
	visited[v] = 1;//访问标志设置为已访问 
	Visit(G.vertex[v].data);
	for (w = FirstAdjVertex(G, G.vertex[v].data); w > 0; w = NextAdjVertex(G, G.vertex[v].data, G.vertex[w].data))
		if (!visited[w])
			DFS(G, w);//递归调用DFS对v的尚未访问的序号为w的邻接顶点 
}
//返回顶点v的第一个邻接顶点的序号 
int FirstAdjVertex(AdjGraph G, VertexType v) {
	ArcNode* p;
	int v1;
	v1 = LocateVertex(G, v);//v1为顶点v在图G中的序号 
	p = G.vertex[v1].firstarc;
	if (p)		//如果顶点v的第一个邻接点的存在,返回邻接点的序号,否则返回-1 
		return p->adjvex;
	else
		return -1;
}
//返回v的相对于w的下一个临界顶点的序号 
int NextAdjVertex(AdjGraph G, VertexType v, VertexType w) {
	ArcNode* p, * next;
	int v1, w1;
	v1 = LocateVertex(G, v);	//v1为顶点v在图G中的序号 
	w1 = LocateVertex(G, w);   //w1为顶点v在图G中的序号 
	for (next = G.vertex[v1].firstarc; next;) {
		if (next->adjvex != w1) {

		}
		next = next->nextarc;
	}
	p = next;			//p指向顶点v的邻接顶点w的结点 
	if (!p || p->nextarc) {//如果w不存在或w是最后一个邻接点,则返回-1 
		return -1;
	}
	else {
		return p->nextarc->adjvex;//返回v的相对于w的下一个邻接点的序号 
	}
}

//广度
void BFSTraverse(AdjGraph G) {
	printf("\n广度优先遍历序列:\n");
	int v, u, w, front, rear;
	ArcNode* p;
	int queue[MaxSize];//定义一个队列Q‘
	front = rear = -1;//初始化队列Q
	for (v = 0; v < G.vexnum; v++) {
		visited[v] = 0;
	}
	v = 0;
	visited[v] = 1;			//设置访问标志为1,表示已经被访问过 
	Visit(G.vertex[v].data);
	rear = (rear + 1) % MaxSize;
	queue[rear] = v;			//v入队 
	while (front < rear) {		//如果队列不为空 
		front = (front + 1) % MaxSize;
		v = queue[front];		//队头元素出队赋值给v 
		p = G.vertex[v].firstarc;
		while (p != NULL) {		//遍历序号为v的所有邻接点 
			if (visited[p->adjvex] == 0) {//如果该顶点未被访问过 
				visited[p->adjvex] = 1;
				Visit(G.vertex[p->adjvex].data);
				rear = (rear + 1) % MaxSize;
				queue[rear] = p->adjvex;
			}
			p = p->nextarc;//p指向下一个邻接点 
		}
	}
}
int main() {
	AdjGraph G;
	printf("采用邻接表创建无向图G:\n");
	CreateGraph(&G);
	printf("输出无向图G:");
	DisplayGraph(G);
	DFSTraverse(G);
	BFSTraverse(G);
	DestroyGraph(&G);
	return 1;
}

 这里比较好理解

就是说利用队列来帮助实现

2、邻接矩阵

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#define MVNUM 100				//最大顶点数
#define MAXINT 32767				//极大值相当于无穷大
 
int visited[MVNUM] = { 0 };			//辅助数组,判断遍历过了没
int visit[MVNUM] = { 0 };			//同理
typedef struct
{
	char vexs[MVNUM];			//顶点数据数组
	int arr[MVNUM][MVNUM];			//邻接矩阵
	int vexnum, arcnum;			//现有顶点数与边数
}AMGraph;							
typedef struct
{
	int* base;				//队列数组
	int front;				//队头的下标
	int rear;				//队尾的下标
}sqQueue;
 
int initGraph(AMGraph& G);			//初始化邻接矩阵
void showGraph(AMGraph G);			//打印邻接矩阵
int locatVex(AMGraph G, char u);		//定位顶点在邻接矩阵的下标
int createGraph(AMGraph& G);			//建立邻接矩阵
void dfsAM(AMGraph G,int i);			//深度优先搜索遍历
void bfsAM(AMGraph G, int i);			//广度优先搜索遍历
int initQueue(sqQueue& Q);			//初始化队列
int enQueue(sqQueue& Q, int i);			//入队
int firstVEX(AMGraph G, int u);			//第一个邻接顶点		
int nextVEX(AMGraph G,int w ,int u);	//下一个邻接顶点
 
int main()
{
	AMGraph G;							
	initGraph(G);
	createGraph(G);
	showGraph(G);
	cout << "the result of dfs is:";
	dfsAM(G,0);
	cout << endl;
	cout << "the result of bfs is:";
	bfsAM(G,0);
}
int initGraph(AMGraph& G)
{
	cout << "please input some vexnum and arcnum!" << endl;
	cin >> G.vexnum >> G.arcnum;			//输入你想要的顶点数和边数
	cout << "please input data of vex!" << endl;
	for (int i = 0; i < G.vexnum; i++)
	{
		cin >>G.vexs[i];			//输入顶点数据
	}
	
	for (int i = 0; i < G.vexnum; i++)
	{
		for (int j = 0; j < G.vexnum; j++)
		{
			G.arr[i][j] = MAXINT;		//邻接矩阵的初值都为无穷大
		}	
	}
		
	return 1;
}
void showGraph(AMGraph G)
{
	for (int i = 0; i < G.vexnum; i++)
	{
		for (int j = 0; j < G.vexnum; j++)
		{
			if (G.arr[i][j] == MAXINT)	//无穷大弄得更好看点
				cout << "∞" << " ";
			else
				cout << " " << G.arr[i][j] << " ";
		}
		cout << endl;
	}
	cout << endl;
}
 
int locateVex(AMGraph G, char u)
{
	for (int i = 0; i < G.vexnum; i++)	
	{
		if (u == G.vexs[i])			//如果u的值和顶点数据匹配,就返回顶点在矩阵中的下标
			return i;
	}
	return -1;
}
 
int createGraph(AMGraph& G)
{
	int i = 0; int j = 0;int w = 0;			//i,j代表顶点下标,w代表权值
	char v1 = 0; char v2 = 0;			//v1,v2为顶点数据
	cout << "please input w of v1 to v2 in graph!" << endl;
	for (int k = 0; k < G.arcnum; k++)
	{
		cin >> v1 >> v2 >> w;			
		i = locateVex(G, v1);			//找到v1在顶点表的下标,并返回赋值给i
		j = locateVex(G, v2);
		G.arr[i][j] = w;
		G.arr[j][i] = G.arr[i][j];		//无向图的矩阵是对称矩阵
	}
 
	cout << endl;
	return 1;
}
 
void dfsAM(AMGraph G, int i)
{//随机选一个顶点下标,这里为0
	cout << G.vexs[i]<<" ";			//输出0下标在顶点表的值	
	visited[i] = 1;				//辅助数组对应下标i的值置为1
	for (int j = 0; j < G.vexnum; j++)
	{
		if (G.arr[i][j] != MAXINT && (!visited[j]))	//只要是邻接的顶点并且没有访问过
		{						//不然就退回,也是递归结束条件
			dfsAM(G, j);				//递归使用函数
		}
	}
}
int initQueue(sqQueue& Q)
{
	Q.base = (int *)malloc(sizeof(int) * MVNUM);	
	//给base动态分配一个int*类型MVNUM个int大小的一维数组空间
	Q.front = Q.rear = 0;	//队头和对尾下标都置为0
	return 1;
}
int enQueue(sqQueue& Q, int i)
{
	if ((Q.rear + 1) % MVNUM == Q.front)			//队满
		return 0;
	Q.base[Q.rear] = i;					//先赋值再加
	Q.rear = (Q.rear + 1) % MVNUM;
	return 1;
}
 
int deQueue(sqQueue& Q, int &u)
{
	if (Q.rear == Q.front)					//队空
		return 0;
	u = Q.base[Q.front];					//要删的值给u然后再加
	Q.front = (Q.front + 1) % MVNUM;
	return 1;
}
 
int firstVEX(AMGraph G, int u)
{//在邻接矩阵u行0列开始遍历,如果找到不等于无穷的,
//并且没有访问过的就返回列的下标,否则就返回无穷
	for (int i = 0; i < G.vexnum; i++)		
	{
		if (G.arr[u][i] != MAXINT && visit[i] == 0) 
		{
			return i;
		}
	}
	return MAXINT;
}
 
int nextVEX(AMGraph G, int w, int u)
{//在邻接矩阵u行w+1列开始遍历,如果找到不等于无穷的,
//并且没有访问过的就返回列的下标,否则就返回无穷
	for (int i = w + 1; i < G.vexnum; i++)
	{
		if (G.arr[u][i] != MAXINT && visit[i] == 0)
		{
			return i;
		}	
	}
	return MAXINT;
}
 
void bfsAM(AMGraph G, int i)
{//随机选一个顶点下标,这里为0
	cout << G.vexs[i] << " ";		//输出i下标在顶点表的值	
	visit[i] = 1;				//辅助数值对应下标i的值置为1
	sqQueue Q;					
	initQueue(Q);
	enQueue(Q, i);				//i为矩阵中顶点的行下标,让它入队(顶点表的下标和矩阵的列下标,行下标一致,本算法中说谁的下标都一样)
	while (Q.rear != Q.front)		//队不为空
	{
		int u = 0;			//接收矩阵中顶点的行下标,因为是邻接矩阵
		deQueue(Q,u);			//出队并让u接收矩阵中顶点的行下标
		for (int w = firstVEX(G, u); w != MAXINT; w = nextVEX(G, w, u))
		{//注意在一次循环中u不变
			if (!visit[w])
			{
				cout << G.vexs[w] << " ";
				visit[w] = 1;
				enQueue(Q, w);
			}
		}
	}
	
}

思路最重要吧

与DFS一致

四、最小生成树

图的生成树:图中所有顶点均由边链接在一起,但不存在回路的图。

一个图可以有许多棵生成树

无向图的生成树

最小生成树(Minimum Spanning Tree):给定一个无向网,在该网所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树。

(一)普利姆算法

不想写了

 代码如下

# include <iostream>
# define SIZE 20
# define NEWSIZE 20
# define maxn 100000000   //表示无限大
using namespace std;
typedef struct ArcNode {  //边的结点结构类型
	int adjvex;           //该边的终点编号
	int weight;           //该边的权值
	struct ArcNode* nextarc;  //指向下一条边的指针
}ArcNode;
typedef struct VexNode {  //顶点结构
	char data;
	ArcNode* firstarc;    //指向第一条与该顶点有关的边的指针
}VexNode;
typedef struct Graph {    //邻接表结构类型
	VexNode* VNode;       //定义邻接表
	int vexnum, arcnum;   //顶点数和边的个数
	int size;             //邻接表的大小
}Graph;
 
int* Adjvex;  //保存在最小生成树中的顶点
int* Lowcost; //保存不在最小生成树中的各点到最小生成树中的点的边的最小权值 
int* Flag;    //标注该点是否已加入最小生成树
 
void Create_Graph(Graph& G)   //创建图(邻接表)
{
	cout << "顶点的个数:";
	cin >> G.vexnum;
	G.VNode = (VexNode*)malloc(SIZE * sizeof(VexNode));
	G.size = SIZE;
	while (G.size <= G.vexnum) {   //根据点的个数动态分配空间
		G.VNode = (VexNode*)realloc(G.VNode, (G.size + NEWSIZE) * sizeof(VexNode));
		G.size += NEWSIZE;
	}
	Adjvex = (int*)malloc((G.size + 10) * sizeof(int));
	Lowcost = (int*)malloc((G.size + 10) * sizeof(int));
	Flag = (int*)malloc((G.size + 10) * sizeof(int));
	cout << "请输入各顶点的名称:";
	for (int i = 1; i <= G.vexnum; i++) {
		cin >> G.VNode[i].data;
		G.VNode[i].firstarc = NULL;  //邻接表初始化,所有单向链表均为空表
	}
	cout << "请输入边的个数:";
	cin >> G.arcnum;
	cout << "请输入各边起点和终点的编号及权重:" << endl;
	int x, y, w;    //x:起始点,y:终点,w:权重
	ArcNode* p, * q;
	for (int i = 1; i <= G.arcnum; i++) {
		cin >> x >> y >> w;
		p = (ArcNode*)malloc(sizeof(ArcNode)); //创建一个用于存放当前边的结点p
		p->nextarc = NULL;
		p->adjvex = y;
		p->weight = w;
		q = G.VNode[x].firstarc;
		//将边按顺序插入到链表末尾
		if (q == NULL) {
			G.VNode[x].firstarc = p;
		}
		else {
			while (q->nextarc != NULL) {
				q = q->nextarc;
			}
			q->nextarc = p;
		}
		p = (ArcNode*)malloc(sizeof(ArcNode));
		p->nextarc = NULL;
		p->adjvex = x;
		p->weight = w;
		q = G.VNode[y].firstarc;
		if (q == NULL) {
			G.VNode[y].firstarc = p;
		}
		else {
			while (q->nextarc != NULL) {
				q = q->nextarc;
			}
			q->nextarc = p;
		}
	}
}
 
void MinSpanningTree_Prim(Graph& G, int v) //从顶点v出发求图的最小生成树
{
	for (int i = 1; i <= G.vexnum; i++) {  //初始化
		Adjvex[i] = v;
		Lowcost[i] = maxn;
		Flag[i] = 0;
	}
	Lowcost[v] = 0;          //初始点对应的权置为0
	Flag[v] = 1;             //标记初始点(即将初始点加入树中)
	cout << G.VNode[v].data; //输出初始点的值
	int num = 1;             //记录目前最小生成树中的点的数目
	ArcNode* p;
	while (num < G.vexnum) {
		p = G.VNode[v].firstarc; //p为新加入树的点的第一个邻接点
		while (p != NULL) {      //由于新点加入,更新各不在最小生成树中的点到最小生成树中点的最小距离
			if (!Flag[p->adjvex] && Lowcost[p->adjvex] > p->weight) {
				Lowcost[p->adjvex] = p->weight;
				Adjvex[p->adjvex] = v;
			}
			p = p->nextarc;
		}
		int min = maxn;
		for (int i = 1; i <= G.vexnum; i++) {  //寻找目前到最小生成树距离最小的点(该点不在最小生成树中)
			if (!Flag[i] && Lowcost[i] < min) {
				min = Lowcost[i];
				v = i;               //更新v为目前到最小生成树距离最小的点(该点不在最小生成树中)
			}
		}
		Flag[v] = 1;             //标记该点(即将该点加入最小生成树中)
		cout << G.VNode[v].data; //输出新加入树的点的值
		num++;                   //最小生成树中的点的数目+1
	}
}
 
int main()
{
	Graph G;
	Create_Graph(G);
	int sum = 0;
	cout << "最小生成树为:";
	MinSpanningTree_Prim(G, 1);  //从1号点出发
	cout << endl;
	for (int i = 1; i <= G.vexnum; i++) {
		cout << Adjvex[i] << " ";
	}
	cout << endl;
	for (int i = 1; i <= G.vexnum; i++) {
		cout << Lowcost[i] << " ";
		sum += Lowcost[i];         //求最小生成树的值
	}
	cout << endl;
	for (int i = 1; i <= G.vexnum; i++) {
		cout << Flag[i] << " ";
	}
	cout << endl;
	cout << "最小生成树的值为:" << sum << endl;
	return 0;
}
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define INF INT_MAX
 
// 邻接矩阵表示的无向图
typedef struct {
    int V; // 顶点数
    int E; // 边数
    int G[100][100]; // 邻接矩阵
} Graph;
 
// 获取边的权重
int getWeight(Graph G, int u, int v) {
    return G[u][v];
}
 
// Kruskal算法求最小生成树
Graph primMST(Graph G) {
    int V = G.V; // 顶点数
    int E = G.E; // 边数
    int parent[V]; // 父节点数组
    int dist[V]; // 从源点到每个顶点的距离数组
    int i, u, v;
    int minCost = 0; // 总代价
    int edgeCount = 0; // 已选边的数量
    Graph mstEdges; // MST边集合
    memset(parent, -1, sizeof(parent)); // 初始化父节点为-1
    memset(dist, INF, sizeof(dist)); // 初始化距离为正无穷大
    priorityQueueNode pq; // 优先队列头结点
    pq.data = (void*)&dist[0];
    pq.index = 0;
    memset(mstEdges.G, 0, sizeof(mstEdges.G)); // 将邻接矩阵清零
    mstEdges.V = V;
    mstEdges.E = 0;
    do { // 不断扩展最小生成树,直到不存在增广路为止
        u = pq.index; // 取出距离源点最近的顶点u
        if (u == -1) break; // 如果已经没有顶点可选了,跳出循环
        pq.index = parent[u]; // 将当前顶点更新为其父节点
        edgeCount++; // 已选边的数量加1
        for (i = 0; i < G.V; i++) { // 遍历所有顶点
            v = i; // 从当前顶点开始选择下一个顶点v
            if (dist[v] > dist[u] + G.G[u][v]) { // 如果从u到v的距离比从u到源点的距离更短,则更新距离和优先级队列头结点
                dist[v] = dist[u] + G.G[u][v];
                pq.data = (void*)&dist[0];
                pq.index = i;
            } else if (i != u && v != u) { // 如果当前顶点u不是目标顶点,且从u到v的距离比从u到源点的距离更短,则将边的权重加入到最小生成树中,并更新优先级队列头结点的位置
                mstEdges.G[edgeCount] = getWeight(G, u, v);
                pq.data = (void*)&mstEdges.G[0];
                pq.index = edgeCount++;
            } else if (i == u && v != u) { // 如果当前顶点u是目标顶点,但从u到v的距离比从u到源点的距离更短,则将边的权重加入到最小生成树中,并将当前顶点更新为其父节点的值为已选边的数量减一(因为此时已经找到了一条增广路径)
                mstEdges.G[edgeCount] = getWeight(G, u, v);
                parent[v] = edgeCount--; // 将当前顶点的父节点设为已选边的数量减一(因为此时已经找到了一条增广路径)
            } else if (i != u && v == u) continue; // 如果当前顶点u是目标顶点且从u到v的距离等于从u到源点的距离,则不需要进行任何操作,直接跳过本次循环继续下一次循环迭代
        }
        minCost += mstEdges.G[mstEdges.E-1]; // 将总代价加上已选边的权重之和作为新的总代价
        mstEdges.E++; // 将已选边的计数加一,表示又选了一条边加入到最小生成树中
    } while (edgeCount < V); // 当已选边的数量小于顶点数时,继续扩展最小生成树直到不存在增广路为止

 时间复杂度O(n^{2})

(二)克鲁斯卡尔算法

 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define INF INT_MAX
 
// 邻接矩阵表示的无向图
typedef struct {
    int V; // 顶点数
    int E; // 边数
    int G[100][100]; // 邻接矩阵
} Graph;
 
// 获取边的权重
int getWeight(Graph G, int u, int v) {
    return G[u][v];
}
 
// Kruskal算法求最小生成树
Graph primMST(Graph G) {
    int V = G.V; // 顶点数
    int E = G.E; // 边数
    int parent[V]; // 父节点数组
    int dist[V]; // 从源点到每个顶点的距离数组
    int i, u, v;
    int minCost = 0; // 总代价
    int edgeCount = 0; // 已选边的数量
    Graph mstEdges; // MST边集合
    memset(parent, -1, sizeof(parent)); // 初始化父节点为-1
    memset(dist, INF, sizeof(dist)); // 初始化距离为正无穷大
    priorityQueueNode pq; // 优先队列头结点
    pq.data = (void*)&dist[0];
    pq.index = 0;
    memset(mstEdges.G, 0, sizeof(mstEdges.G)); // 将邻接矩阵清零
    mstEdges.V = V;
    mstEdges.E = 0;
    do { // 不断扩展最小生成树,直到不存在增广路为止
        u = pq.index; // 取出距离源点最近的顶点u
        if (u == -1) break; // 如果已经没有顶点可选了,跳出循环
        pq.index = parent[u]; // 将当前顶点更新为其父节点
        edgeCount++; // 已选边的数量加1
        for (i = 0; i < G.V; i++) { // 遍历所有顶点
            v = i; // 从当前顶点开始选择下一个顶点v
            if (dist[v] > dist[u] + G.G[u][v]) { // 如果从u到v的距离比从u到源点的距离更短,则更新距离和优先级队列头结点
                dist[v] = dist[u] + G.G[u][v];
                pq.data = (void*)&dist[0];
                pq.index = i;
            } else if (i != u && v != u) { // 如果当前顶点u不是目标顶点,且从u到v的距离比从u到源点的距离更短,则将边的权重加入到最小生成树中,并更新优先级队列头结点的位置
                mstEdges.G[edgeCount] = getWeight(G, u, v);
                pq.data = (void*)&mstEdges.G[0];
                pq.index = edgeCount++;
            } else if (i == u && v != u) { // 如果当前顶点u是目标顶点,但从u到v的距离比从u到源点的距离更短,则将边的权重加入到最小生成树中,并将当前顶点更新为其父节点的值为已选边的数量减一(因为此时已经找到了一条增广路径)
                mstEdges.G[edgeCount] = getWeight(G, u, v);
                parent[v] = edgeCount--; // 将当前顶点的父节点设为已选边的数量减一(因为此时已经找到了一条增广路径)
            } else if (i != u && v == u) continue; // 如果当前顶点u是目标顶点且从u到v的距离等于从u到源点的距离,则不需要进行任何操作,直接跳过本次循环继续下一次循环迭代
        }
        minCost += mstEdges.G[mstEdges.E-1]; // 将总代价加上已选边的权重之和作为新的总代价
        mstEdges.E++; // 将已选边的计数加一,表示又选了一条边加入到最小生成树中
    } while (edgeCount < V); // 当已选边的数量小于顶点数时,继续扩展最小生成树直到不存在增广路为止

这个

#include <stdio.h>
#include <stdlib.h>
#define VertexMax 20 //最大顶点数为20

typedef char VertexType; 

typedef struct
{
	VertexType begin;
	VertexType end;
	int weight;
}Edge;//边集数组edge[]的单元 

typedef struct
{
	VertexType Vertex[VertexMax];//顶点数组 
	Edge edge[VertexMax];//边集数组 
	int vexnum;//顶点数 
	int edgenum;//边数 
}EdgeGraph;

void CreateEdgeGraph(EdgeGraph *E)
{
	int i;
	
	printf("请输入顶点数和边数:\n");
	printf("顶点数 n="); 
	scanf("%d",&E->vexnum);
	printf("边  数 e="); 
	scanf("%d",&E->edgenum);
	printf("\n"); 
	//printf("\n"); 
	
	printf("输入顶点(无需空格隔开):"); 
	scanf("%s",E->Vertex);
	printf("\n\n");
	
	printf("输入边信息和权值(如:AB,15):\n");
	for(i=0;i<E->edgenum;i++)
	{
		printf("请输入第%d边的信息:",i+1);
		scanf(" %c%c,%d",&E->edge[i].begin,&E->edge[i].end,&E->edge[i].weight);
	}	
}

void print(EdgeGraph *E)
{
	int i;
	
	printf("\n-----------------------------------\n"); 
	printf(" 顶点数组Vertex:");
	for(i=0;i<E->vexnum;i++)
	{
		printf("%c ",E->Vertex[i]);
	}
	printf("\n\n");
	
	printf(" 边集数组edge:\n\n");
	printf("\t\tBegin	End	Weight\n");
	for(i=0;i<E->edgenum;i++)
	{
		printf("\tedge[%d]	%c	%c	%d\n",i,E->edge[i].begin,E->edge[i].end,E->edge[i].weight);
	}
	printf("\n-----------------------------------\n");
}

void sort(EdgeGraph *E)
{
	int i,j;
	Edge temp;
	
	for(i=0;i<E->edgenum-1;i++)
	{
		for(j=i+1;j<E->edgenum;j++)
		{
			if(E->edge[i].weight>E->edge[j].weight)
			{
				temp=E->edge[i];
				E->edge[i]=E->edge[j];
				E->edge[j]=temp;
			}
		}
	}
}

int LocateVex(EdgeGraph *E,VertexType v)//查找元素v在一维数组 Vertex[] 中的下标,并返回下标 
{
	int i;
	
	for(i=0;i<E->vexnum;i++)
	{
		if(v==E->Vertex[i])
		{
			return i; 
		} 
	 } 
	 
	 printf("No Such Vertex!\n");
	 return -1;
}

int FindRoot(int t,int parent[])//t接收到是结点在Vertex数组中的下标 
{
	while(parent[t]>-1)//parent=-1表示没有双亲,即没有根节点 
	{
		t=parent[t];//逐代查找根节点 
	}
	
	return t;//将找到的根节点返回,若没有根节点返回自身 
}

void MiniSpanTree_Kruskal(EdgeGraph *E)
{
	int i;
	int num;//生成边计数器,当num=顶点数-1 就代表最小生成树生成完毕 
	int root1,root2; 
	int LocVex1,LocVex2; 
	int parent[VertexMax];//用于查找顶点的双亲,判断两个顶点间是否有共同的双亲,来判断生成树是否会成环 
	
	//1.按权值从小到大排序 
	sort(E);
	print(E);
	//2.初始化parent数组 
	for(i=0;i<E->vexnum;i++)
	{
		parent[i]=-1;
	}
	
	printf("\n 最小生成树(Kruskal):\n\n");
	//3.
	for(num=0,i=0;i<E->edgenum;i++)
	{
		LocVex1=LocateVex(E,E->edge[i].begin);
		LocVex2=LocateVex(E,E->edge[i].end);
		root1=FindRoot(LocVex1,parent);
		root2=FindRoot(LocVex2,parent);
	
		
		if(root1!=root2)//若不会成环,则在最小生成树中构建此边 
		{
			printf("\t\t%c-%c w=%d\n",E->edge[i].begin,E->edge[i].end,E->edge[i].weight);//输出此边 
			parent[root2]=root1;//合并生成树
			num++;
			
			if(num==E->vexnum-1)//若num=顶点数-1,代表树生成完毕,提前返回 
			{
				return;
			}
		} 
	}
	
}

int main() 
{
	EdgeGraph E;
	
	CreateEdgeGraph(&E);
	MiniSpanTree_Kruskal(&E);
	
	return 0;
	
}

.

代码的初始值为-1,思路都一样

五、最短路径

(一)迪杰斯特拉算法

可以算出开始点到其他点的最短距离

顶点i第一轮第二轮第三轮第四轮第五轮第六轮
1

6

V0->V1

5

V0->V2->V1

2

3

V0->V2

3\infty

8

V0->V2->V3

6

V0->V2->V1->3

4\infty\infty

9

V0->V2->V1->V4

9

V0->V2->V1->V4

5\infty

10

V0->V2->V5

10

V0->V2->V5

10

V0->V2->V5

10

V0->V2->V5

6\infty\infty\infty\infty

11

V0->V2->V1->V4->v6

11

V0->V2->V1->V4->v6

S{0,2}{0,2,1}{0,2,1,3}{0,2,1,3,4}{0,2,1,3,4,5}{0,2,1,3,4,5,6}

代码

#include<stdio.h>
#include<stdlib.h>
//最短路径算法 迪杰斯特拉 求有向图G 从某一个顶点开始 到其余各个顶点的最短路径P以及带权长度
//P为前驱顶点的下标 D为从选取的顶点V0到V顶点的最短路径长度
 
typedef int P[200];//储存最短路径下标
typedef int D[200];//v0到v的最短路径长度和
 
//邻接矩阵
typedef struct AdjacentMatrix
{
    //顶点集
    int Vertices[200];
    //边集
    int Edge[200][200];
    //顶点数 边数
    int numV, numE;
}AdjMatrix;
 
void creategrahp(AdjMatrix* G)
{
    int n, e;//n代表顶点数 e代表边数
    int vi, vj;//vi vj代表边的两个顶点对
    int w;//表示边的权值
    printf("要输入的顶点数和边数\n");
    scanf("%d%d",&n,&e);
    G->numV = n; 
    G->numE = e;
    //图的初始化
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            if(i == j)
            {
                //一个非带权的图 0代表没有边 1代表有边
                //边不指向自己 即对角线为0
                G->Edge[i][j] = 0;
            }
            else
            {
                //如果是带权的图 初始化为0或者为一个不可能的值
                G->Edge[i][j] = 65535;
            }
        }
    }
    //将顶点存入数组
    for(int i = 0; i < G->numV; i++)
    {
        printf("请输入第%d个节点的信息\n",i + 1);
        scanf("%d", &G->Vertices[i]);
    }
    //输入边的信息
    for(int i = 0; i< G->numE; i++)
    {
        //如果输入的是顶点的值 需要从顶点集中查找对应的下标 
        //如果是带权图 还要输入权的信息
        printf("请输入边的信息Vi,Vj和权值w\n");
        scanf("%d%d%d",&vi,&vj,&w);
        G->Edge[vi][vj] = w;
        //如果是带权图 等于权值
        //如果是有向图 就不对称
        //如果是无向图 矩阵对称
        G->Edge[vj][vi] = w;
    }
}
 
 
void ShortPath_Dijkstra(AdjMatrix* G, int v0, P* p, D* d)
{
    int k;//记录当前最短路径的下标
    int final[200];//final[x] = 1 表示已求得的到v0的最短路径
    //初始化DPFinal
    for(int i = 0; i < G->numV; i++)
    {
        final[i] = 0;//初始化为未知状态
        (*d)[i] = G->Edge[v0][i];//如果v0传进的是值 寻找下标
        (*p)[i] = -1;
    }
    final[v0] = 1;
    (*d)[v0] = 0;//自己到自己的路径为0
 
    //主循环 求每次v0到v的最短路径
    for(int i = 1; i < G->numV; i++)
    {
        int min = 65535;
        //寻找与v0距离最近的顶点
        for(int j = 0; j < G->numV; j++)
        {
            if(final[j] != 1 && (*d)[j] < min)
            {
                min = (*d)[j];
                k = j;
            }
        }
        //把Vk加入到最短路径中
        final[k] = 1;
        //修正当前最短路径的距离
        //以Vk作为中转,更新以Vk为中心的邻接点到V0的距离
        for(int j = 0; j < G->numV; j++)
        {
            //如果当前找到v的顶点的路径小于原来的路径长度
            if(min + G->Edge[k][j] < (*d)[j] && final[j] != 1)
            {
                //说明找到了更短的路径 修改DP
                (*d)[j] = min + G->Edge[k][j];
                (*p)[j] = k;
            }
        }
    }
}
 
int main()
{
    AdjMatrix* G = (AdjMatrix*)malloc(sizeof(AdjMatrix));
    int p[200];
    int d[200];
    creategrahp(G);
    int v0 = 0;
    ShortPath_Dijkstra(G, v0, &p, &d);
    for (int i = 1; i < G->numV; i++)
    {
        printf("v%d - v%d:", v0, i);
        int j = i;
        while (p[j] != -1)
        {
            printf("%d", p[j]);
            j = p[j];
        }   
        printf("\n");
    }
 
    printf("最短路径长度");
    for (int i = 1; i < G->numV; i++)
    {
        printf("v%d-v%d : %d\n", G->Vertices[0], G->Vertices[i], d[i]);
    }
    system("pause");
    return 0;
}

这里的final[]相当于S集合

时间复杂度O(n^{3}

(二)弗洛伊德算法

这个算的是所有点之间的最短长度

代码

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdbool.h>
#include <malloc.h>
#define MVNum 100//最大顶点数
#define MaxInt 66666//表示极大值
typedef struct {
	char vexs[MVNum];//顶点表(顶点为字符型)
	int arcs[MVNum][MVNum];//邻接矩阵(权值为整型)
	int vexnum, arcnum;//图的当前点数和边数
}AMGraph;
 
//定位
int LocateVex(AMGraph* G, char v) {
	int i;
	for (i = 0; i < G->vexnum; i++) {
		if (G->vexs[i] == v) {
			return i;
		}
	}
	return -1;
}
 
//无向网的建立
AMGraph* CreateUDN() {
	int i, j, k, w;
	char v1, v2;
	AMGraph* G = malloc(sizeof(AMGraph));
	printf("输入总顶点数,边数\n");
	scanf("%d%d", &G->vexnum, &G->arcnum);
	getchar();//吸收换行符
	printf("依次输入点的信息\n");
	for (i = 0; i < G->vexnum; i++) {
		scanf("%c", &G->vexs[i]);
	}
	getchar();//吸收换行符
	for (i = 0; i < G->vexnum; i++)
		for (j = 0; j < G->vexnum; j++) {
			if (i == j) {
				G->arcs[i][j] = 0;
			}
			else {
				G->arcs[i][j] = MaxInt;
			}
		}
	for (k = 0; k < G->arcnum; k++) {
		printf("输入一条边依附的顶点及权值\n");
		scanf("%c%c", &v1, &v2);
		scanf("%d", &w);
		getchar();//吸收换行符
		i = LocateVex(G, v1), j = LocateVex(G, v2);//确定v1、v2在顶点数组的下标
		G->arcs[i][j] = w;//边<v1,v2>权值置为w
		G->arcs[j][i] = w;//无向网对称边<v2,v2>权值也置为w
	}
	return G;
}
 
//输出邻接矩阵
void print(AMGraph* G) {
	int i, j;
	printf("  ");
	for (i = 0; i < G->vexnum; i++) {
		printf("%c  ", G->vexs[i]);
	}
	printf("\n");
	for (i = 0; i < G->vexnum; i++) {
		printf("%c ", G->vexs[i]);
		for (j = 0; j < G->vexnum; j++) {
			if (G->arcs[i][j] == MaxInt)
				printf("∞  ");
			else
				printf("%d  ", G->arcs[i][j]);
		}
		printf("\n");
	}
}
 
//弗洛伊德算法
void Floyd(AMGraph* G) {
	int distance[MVNum][MVNum];
	int i, j, k;
	//初始化距离矩阵
	for (i = 0; i < G->vexnum; i++) {
		for (j = 0; j < G->vexnum; j++) {
			distance[i][j] = G->arcs[i][j];
		}
	}
	//中间节点迭代
	for (k = 0; k < G->vexnum; k++) {
		for (i = 0; i < G->vexnum; i++) {
			for (j = 0; j < G->vexnum; j++) {
 
				if (distance[i][k] + distance[k][j] < distance[i][j]) {
					distance[i][j] = distance[i][k] + distance[k][j];
				}
			}
		}
	}
	//输出
	printf("每对顶点间的最短距离:\n");
	for (i = 0; i < G->vexnum; i++) {
		for (j = 0; j < G->vexnum; j++) {
			if (distance[i][j] == MaxInt) {
				printf("∞  ");
			}
			else {
				printf("%d  ", distance[i][j]);
			}
		}
		printf("\n");
	}
}
 
int main() {
	AMGraph* G = CreateUDN();
	printf("该无向网邻接矩阵为:\n");
	print(G);
	Floyd(G);
}

关键判断distance[i][k] + distance[k][j] < distance[i][j]关系

加上path

void ShortestPath_Floyd(Graph G, int** Path, int** D)  //Path,D 为二维数组 
{
	int v, w, k;
	for (v = 0; v < G.numv; v++)
	{
		for (w = 0; w < G.numv; w++)
		{
			D[v][w] = G.edge[v][w];  //初始化 D 数组为邻接矩阵
			//若 v 与 w 之间有弧,将 w 的前驱置为 v
			//若 v 与 w 之间没有弧,将 w 的前驱置为 -1
			if (D[v][w] < INFINITY)
			{
				path[v][w] = v;
			}
			else
			{
				path[v][w] = -1;
			}
		}
	}
	for (k = 0; k < G.numv;k++)
	{
		for (v = 0; v < G.numv; v++)
		{
			for(w = 0;w < G.numv; w++)
			{
				if (D[v][w] > D[v][k] + D[k][w]) //如果从 v 经 k 到 w 的路径最短
				{
					D[v][w] = D[v][k] + D[k][w];
					Path[v][w] = Path[v][k];
				}
			}
		}
	}

	//显示最短路径
	printf("各顶点间最短路径如下:\n");    
	for(v = 0; v < G.numv; v++)   
	{        
		for(w = v+1; w < G.numv; w++)  
		{
			printf("v%d-v%d weight: %d ",v,w,D[v][w]);
			k = Path[v][w];		//获得第一个路径的顶点下标
			printf(" path: %d",v);	//打印源点 
			while(k != w)		//如果路径顶点下标不是终点 
			{
				printf(" -> %d",k);	//打印路径顶点 
				k = Path[k][w];	//获得下一个路径顶点下标
			}
			printf(" -> %d\n",w);	//打印终点
		}
		printf("\n");
	}
}

六、拓扑排序(这个不考)

有向无环图

 (一)AOV网

  1. 若是从i到j有一条有向路径,则i是j的前驱,j是i的后继
  2. 若<i,j> 是网中有向边,则i是j的直接前驱,j是i的直接后继
  3. AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然不可能

(二)拓扑排序

1、在有向图中选一个没有前驱的顶点且输出
2、从图中删除该顶点和所有以他为尾的弧
3、重复上述步骤,直到所有的顶点均已输出,或者当图中不存在无前驱的顶点为止

比如我们要对上述图进行拓扑排序 第一次寻找的是A这一个结点 因为A没有前驱 接下来就是删除A以及以A为弧尾的弧,此时就是如下图

在从中选择一个没有前驱的结点,这里BCD 都可以,这里我们就选择B 同样的道理 删除B以及以B为弧尾的弧此时就是如下图

同样的道理 再选一个C删除以C为弧尾的弧

再删除 D

再删除E
所有总的拓扑排序是 当然排序可能不止一种 因为可能不止一个结点在某一步骤中是无前驱的
ABCDE 有人可能会说了这个图太简单了,能不能难一点的图来拓扑
如下

拓扑排序可以如下
AEBDC
EABDC
AEDBC
EADBC

(三)判断一个有向图是否为有向无环图的方法是?

方法一∶深度优先遍历

若从有向图上的某个顶点u出发,在DFS(u)结束之前出现一条从顶点v到u的边,由于v在生成树上是u的子孙,则图中必定存在包含u和v的环,因此深度优先遍历可以检测一个有向图是否有环。

方法二∶拓扑排序

数组

//有向无环图的拓扑排序
#include<stdio.h>
#include<stdlib.h>
typedef struct graph
{
	char* vexs;//顶点数值
	int** arcs;//邻接矩阵
	int vexNum;//顶点数
	int arcNum;//边数
}Graph;
 
typedef struct Node//栈的建立与存顶点下标有关
{
	int data;
	struct Node* next;
}Node;
 
Node* initStack()
{
	Node* stack=(Node*)malloc(sizeof(Node));
	stack->data=0;
	stack->next=NULL;
	return stack;
}
 
void push(Node* stack,int data)//压栈
{
	Node* node=(Node*)malloc(sizeof(Node));
	node->data=data;
	node->next=stack->next;
	stack->next=node;
	stack->data++;
}
 
int isEmpty(Node* stack)//判断是否为空栈
{
	if(stack->next==NULL)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}
 
int pop(Node* stack)//出栈
{
	if(isEmpty(stack))
	{
		return -1;
	}
	else
	{
		Node* node=stack->next;		
		int data=node->data;
		stack->next=node->next;
		free(node);
		stack->data--;
		return data;
	}
}
 
Graph* initGraph(int vexNum)//分配空间
{
	Graph* G = (Graph*)malloc(sizeof(Graph));
	G -> vexs = (char*)malloc(sizeof(char) * vexNum);
	G -> arcs = (int**)malloc(sizeof(int*) * vexNum);
	for (int i = 0 ; i < vexNum; i++) 
	{
		G -> arcs[i] = (int*)malloc(sizeof(int) * vexNum);
	}
	G -> vexNum = vexNum;
	G -> arcNum = 0;
	return G;
}
 
void createGraph(Graph* G, char* vexs, int* arcs)//创建图
{
	for (int i = 0 ; i < G -> vexNum; i++) 
	{
		G -> vexs[i] = vexs[i];
		for (int j = 0; j < G -> vexNum; j++) 
		{
			G -> arcs[i][j] = *(arcs + i * G -> vexNum + j);
			if (G -> arcs[i][j] != 0)
				G -> arcNum ++;
		}
	}
	G -> arcNum /= 2;
}
 
int* findInDegrees(Graph* G)//找出入度
{
	int* inDegrees=(int*)malloc(sizeof(int)*G->vexNum);
	for(int i=0;i<G->vexNum;i++)//初始化
	{
		inDegrees[i]=0;
	}
	for(int i=0;i<G->vexNum;i++)
	{
		for(int j=0;j<G->vexNum;j++)
		{
			if(G->arcs[i][j])
			{
				inDegrees[j]++;
			}
		}
	}
	return inDegrees;
}
 
void topologicalSort(Graph* G)//拓扑排序
{
	int index=0;
	int* top=(int*)malloc(sizeof(int)*G->vexNum);//存下标的数组
	int* inDegrees=findInDegrees(G);
	Node* stack=initStack();
	for(int i=0;i<G->vexNum;i++)//入度为0的压栈
	{
		if(inDegrees[i]==0)
		{
			push(stack,i);
		}
	}
	while(!isEmpty(stack))//栈不为空,循环执行入度的减法(去掉输出顶点指向的下一个顶点的边)
	{
		int vexindex=pop(stack);//出栈的是顶点的下标
		top[index++]=vexindex;//保存顶点下标
		for(int j=0;j<G->vexNum;j++)
		{
			if(G->arcs[vexindex][j])//下一个顶点有入度时减去
			{
				inDegrees[j]--;
				if(inDegrees[j]==0)//顶点入度减到0了直接入栈
				{
					push(stack,j);
				}
			}
		}
	}
	for(int i=0;i<G->vexNum;i++)//依次输出入度为零的顶点
	{
		printf("%c ",G->vexs[top[i]]);
	}
	printf("\n");
}
 
void DFS(Graph* G,int* flag,int index)//深度优先遍历
{
	printf("%c ",G->vexs[index]);
	flag[index]=1;//已经访问过顶点标记为1,之后不会再访问
	for(int i=0;i<G->vexNum;i++)
	{
		if(G->arcs[index][i]==1&&!flag[i])
		{
			DFS(G,flag,i);
		}
	}
}
 
int main()
{
	Graph* G=initGraph(6);
	int* flag=(int*)malloc(sizeof(int)*G->vexNum);
	for(int i=0;i<G->vexNum;i++)//首先赋值为0,表示未访问任何顶点
	{
		flag[i]=0;
	}
	int arcs[6][6]={
		0,1,1,1,0,0,
		0,0,0,0,0,0,
		0,1,0,0,1,0,
		0,0,0,0,1,0,
		0,0,0,0,0,0,
		0,0,0,1,1,0
	};
	createGraph(G,"123456",(int*)arcs);
	DFS(G,flag,0);
	printf("\n");
	topologicalSort(G);
	return 0;
}

邻接表

#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 15
#define MAXINT 10000
int get_index(int* arr, int e)//得到输出的顶点,在数组中的下标
{
    int i = 0;
    for(i = 0; i < MAXSIZE; i++)
    {
        if(arr[i] == e)return i;
    }
    return -1;
}
 
void TopologicalSorting(int arcs[][MAXSIZE])
{
    int m,n,i,j,a,b;
    int vertex[MAXSIZE] = {0};//初始化顶点数组
    int visited[MAXSIZE] = {0};
    printf("请输入顶点数和弧的数目:>");
    scanf("%d%d",&m,&n);
    for(i = 0; i < m; i++)
    {
        printf("请输入每一个顶点:>");
        getchar();
        scanf("%d", &vertex[i]);
    }
    for(j = 0; j < n; j++)
    {
        printf("请输入每一条边:>");
        getchar();
        scanf("%d%d",&a,&b);
        arcs[get_index(vertex,a)][get_index(vertex,b)] = 1;//根据输入更新邻接矩阵
    }
    int num = 0;
    while(num != m)//找到全部顶点就结束
    {
        for(i = 0; i < m; i++)//循环遍历邻接矩阵
        {
            for(j = 0; j < m; j++)
            {
                //如果找到某一列的数据全部为0,说明这一列所代表的顶点是没有前驱的
                //这里visited[j],是用来限制某一行,visited[i]为限制某一列
                if((arcs[j][i] == 1 && !visited[j]) || visited[i])break;
            }
            if(j == m)
            {
                printf("%d",vertex[i]);//输出没有前驱的顶点
                num++;//找到的顶点数加1
                visited[i] = 1;//设置这个顶点为已访问
            }
        }
    }
 
}
 
 
int main()
{
    int arcs[MAXSIZE][MAXSIZE]={{0}};//邻接矩阵
    TopologicalSorting(arcs);
    return 0;
}

栈的实现

#include <stdio.h>
#include <stdlib.h>

#define MAX_VER_NUM 2000
#define VertexData int
#define ERROR 0
#define OK 1

typedef enum { DG, UDG, DN, UDN }GraphKind;

typedef struct ArcNode{
	VertexData adjvex;
	struct ArcNode* nextarc;
}ArcNode;

typedef struct VexNode {
	VertexData data;
	ArcNode* firstArc;
}VertexNode;

typedef struct {
	VertexNode vertex[MAX_VER_NUM];
	int num_vex, num_arc;
	GraphKind kind;
}AdjList;

typedef struct {
	int data[MAX_VER_NUM];
	int top;
}Stack;

Stack InitStack();     //初始化栈
void PushStack(Stack* S, int v);   //入栈
void PopStack(Stack* S, int* v);   //出栈
int LocateVertex(AdjList A, VertexData v);//找到顶点位置
AdjList* CreateGraph(int n, int m);       //创建图
void FindID(AdjList A, int indegree[MAX_VER_NUM]);//求入度
int TopoSort(AdjList A);   //拓扑排序


int main()
{
	int n, m;
	scanf("%d%d", &n, &m);
	AdjList *A;
	A = CreateGraph(n, m);
	TopoSort(*A);
	return 0;
}

Stack InitStack()
{   //初始化栈
	Stack S;
	S.top = -1;
	return S;
}

void PushStack(Stack *S, int v)
{   //入栈
	if (S->top == MAX_VER_NUM - 1) {
		return;
	}
	S->top++;
	S->data[S->top] = v;
}

void PopStack(Stack* S, int *v)
{   //出栈
	if (S->top == -1) {
		return;
	}
	*v = S->data[S->top];
	S->top--;
}

int LocateVertex(AdjList A, VertexData v)
{   //找到顶点位置
	for (int i = 0; i < A.num_vex; i++) {
		if (A.vertex[i].data == v) {
			return i;
		}
	}
	return -1;
}

AdjList* CreateGraph(int n, int m)
{   //创建图
	AdjList *A;
	A = (AdjList*)malloc(sizeof(AdjList));
	if (A == NULL) {
		return NULL;
	}
	A->kind = DG;
	A->num_vex = n;
	A->num_arc = m;
	for (int i = 0; i < A->num_vex; i++) {
		scanf("%d", &A->vertex[i].data);//若要输入字符则要改变
		A->vertex[i].firstArc = NULL;
	}
	VertexData v1, v2;
	int loc1;
	for (int i = 0; i < A->num_arc; i++) {
		scanf("%d%d", &v1, &v2);//若要输入字符则要改变
		ArcNode* tmp;
		tmp = (ArcNode*)malloc(sizeof(ArcNode));
		if (tmp == NULL) {
			return 0;
		}
		tmp->adjvex = v2;
		loc1 = LocateVertex(*A, v1);
		tmp->nextarc = A->vertex[loc1].firstArc;
		A->vertex[loc1].firstArc = tmp;
	}
	return A;
}

void FindID(AdjList A, int indegree[MAX_VER_NUM])
{   //求入度
	ArcNode* tmp;
	tmp = (ArcNode*)malloc(sizeof(ArcNode));
	if (tmp == NULL) {
		return;
	}
	for (int i = 0; i < A.num_vex; i++) {
		indegree[i] = 0;
	}
	int loc;
	for (int i = 0; i < A.num_vex; i++) {
		tmp = A.vertex[i].firstArc;
		while (tmp != NULL) {
			loc = LocateVertex(A, tmp->adjvex);
			indegree[loc]++;
			tmp = tmp->nextarc;
		}
	}
}

int TopoSort(AdjList A)
{   //拓扑排序
	Stack S;
	int indegree[MAX_VER_NUM];
	int count = 0;//记录遍历顶点数,若小于A.num_vex,则A中有环
	ArcNode* tmp;
	tmp = (ArcNode*)malloc(sizeof(ArcNode));
	if (tmp == NULL) {
		return 0;
	}
	FindID(A, indegree);
	S = InitStack();
	for (int i = 0; i < A.num_vex; i++) {
		if (indegree[i] == 0) {
			PushStack(&S, i);
		}
	}
	int v, k, loc_k;
	while (S.top != -1) {
		PopStack(&S, &v);
		printf("%d ", A.vertex[v].data);//若要输入字符则要改变
		count++;
		tmp = A.vertex[v].firstArc;
		while (tmp != NULL) {
			k = tmp->adjvex;
			loc_k = LocateVertex(A, k);
			indegree[loc_k]--;
			if (indegree[loc_k] == 0) {
				PushStack(&S, loc_k);
			}
			tmp = tmp->nextarc;
		}
	}
	if (count < A.num_vex) {
		return ERROR;
	}
	else return OK;
}

队列

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_VERTEX_NUM 20  // 最大顶点数

typedef struct {
	char vertex[MAX_VERTEX_NUM];  // 存放顶点信息
	int edge[MAX_VERTEX_NUM][MAX_VERTEX_NUM];  // 存放边信息
	int vertex_num;  // 顶点数
	int edge_num;  // 边数
} Graph;

int indegree[MAX_VERTEX_NUM];           //存放每个顶点的入度
/*这里我们不要判断某个顶点的出度,在拓扑排序中我们用不到这些*/

//初始化顶点的入度数组
void Initindegree(Graph G){
	//首先初始化入度数组
	for(int i=0;i<G.vertex_num;i++){
		indegree[i]=0;
	}
	//统计每个元素对应邻接矩阵列中1的个数即可判断有几条边进入
	for(int i=0;i<G.vertex_num;i++){
		int count=0;
		for(int j=0;j<G.vertex_num;j++){
			if(G.edge[j][i]==1)
				count++;
		}
		indegree[i]=count;
	}
}

// 拓扑排序函数
bool Topologicalsort(Graph G) {
	char queue[MAX_VERTEX_NUM];
	int front = -1, rear = -1;    // 初始化队列,front和rear都设为0
	
	// 首先把入度为0的顶点都进队列
	for (int i = 0; i < G.vertex_num; i++) {
		if (indegree[i] == 0) {
			queue[++rear] = i; // 注意这里应该存储顶点的索引,而不是顶点的值
		}
	}
	
	int count = 0; // 这个变量用来记录当前已输出的顶点数
	while (front != rear) {
		int cur_vertex = queue[++front]; // 出队一个顶点,front原先是零,现在是1
		printf("%c ", G.vertex[cur_vertex]); // 输出顶点的值
		count++; // 已输出的顶点数加1
		
		// 遍历以当前顶点为起点的所有边
		for (int i = 0; i < G.vertex_num; i++) {
			if (G.edge[cur_vertex][i] == 1) {
				indegree[i]--; // 将这些边的终点的入度减1
				if (indegree[i] == 0) { // 如果某个终点的入度变为0,就将其入队
					queue[++rear] = i;
				}
			}
		}
	}
	
	if (count == G.vertex_num) { // 如果已输出的顶点数等于总顶点数,说明拓扑排序成功
		return true;
	} else { // 否则说明存在环,拓扑排序失败
		return false;
	}
}

int main(){
	Graph G;
	G.vertex_num=5;
	G.edge_num=6;
	//人为构造图的顶点
	for(int i=0;i<G.vertex_num;i++){
		G.vertex[i]= 'A'+i;
	}
	for(int i=0; i<G.vertex_num; i++){
		for(int j=0; j<G.vertex_num; j++){
			G.edge[i][j]=0;
		}
	}
	//人为构造图的边
	G.edge[0][1]=1;
	G.edge[0][3]=1;
	G.edge[1][3]=1;
	G.edge[1][2]=1;
	G.edge[3][4]=1;
	G.edge[2][4]=1;
	G.edge[3][2]=1;
	Initindegree(G);
	Topologicalsort(G);
}

七、关键路径

1、AOE网

2、AOE网的性质

(1)只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;

(2)只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生,另外有些活动是可以并行进行的

3.关键路径

        从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动。

        完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,则整个工程的完成时间就会延长。

4.最早最晚时间


(1)事件vk的最早发生时间ve(k):决定了所有从vk开始的活动能够开工的最早时间;

(2)活动ai的最早开始时间e(i):指该活动弧的起点所表示的事件的最早发生时间;

(3)事件vk的最迟发生时间vl(k):指在不推迟整个工程完成的前提下,该事件最迟必须发生的时间;

(4)活动ai的最迟开始时间l(i):指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差;

(5)活动ai的时间余量:d(i)=l(i)-e(i),表示在不增加完成整个工程所需总时间的情况下,活动ai可以拖延的时间;

(6)若一个活动的时间余量为0,则说明该活动必须要如期完成,d(i)=0即l(i)=e(i)的活动ai是关键活动,由关键活动组成的路径就是关键路径。

5、求关键路径

1.步骤

  • (1)求所有事件的最早发生时间ve();
  • (2)求所有事件的最迟发生时间vl();
  • (3)求所有活动的最早发生事件e();
  • (4)求所有活动的最迟发生时间l();
  • (5)求所有活动的时间余量d();
  • (6)d(i)=0的就是关键活动,所有关键活动组成的路径即为关键路径。

2.举例

1.定义一维数组ve[i]并置初值0(记录事件的最早发生时间)、一维数组vI[i](记录事件的最晚发生时间)、一维数组topo[i](记录拓扑序列的顶点序号)。

2.调用拓扑排序算法,使拓扑序列保存在topo中。

3.根据topo中的值,按从前向后的拓扑次序,依次求每个事件的最早发生时间,循环n(总顶点数)次,执行以下操作:

取得拓扑序列中的顶点序号k,k= topo[i];
用指针p依次指向k的每个邻接顶点,取得每个邻接顶点的序号j,j=p->adjvex,依次更新顶点j的最早发生时间ve[j](if(ve[i]<ve[k]+ p->weight)   ve[i]= ve[k] + p->weight);
4.将每个事件的最迟发生时间vl[i]初始化为汇点的最早发生时间,即vl[i]=ve[n-1]。
5.根据topo中的值,按从后向前的逆拓扑次序,依次求每个事件的最迟发生时间,循环n次,执行以下操作:

取得拓扑序列中的顶点序号k,k= topo[i];
用指针p依次指向k的每个邻接顶点,取得每个邻接顶点的序号j,j=p->adjvex,依次根据 k的邻接点,更新k的最迟发生时间vI[k](if(vl[k]>vl[j]-p->weight)   vl[k]=vl[j]-p->weight);
6.判断某一活动是否为关键活动,循环n次,执行以下操作:

对于每个顶点vi,用指针p依次指向vi的每个邻接顶点,取得每个邻接顶点的序号j,j=p->adjvex,分别计算活动<vi,vj>的最早和最晚开始时间e和l;
如果e和l相等,则活动<vi,vj>为关键活动,输出弧<vi,vj>。

代码

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
 
#define MVNUM  100
 
typedef struct ArcNode {
	int adjvex;
	int weight;
	struct ArcNode* next;
}ArcNode;
 
typedef struct VNode {
	char data;
	ArcNode* firstarc;
}VNode, AdjList[MVNUM];
 
typedef struct {
	AdjList vertices;
	int vexnum, arcnum;
}ALGraph;
//定位
int LocateVex(ALGraph* G, char v) {
	int i;
	for (i = 0; i < G->vexnum; i++) {
		if (G->vertices[i].data == v) {
			return i;
		}
	}
	return -1;
}
 
//有向网邻接表的建立
ALGraph* CreateGraph() {
	int i, j, k, v1, v2, w;
	ALGraph* G = malloc(sizeof(ALGraph));
	printf("输入顶点数和边数:\n");
	scanf("%d%d", &G->vexnum, &G->arcnum);
	getchar();
	printf("输入顶点信息:\n");
	for (i = 0; i < G->vexnum; i++) {
		scanf("%c", &G->vertices[i].data);
		G->vertices[i].firstarc = NULL;
	}
	getchar();
	for (k = 0; k < G->arcnum; k++) {
		printf("输入一条弧依附的起点和终点和权重:\n");
		scanf("%c%c%d", &v1, &v2, &w);
		getchar();
		i = LocateVex(G, v1), j = LocateVex(G, v2);
		ArcNode* p = malloc(sizeof(ArcNode));
		p->adjvex = j;
		p->weight = w;
		p->next = G->vertices[i].firstarc;
		G->vertices[i].firstarc = p;
	}
	return G;
}
 
//---------------------------------------------------------------------------
//拓扑排序
bool TopoSort(ALGraph* G, int* topo) {
	int indegree[MVNUM] = { 0 };//记录顶点入度
	int i, j, v;
	int count = 0;//记录输出的顶点数
	int stack[MVNUM];//定义一个简单的栈
	int top = 0;//栈顶指针
	ArcNode* p = NULL;
 
	//统计各顶点入度
	for (i = 0; i < G->vexnum; i++) {
		p = G->vertices[i].firstarc;
		while (p != NULL) {
			indegree[p->adjvex]++;
			p = p->next;
		}
	}
 
	//将所有入度为0的顶点入队
	for (i = 0; i < G->vexnum; i++) {
		if (indegree[i] == 0) {
			stack[top++] = i;
		}
	}
 
	//栈非空时循环
	while (top > 0) {
		v = stack[--top];//出栈一个顶点
		topo[count++] = v;//排序的顶点数加1并将顶点存入数组topo
		//遍历该顶点为起点的所有边
		p = G->vertices[v].firstarc;
		while (p != NULL) {
			indegree[p->adjvex]--;//入度减1
			//若入度为0,入队
			if (indegree[p->adjvex] == 0) {
				stack[top++] = p->adjvex;
			}
			p = p->next;//指向下一条边
		}
	}
 
	//输出顶点数小于总顶点数,说明有环
	if (count < G->vexnum) {
		printf("该图有环,拓扑排序失败。\n");
		return false;
	}
	return true;
}
//----------------------------------------------------------------------------
//关键路径算法
bool CriticalPath(ALGraph* G) {
	int topo[MVNUM];//储存拓扑序列
	int ve[MVNUM] = { 0 };//记录每个事件最早发生时间
	int vl[MVNUM];//记录每个事件最晚发生时间
	int i, j, k, e, l;
	ArcNode* p = NULL;
	if (!TopoSort(G, topo))	return false;//若拓扑排序失败,则存在有向环,返回false
 
	//按拓扑次序求每个事件的最早发生时间
	for (i = 0; i < G->vexnum; i++) {
		k = topo[i];//取拓扑序列中的顶点序号k
		p = G->vertices[k].firstarc;//指针p指向顶点k的第一个邻接点
		//更新顶点k的所以邻接点的最早发生时间
		while (p != NULL) {
			j = p->adjvex;//j为顶点k的邻接点的序号
			//更新顶点j最早发生时间(不理解的话,找个AOE-网代入联想下)
			if (ve[j] < ve[k] + p->weight) {
				ve[j] = ve[k] + p->weight;
			}
			p = p->next;//指针噗指向下一邻接点
		}
	}
 
 
	//初始化数组Vl
	for (i = 0; i < G->vexnum; i++) {
		vl[i] = ve[G->vexnum - 1];
	}
 
	//按逆拓扑次序求每个事件的最晚发生时间
	for (i = G->vexnum - 1; i >= 0; i--) {
		k = topo[i];//取拓扑序列中的顶点序号k
		p = G->vertices[k].firstarc;//指针噗指向顶点k的第一个邻接点
		//根据顶点k的邻接点更新k的最晚发生时间
		while (p != NULL) {
			j = p->adjvex;//j为邻接点序号
			//更新顶点k的最晚发生时间
			if (vl[k] > vl[j] - p->weight) {
				vl[k] = vl[j] - p->weight;
			}
			p = p->next;//指针p指向下一邻接点
		}
	}
 
	//判断每一活动是否为关键活动
	//每次循环针对以顶点i为开始点关联的所有活动
	for (i = 0; i < G->vexnum; i++) {
		p = G->vertices[i].firstarc;//指针p指向顶点i的第一个邻接点
		while (p != NULL) {
			j = p->adjvex;//j为邻接点序号
			e = ve[i];//计算活动<i,j>的最早开始时间
			l = vl[j] - p->weight;//计算活动<i,j>的最晚开始时间
			//若为关键活动,则输出
			if (e == l) {
				printf("<%c,%c>  ", G->vertices[i].data, G->vertices[j].data);
			}
			p = p->next;//指针p指向下一邻接点
		}
	}
}
 
int main() {
	ALGraph* G = CreateGraph();
	printf("关键活动为:\n");
	CriticalPath(G);
	return 0;
}

总结 
关键路径算法是一个解决项目管理中最短时间问题的经典工具。在计算关键路径时,我们需要对整个项目进行遍历,并计算出每个活动的最早开始时间、最晚开始时间、最早完成时间以及最晚完成时间,进而得到整个项目的关键路径。因此,关键路径算法的时间复杂度相对较高,但也有一些优化方法可以应用。

算法的时间复杂度分析:对于有 n 个顶点和 e 条弧的 AOE 网而言,拓扑排序的时间复杂度是 O(n+e),计算事件的最晚发生时间的时间复杂度也是 O(n+e),最后计算关键路径的时间复杂度还是 O(n+e),所以整体的时间复杂度依然是 O(n+e)。

结束结束,长脑子了!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1852070.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实现文件分片合并功能并使用Github Actions自动编译Release

一、编译IOS镜像 1.1 编译 起因是公司电脑使用的Win11 23H2的预览版&#xff0c;这个预览版系统的生命周期只到2024-09-18&#xff0c;到期后就会强制每两小时重启。这是Windows强制升级系统的一种手段。 虽然公司里的台式电脑目前用不到&#xff0c;但是里面还保留许多旧项…

Jenkins定时构建自动化(一):Jenkins下载安装配置

目录 ​编辑 一、jdk下载安装 1. 已下载安装jdk 2. 未下载安装jdk 二、jenkins安装 1. .war包安装 三、获取IP地址 四、jenkins网页配置 一、jdk下载安装 1. 已下载安装jdk &#xff08;1&#xff09;查询jdk版本命令&#xff1a;java -version &#xff08;2&#xff09;…

[SAP ABAP] 运算符

1.算数运算符 算术运算符描述加法-减法*乘法/除法MOD取余 示例1 输出结果: 输出结果: 2.比较运算符 比较运算符描述示例 等于 A B A EQ B <> 不等于 A <> B A NE B >大于 A > B A GT B <小于 A < B A LT B >大于或等于 A > B A GE B <小…

Html去除a标签的默认样式

Html去除a标签的默认样式, a标签超链接字体默认蓝色带下划线; 去除可用: a{text-decoration:none;color:inherit;cursor:auto; }测试代码 <!DOCTYPE html> <html lang"zh-CN" dir"ltr"><head><meta charset"utf-8"/>&…

【开发12年码农教你】Android端简单易用的SPI框架-——-SPA

Service(priority 1) public class APrinterService implements IPrinterService { Override public void print() { System.out.println(“this is a printer service.”); } } 复制代码 B模块 —— BPrinterService Service(path“b_printer”, priority 2) public class…

微积分-导数1(导数与变化率)

切线 要求与曲线 C C C相切于 P ( a , f ( a ) ) P(a, f(a)) P(a,f(a))点的切线&#xff0c;我们可以在曲线上找到与之相近的一点 Q ( x , f ( x ) ) Q(x, f(x)) Q(x,f(x))&#xff0c;然后求出割线 P Q PQ PQ的斜率&#xff1a; m P Q f ( x ) − f ( a ) x − a m_{PQ} \…

java小代码(1)

代码 &#xff1a; 今日总结到此结束&#xff0c;拜拜&#xff01;

FlinkCDC sink paimon 暂不支持exactly-once写入,而通过 幂等写

幂等写入&#xff1a; 一个幂等操作无论执行多少次都会返回同样的结果。例如&#xff0c;重复的向hashmap中插入同样的key-value对就是幂等操作&#xff0c;因为头一次插入操作之后所有的插入操作都不会改变这个hashmap&#xff0c;因为hashmap已经包含这个key-value对了。另一…

算法期末整理

目录 一 算法概述 二 递归与分治策略 三 动态规划 四 贪心算法 五 回溯法 六 分支限界法 七 随机化算法 八 线性规划与网络流 一 算法概述 算法的概念 通俗地讲&#xff0c;算法是指解决问题的一种方法或一个过程。更严格地讲&#xff0c;算法是由若干条指令组成的有穷…

android 对不同日期和时间的格式方法的封装

break; default: result “星期一”; break; } return result; } /** 判断两个时间是否属于同一天 param time1 param time2 return */ public boolean isSameDay(long time1, long time2) { Calendar calen Calendar.getInstance(); calen.setTimeInMillis(t…

ROS | 常见故障排查

1.开启后发出一个WIFI WIFI名字&#xff1a;WHEELTEC接数字 安全密钥&#xff1a;dongguan 2.显示屏接口 USB接口接键鼠 3.远程登录命令 ssh -Y wheeltec192.168.0.100 是小车发出的WIFI的一个IP地址 4. 登录后确保IP地址 ip a 看一下 当前ip地址 倒数第四行-当前ip地址 1…

读AI新生:破解人机共存密码笔记09精神安全权

1. 自动化史塔西 1.1. 作恶的创新也在快速发展 1.1.1. 心怀恶意的人们正在迅速地想出滥用人工智能的新方法 1.2. 史塔西&#xff08;Stasi&#xff09;是得意智皿煮国和共家国部全安更广为人知的名字&#xff0c;它被广泛认为是“有史以来最高效、最具性压镇的报情和秘密惊诧…

[SAP ABAP] 工作区

工作区是指多个预定义类型变量组成的一个变量(可理解为一组变量的集合) 程序中使用DATA关键字定义工作区 示例1 上述代码定义了2个工作区 lw_student0 和 lw_student1 示例2 lw_student是一个工作区 输出结果: 1.工作区变量赋值 示例1 输出结果如下所示&#xff1a; 示例2…

C++ | Leetcode C++题解之第179题最大数

题目&#xff1a; 题解&#xff1a; class Solution { public:string largestNumber(vector<int> &nums) {sort(nums.begin(), nums.end(), [](const int &x, const int &y) {return to_string(x) to_string(y) > to_string(y) to_string(x);});if (nu…

java基于ssm+jsp 汽车在线销售系统

1 前台功能模块 网站首页 网页首页汽车在线销售系统模块如下&#xff1a;首页、汽车信息、新闻资讯、留言反馈、我的收藏管理等功能图1 图1网页首页 网页前台车辆信息效果图如图2所示 图2 车辆信息界面图 2 管理员功能模块 管理员输入个人的账号、密码登录系统&#xff0c…

Android使用zxing生成二维码

效果图如下&#xff1a; **前提&#xff1a;导入zxing的jar后开始操作&#xff0c;老规矩最后有源码&#xff0c;作者布局默认相对布局。 第一步&#xff1a;定义二维码的长宽高及图片控件** 第二步&#xff1a;实例化QRCodeWriter后利用for循环将二维码画出来&#xff0c;然后…

UE4引擎支持HTML5

文章目录 目的UE版本html5版本编译HTML5小结目的 本篇文章主要写下UE4如何编译支持HTML5,至于为什么是UE4而不是UE5,或者一些其他的话题,本篇不做讨论。 UE版本 先来看看UE的版本管理,如下图:有大概70个分支,有些还在维护,有些还在升级;tags有200多个,很可能还会增…

Java程序之可爱的小兔兔

题目&#xff1a; 古典问题&#xff0c;有一对兔子&#xff0c;从出生后第3个月起每个月都生一对兔子&#xff0c;小兔子长到第三个月后每个月又生一对兔子&#xff0c;假如兔子都不死&#xff0c;问每个月的兔子总数为多少? 程序分析&#xff1a; 兔子的规律为数列1,1,2,3,…

【html】用html+css实现银行的账户信息表格

我们先来看一看某银行的账户信息表格 我们自己也可以实现类似的效果 效果图: 大家可以看到&#xff0c;其实效果差不多 接下来看看我们实现的代码 源码&#xff1a; <!DOCTYPE html> <html lang"zh"><head><meta charset"UTF-8"&…

AI网络爬虫:搜狗图片的时间戳反爬虫应对策略

如何批量爬取下载搜狗图片搜索结果页面的图片&#xff1f;以孙允珠这个关键词的搜索结果为例&#xff1a; https://pic.sogou.com/pics?query%E5%AD%99%E5%85%81%E7%8F%A0&mode2 翻页规律如下&#xff1a; https://pic.sogou.com/napi/pc/searchList?mode2&start38…