数据结构——图

“ 纸上得来终觉浅,觉知此事要躬行”

目录

1.图的存储结构

1.1.邻接矩阵

1.2.邻接表

2.图的创建

2.1.用邻接矩阵存储图的算法实现(无向带权图)

2.2.用邻接表存储图的算法实现(无向带权图)

3.图的遍历

3.1.邻接矩阵的深度优先遍历(DFS)

3.2.邻接表的深度优先遍历(DFS)

3.3.邻接矩阵的广度优先遍历(BFS)

3.4.邻接表的广度优先遍历(BFS)

4.最小生成树

4.1.Prim算法

4.3.Kruskal算法

 5.最短路径

5.1.DijKstra算法

5.2.Floyd算法

5.3.边的权值相同的图的最短路径(广度优先搜索)

6.拓扑排序


1.图的存储结构

1.1.邻接矩阵

typedef struct {
	char vexs[MAXVEX];//顶点数组
	int arc[MAXVEX][MAXVEX];//矩阵
	int vexnum,arcnum;//顶点数,边数
}MGraph;

1.2.邻接表

 可以将每一行都看成一个单链表,第一个结点的下标就是源点,其余结点的数据域就是源点的邻接点在顶点数组中的下标

typedef struct ArcNode{//边表结点
	int adjvex;
	struct ArcNode *next;
}ArcNode,*ArcList;
typedef struct{//顶点表结点
	char data;
	ArcNode *firstarc;
}VNode,AdjList[MAXVEX];
typedef struct{//图的结构定义
	AdjList vertices;
	int vexnum,arcnum;
}ALGraph;

2.图的创建

2.1.用邻接矩阵存储图的算法实现(无向带权图)

如果是有向图,删去G.arcs[j][i]=arcs[i][j]就行

如果是无权图,初始化可将矩阵每一条边的权值都赋值为0,且读入边的时候不用读入权值了

const int MAXINT=32767;
void createAMGraph(AMGraph &G){//用邻接矩阵存储图的算法实现
	int i,j,w;
	cout<<"请输入顶点数和边数:";
	cin>>G.vexnum>>G.arcnum;
	cout<<"请输入各顶点的字符:"<<endl;
	for(i=0;i<G.vexnum;i++){
		cin>>G.vexs[i]; 
	}
	for(i=0;i<G.vexnum;i++){//对邻接矩阵初始化,将每条边的权值都赋值为无穷大
		for(int j=0;j<G.vexnum;j++){
			G.arcs[i][j]=MAXINT;
		}
	}
	cout<<"请输入各边的顶点(vi,vj)以及边对应的权值:"<<endl;
	for(int k=0;k<G.arcnum;k++){//循环arcnum次,就是读入每一条边的两个顶点以及权值来初始化arcs
		cin>>i>>j>>w;
		G.arcs[i][j]=w;
		G.arcs[j][i]=G.arcs[i][j];//因为是无向图,所以arcs从i到j有边则从j到i也有边
	}
}

2.2.用邻接表存储图的算法实现(无向带权图)

如果是有向图,删除

ArcList s=new ArcNode();
s->adjvex=i;
s->next=G.vertices[j].firstarc;
G.vertices[j].firstarc=s;

如果是无权图,循环读入每条边的时候,就不用读入权值了

头插法:其实顶点表和每一个顶点和它的邻接点的都构成了一个单链表,顶点表的指针域存的就是首结点,然后将邻接点用头插法插入这个单链表中,即邻接点插入到首节点之前,每一个单链表的头结点就是图的顶点表的结点,第i个顶点对应的单链表的首结点就是G.vertiecs[i].firstarc,让s->next=G.vertiecs[i].firstarc,再改变头结点的指针域指向s,即s成为首节点。

void CreateALGraph(ALGraph &G){
    int i,j,k,w;
    cout<<"请输入图的顶点数和边数:";
    cin>>G.vexnum>>G.arcnum;
    cout<<"请输入各顶点编号或字符:";
    for(i=0;i<G.vexnum;i++){//将顶点表初始化,读入顶点的信息并将顶点的指针域指空
        cin>>G.vertices[i].data;
        G.vertices[i].firstarc=NULL;
    }
    cout<<"请输入各边的顶点(vi,vj)及边对应的权值:"<<endl;
    for(k=0;k<G.arcnum;k++){//循环arcnum次,读入每条边的两个顶点,用头插法将邻接点插入到边表中
        cin>>i>>j>>w;
        ArcList e=new ArcNode();
        e->adjvex=j;
        e->next=G.vertices[i].firstarc;
        G.vertices[i].firstarc=e;
        ArcList s=new ArcNode();
        s->adjvex=i;
        s->next=G.vertices[j].firstarc;
        G.vertices[j].firstarc=s;
    }    
}

3.图的遍历

3.1.邻接矩阵的深度优先遍历(DFS)

图的遍历就是从一个顶点出发,遍历图中的所有顶点,深度优先遍历也叫深度优先搜索,深度优先遍历就像树的前序遍历。它从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。这里讲的是连通图,对于非连通图,只需要对它的连通分量分别进行深度优先遍历,即在先前一个顶点进行一次深度优先遍历后,若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作为起始点,重复上述过程,直至图中所有顶点都被访问到为止。

bool visited[G.numVertexes];//标志数组,用来标记某个点是否被访问过
void DFS(MGraph &G,int i){
	visited[i]=true;//对顶点i深度优先遍历,将该顶点标志为已访问
	cout<<G.vexs<<endl;//打印顶点信息
	for(int j=0;j<G.numEdges;j++){
		if(G.arc[i][j]!=0&&!visited[j])//找到顶点i的未被访问的邻接点j
		DFS(G,j);//对未被访问的邻接点深度优先搜索
	}
}
void DFSTraverse(MGraph &G){
	int i;
	for(int i=0;i<G.numVertexes;i++)
		visited[i]=false;//对标志数组初始化
	for(int i=0;i<G.numEdges;i++){
		if(!visited[i]) DFS(G,i);//对于联通,该处dfs只会指向一次
	}
}

3.2.邻接表的深度优先遍历(DFS)

bool visited[G.vesnum]//标志数组
void DFS(ALGraph &G,int i){
	visited[i]=true;//顶点i标记为已访问
	cout<<G.vertices[i].data;//打印顶点信息
	ArcList p=G.vertices[i].firstarc;//得到所有邻接点组成的单链表的首结点
	while(p){//遍历所有邻接点,每一个邻接点结点的数据域存的都是该顶点在顶点表中的下标
		if(!visited[p->adjvex])//如果没有被访问过
		DFS(G,p->adjvex);//就对该邻接点递归遍历
		p=p->next;
	}
}
void DFSTraverse(ALGraph &G){
	for(int i=0;i<G.vexnum;i++){
		visited[i]=false;//初始化标志数组
	}
	for(int i=0;i<G.vexnum;i++){
		if(!visited[i]){//对于连通图,该处只会执行一次
			DFS(G,i);
		}
	}
}

3.3.邻接矩阵的广度优先遍历(BFS)

广度优先遍历有称作广度优先搜索。图的广度优先搜索就类似于树的层序遍历,需要借助队列来实现,对于每一个顶点,访问该顶点后访问它的所有邻接点,即一层层访问。

void BFS(AMGraph &G){
	Queue Q;
	initQ(Q);//初始化一个辅助队列
	int x;
	for(int j=0;j<G.vexnum;j++){//对每一个顶点做循环
		if(!visited[j]){//如果该顶点未被访问过
			cout<<G.vexs[j];//打印顶点
			visited[j]=true;//标记为已访问
			enQueue(Q,j);//将该顶点入队
			while(!isEmpty(Q)){//若当前队列不空
				deQueue(Q,x);//队头出队赋值给x
				for(int k=0;k<G.vexnum;k++){//循环所有顶点
					if(!visited[k]&&G.arcs[x][k]!=0){//如果其他顶点(k)与当前顶点右边且未被访问过
						enQueue(Q,k);//将该顶点入队并标记为已访问
						visited[k]=true;
						cout<<G.vexs[k];//打印顶点
					}
				}
			}
		}
	}
}

3.4.邻接表的广度优先遍历(BFS)

void BFS(ALGraph &G){
	Queue Q;
	initQ(Q);//初始化一个辅助队列
	int i,j;
	for(int i=0;i<G.vexnum;i++){//循环所有顶点
		if(!visited[i]){//如果该顶点没有被访问
			visited[i]=true;//标记为已访问
			cout<<G.vertices[i].data;//打印该顶点
			enQueue(Q,i);//加入队列
			while(!isEmpty(Q)){//当队列不空的时候
				deQueue(Q,j);//队首出队赋值给j
				ArcList p=G.vertices[j].firstarc;//找到顶点j对应的邻接表,即所有的邻接点
				while(p){//当链表不空,即没有遍历完该点的所有邻接点
					if(!visited[p->adjvex]){//该邻接点还未被访问
						visited[p->adjvex]=true;//将该邻接点标记为已访问
						cout<<G.vertices[p->adjvex].data;//打印该顶点的信息
						enQueue(Q,p->adjvex);//将该顶点入队
					}
					p=p->next;
				}
			}
		}
	}
	
} 

4.最小生成树

一个连通图的生成树是一个极小的联通子图,它含有图中的全部顶点,但只有足以构成一棵树的n-1条边,我们把构造联通网的最小代价生成树称为最小生成树。

4.1.Prim算法

Prime算法是从点的角度来考虑构造一个最小生成树,每次加入一个点,改点和已加入的点构成一条权最小的边,加入n个点的时候也有了n-1条边。假设加入最小生成树的点集合为U,未加入的点集合为V,初始时选择任意一个顶点加入集合U,假设该点为a,则U={a},然后从未加入U的点集合V中选一个距离a最近的顶点,假设为b,则U={a,b},然后再从V中选一个距离U中任意一个点最近的顶点,以此类推,直到选完n个顶点,则这n个顶点和n-1条边就构成最小生成树。

其实,每次选入最小生成树的顶点所构成的集合可以看成一个整体,然后我们看这个集合的所有顶点往外引出的边中权值最小的是哪一条,而这个边的终点便是我们要加入最小生成树的顶点

以下面的图为例:

用lowcost[i]表示以i为终点的边的最小权值,当lowcost[i]=0表示以i为终点的最小权值为0,也就是顶点i已经加入了最小生成树

用ajdvex[i]表示以adjvex[i]为起点,i为终点,即加入最小生成树的终点i与点adjvex[i]构成的边

则最小生成树第i条边可以表示为<adjvex[i],i>=lowcost[i]

我们以V0为起始点 ,则顶点V0加入生成树

初始时lowcost数组值为[0,10,*,*,*,11,*,*,*] adjvex为[0,0,0,0,0,0,0,0,0] 

 

最小生成树只有V0,所以找一个离V0最近的顶点,即V1加入最小生成树,则V0-V1这条边被加入最小生成树, 因为V1的加入,这个最小生成树中的点到其他点的最小距离也可能会改变,更新两个数组 lowcost数组的值为[0,0,18,*,*,11,16,*,12] adjvex数组为[0,1,0,0,0,0,1,0,1]

最小生成树已有V0和V1,我们找距离这两个点任意一个最小的顶点,其实就是找从V0和V1引出的边权值最小的,即V5加入最小生成树,边V0-V5权值最小,加入生成树,因为V5的加入,我们更新到最小生成树到其他顶点的距离 lowcost数组的值为[0,0,18,*,26,0,16,*,12] adjvex数组为[0,0,1,0,5,0,1,0,1]

 

最小生成树已有V0,V1,V5,我们找一个距离三个顶点任意一个权值最小的边,即V8,V1-V8加入最小生成树,由于V8的加入,我们更新其他顶点到最小生成树中顶点的距离  lowcost数组的值为[0,0,8,21,26,0,16,*,0] adjvex数组为[0,0,8,8,5,0,1,0,1]

最小生成树已有V0,V1,V5,V8,我们再从未加入最小生成树的顶点中找一个距离已加入最小生成树里的任意顶点权值最小的顶点,可知V2到V8这条边权值最小,V2加入最小生成树,V2-V8这条边加入最小生成树,由于V2的加入, 我们更新其他顶点到最小生成树的顶点距离  lowcost数组的值为[0,0,0,21,26,0,16,*,0] adjvex数组为[0,0,8,8,5,0,1,0,1]

最小生成树已有V0,V1,V5,V8,V2,我们再从未加入最小生成树的顶点中找一个距离已加入最小生成树里的任意顶点权值最小的顶点,可知V1到V6这条边权值最小,V6加入最小生成树,V1-V6这条边加入最小生成树,由于V6的加入, 我们更新其他顶点到最小生成树的顶点距离  lowcost数组的值为[0,0,0,21,26,0,0,19,0] adjvex数组为[0,0,8,8,5,0,1,6,1] 

最小生成树已有V0,V1,V5,V8,V2,V6,我们再从未加入最小生成树的顶点中找一个距离已加入最小生成树里的任意顶点权值最小的顶点,可知V6到V7这条边权值最小,V7加入最小生成树,V6-V7这条边加入最小生成树,由于V7的加入, 我们更新其他顶点到最小生成树的顶点距离  lowcost数组的值为[0,0,0,16,4,0,0,0,0] adjvex数组为[0,0,8,7,7,0,1,6,1]  

 

最小生成树已有V0,V1,V5,V8,V2,V6,V7,我们再从未加入最小生成树的顶点中找一个距离已加入最小生成树里的任意顶点权值最小的顶点,可知V7到V4这条边权值最小,V4加入最小生成树,V7-V4这条边加入最小生成树,由于V4的加入, 我们更新其他顶点到最小生成树的顶点距离  lowcost数组的值为[0,0,0,16,0,0,0,0,0] adjvex数组为[0,0,8,8,7,0,1,6,1]   

 

最小生成树已有V0,V1,V5,V8,V2,V6,V7,V4,我们再从未加入最小生成树的顶点中找一个距离已加入最小生成树里的任意顶点权值最小的顶点,可知V7到V3这条边权值最小,V4加入最小生成树,V7-V3这条边加入最小生成树,由于V3的加入, 我们更新其他顶点到最小生成树的顶点距离  lowcost数组的值为[0,0,0,0,0,0,0,0,0] adjvex数组为[0,0,8,8,7,0,1,6,1]    

 现在,lowcost数组的值全部为0,表示所有的顶点均已加入最小生成树,则该图的最小生成树就出来啦,而adjvex这个数组里存的值代表什么意思呢,让我们分析一下adjvex[0]=0因为0是起始点,adjvex[1]=0因为我们加入最小生成树的边是V0-V1,adjvex[2]=8因为我们加入最小生成树的边是V2-V8,依次类推,便知道adjvex的作用啦,<adjvex[i],i>其实就是加入最小生成树的一条边。

算法实现:

void MinSpanTree_Prim(MGraph &G){
	int min,i,j,k;
	int adjvex[MAXVEX];
	int lowcost[MAXVEX];
	int sum=0;//用来计算各边权值和
	adjvex[0]=0;//初始化adjvex[0]=0
	lowcost[0]=0;//初始化lowcost[0]=0表示V0加入最小生成树
	for(i=1;i<G.numVertexes;i++){//对lowcost初始化其实就是用V0到所有顶点的距离来初始化
		lowcost[i]=G.arc[0][i];
		adjvex[i]=0;//所有顶点都是以V0为起始点
	}
	for(i=1;i<G.numVertexes;i++){//循环n-1次,则可以找出n-1个顶点加入最小生成树
		min=INF;
		j=1,k=0;
		while(j<G.numVertexes){//我们从目前未加入最小生成树的边中找一个最短的
			if(lowcost[j]!=0&&lowcost[j]<min){
				min=lowcost[j];
				k=j;//记录这个边的终点的下标
			}
			j++;
		}
		cout<<"("<<"V"<<adjvex[k]<<","<<"V"<<k<<")"<<"="<<min<<endl;
		sum+=G.arc[adjvex[k]][k];
		lowcost[k]=0;//将lowcost[k]赋值为0,表示以顶点k加入最小生成树
		for(j=1;j<G.numVertexes;j++){//根据新加入的顶点改变最小生成树集合到其他顶点的最小权值
			if(lowcost[j]!=0&&G.arc[k][j]<lowcost[j]){
				lowcost[j]=G.arc[k][j];
				adjvex[j]=k;
			}
		}
	}
	cout<<"最小生成树的权值和:"<<sum<<endl;
}

根据本图输出样例:

 

4.3.Kruskal算法

Kruskal算法是从边的角度构造最小生成树,每次选择一条权值最小的边加入最小生成树,直到找到n-1条边,则构造完成。但加入的边不仅要是最小,还要满足不何已加入最小生成树的边构成回路,这个条件可以用并查集来实现。此时我们要用到图的存储结构的边集数组。

typedef struct node{
	int begin;
	int end;
	int weight;
	bool operator<(const struct node &b) const{
		return weight<b.weight;
	}
}Edge;

我们用一个parent数组来存每个点的祖宗结点。初始化是parent数组全部为0

我们会有一个find函数来找传入的顶点的祖宗结点

int Find(int *parent,int f){//从parent数组里找到f的祖宗结点
	while(parent[f]>0){
		f=parent[f];
	}
	return f;
}

我们还以下图为例来演示如何构造最小生成树  

 

  选择第1条边,从未加入最小生成树的边中选择一条最小的边,由图可知,权值最小的边为V4-V7,而且这条边没有与最小生成树里的其他边构成回路,则加入最小生成树。在find函数里,先传入4,由于parent[4]=0,所以直接返回4,同样,传入7的时候也返回7了,4!=7即说明这条边的两个顶点原来不属于同一个集合,我们就可以将这条边加入最小生成树,并合并这两个集合,我们将以此边的结尾顶点放入下标为起点的parent数组里,即parent[4]=7,此时parent数组的值为[0,0,0,0,7,0,0,0,0]

 选择第2条边,从未加入最小生成树的边中选择一条最小的边,由图可知,权值最小的边为V2-V8,而且这条边没有与最小生成树里的其他边构成回路,则加入最小生成树。在find函数里,先传入2,由于parent[2]=0,所以直接返回2,同样,传入8的时候也返回8了,2!=8即说明这条边的两个顶点原来不属于同一个集合,我们就可以将这条边加入最小生成树,并合并这两个集合,我们将以此边的结尾顶点放入下标为起点的parent数组里,即parent[2]=8,此时parent数组的值为[0,0,8,0,7,0,0,0,0]

 

选择第3条边,从未加入最小生成树的边中选择一条最小的边,由图可知,权值最小的边为V0-V1,而且这条边没有与最小生成树里的其他边构成回路,则加入最小生成树。在find函数里,先传入0,由于parent[0]=0,所以直接返回0,同样,传入1的时候也返回1了,0!=1即说明这条边的两个顶点原来不属于同一个集合,我们就可以将这条边加入最小生成树,并合并这两个集合,我们将以此边的结尾顶点放入下标为起点的parent数组里,即parent[0]=1,此时parent数组的值为[1,0,8,0,7,0,0,0,0] 

 选择第4条边,从未加入最小生成树的边中选择一条最小的边,由图可知,权值最小的边为V0-V5,而且这条边没有与最小生成树里的其他边构成回路,则加入最小生成树。在find函数里,先传入0,由于parent[0]=1,parent[1]=0,所以返回1,同样,传入5的时候parent[5]=0,返回5了,1!=5即说明这条边的两个顶点原来不属于同一个集合,我们就可以将这条边加入最小生成树,并合并这两个集合,我们将以此边的结尾顶点放入下标为起点的parent数组里,即parent[1]=5,此时parent数组的值为[1,5,8,0,7,0,0,0,0] 

 选择第5条边,从未加入最小生成树的边中选择一条最小的边,由图可知,权值最小的边为V1-V8,而且这条边没有与最小生成树里的其他边构成回路,则加入最小生成树。在find函数里,先传入1,由于parent[1]=5,parent[5]=0,所以返回5,同样,传入8的时候parent[8]=0,返回8了,5!=8即说明这条边的两个顶点原来不属于同一个集合,我们就可以将这条边加入最小生成树,并合并这两个集合,我们将以此边的结尾顶点放入下标为起点的parent数组里,即parent[5]=8,此时parent数组的值为[1,5,8,0,7,8,0,0,0] 

选择第6条边,从未加入最小生成树的边中选择一条最小的边,由图可知,权值最小的边为V3-V7,而且这条边没有与最小生成树里的其他边构成回路,则加入最小生成树。在find函数里,先传入3,由于parent[3]=0,所以返回3,同样,传入7的时候parent[7]=0,返回7了,3!=7即说明这条边的两个顶点原来不属于同一个集合,我们就可以将这条边加入最小生成树,并合并这两个集合,我们将以此边的结尾顶点放入下标为起点的parent数组里,即parent[3]=7,此时parent数组的值为[1,5,8,7,7,8,0,0,0]  

 

选择第7条边,从未加入最小生成树的边中选择一条最小的边,由图可知,权值最小的边为V1-V6,而且这条边没有与最小生成树里的其他边构成回路,则加入最小生成树。在find函数里,先传入1,由于parent[1]=5,parent[5]=8,parent[8]=0,所以返回8,同样,传入6的时候parent[6]=0,返回6了,8!=6即说明这条边的两个顶点原来不属于同一个集合,我们就可以将这条边加入最小生成树,并合并这两个集合,我们将以此边的结尾顶点放入下标为起点的parent数组里,即parent[8]=6,此时parent数组的值为[1,5,8,7,7,8,0,0,6]   

 

选择第8条边,从未加入最小生成树的边中选择一条最小的边,由图可知,权值最小的边为V5-V6,但这条边与加入生成树的其他边构成回路了,不能加入最小生成树。在find函数里,先传入5,由于parent[5]=8,parent[8]=6,parent[6]=0,所以返回6,同样,传入6的时候parent[6]=0,返回6了,6=6即说明这条边的两个顶点属于同一个集合,我们就不可以将这条边加入最小生成树

那就要重新选择第8条边,由图可知,除了已加入最小生成树的边和V5-V6这条边,权值最小的边是V6-V7,且这条边不与最小生成树的其他边构成回路,故可以加入最小生成树。在find函数里,先传入6,parent[6]=0,故返回6,同样传入7的时候,parent[7]=0返回7,6!=7即说明这条边的两个顶点不属于同一个集合,我们就可以将这条边加入最小生成树,并合并这两个集合,我们将以此边的结尾顶点放入下标为起点的parent数组里,即parent[6]=7,此时parent数组的值为[1,5,8,7,7,8,7,0,6]    

 到此,我们就找到了n-1条边,且将所有顶点都加入了最小生成树。

算法实现:

void MinSpanTree_Kruskal(MGraph &G){
	int parent[MAXVEX];
	Edge edge[MAXEDGE];
	int sum=0;
	cout<<"请输入顶点数和边数:";
	cin>>G.numVertexes>>G.numEdges;
	for(int i=0;i<G.numVertexes;i++)
	parent[i]=0;
	cout<<"请输入边(vi,vj)以及对应的权值w:";
	for(int i=0;i<G.numEdges;i++){
		cin>>edge[i].begin>>edge[i].end>>edge[i].weight;
	}
	sort(edge,edge+G.numEdges);
	for(int i=0;i<G.numEdges;i++){//循环遍历所有边
		int m=Find(parent,edge[i].begin);//找到该边起始点的祖宗结点
		int n=Find(parent,edge[i].end);//找到该边的终点的祖宗结点
		if(m!=n){//如果不相等就加入最小生成树
			parent[m]=n;//并合并
			cout<<"("<<edge[i].begin<<","<<edge[i].end<<")"<<" "<<edge[i].weight<<endl;
			sum+=edge[i].weight;
		}
	}
	cout<<"最小生成树的权值之和为:"<<sum<<endl;
}

 根据本图的输出样例:

 5.最短路径

5.1.DijKstra算法

该算法是典型的单源最短路算法,可以求源点到其余各点的最短路径。从源点出发,先找到一个距离源点最近的顶点,然后根据这个新加入的顶点去更新源点到其他点的距离,比如说如果a到b的距离是4,a到c的距离是6,b到c的距离是1,如果直接从a到c的距离是大于从a到b在到c的,那我们就可以通过b来更新a到c的最短路径,而且因为每次我们都选的是目前所能到达的边里最短的,基于贪心,我们最后到达每一个顶点的路径一定都是最短的。

用D[i]表示从源点到顶点i的最短路径,用final[i]=1表示已求出源点到顶点i的最短路径中,用P[i]表示从源点到顶点i的最短路径要进过的前驱顶点。初始时用v0到各点的距离来初始化D数组,所以P数组全部初始化为0,即都是从V0顶点直接到达的,final数组全部赋值为0.

以下图为例来模拟DijKstra算法

 我们以V0为源点来求V0到各点的最短路径

 则D数组的值为[0,1,5,*,*,*,*,*,*],final数组的值[1,0,0,0,0,0,0,0,0] P数组的值[0,0,0,0,0,0,0,0,0]

距离源点最近的很明显就是1,并求出了V0到V1的最短路径就是1,故应将此顶点的final值设为1,并根据V1的加入来修改V0到其他点的最短路径,比如说V0到V5的路径本来是5,但当从V0-V1-V5时,最短路径变成了1+3=4,这就是我们每次加入一个顶点就以此顶点为源点更新其他路径的原因,此次更新的还要顶点V3,V4,因为本来V0到V3和V4的距离是无穷大的,但此时因为V1的加入,这两点是可达的了。而且顶点V2,V3,V4的前驱都应该是1。

则D数组的值为[0,1,4,8,6,*,*,*,*],final数组的值[1,1,0,0,0,0,0,0,0] P数组的值[0,0,1,1,1,0,0,0,0]

然后再从D数组里找一条距离V0权值最小的顶点,由于V0和V1已求出最短路径,则不参与最小权值的比较了,这样就可以得到剩下的里面权值最小的是4,对应的顶点小标2,即顶点V2加入,则说明求出了V0到V2的最短路径就是4,修改final[2]=1,再根据加入的V2更新V0到其他点的最短距离,原本V0到V5的最短距离是无穷,由于V2的加入,最短距离更新为4+7,本来V0到V4的最短距离是从V0-V1-V4为6,但现在可以从V0-V1-V2-V4为,5,且这两个顶点的前驱都是2

则D数组的值为[0,1,4,8,5,11,*,*,*],final数组的值[1,1,1,0,0,0,0,0,0] P数组的值[0,0,1,1,2,2,0,0,0]

 

然后再从D数组里找一条距离V0权值最小的顶点,由于V0和V1还有V2已求出最短路径,则不参与最小权值的比较了,这样就可以得到剩下的里面权值最小的是5,对应的顶点下标4,即顶点V4加入,则说明求出了V0到V4的最短路径就是5,修改final[4]=1,再根据加入的V4更新V0到其他点的最短距离,原本V0到V6,V7的最短距离是无穷,由于V4的加入,最短路径变成了5+6,5+9,本来V0到V5的最短距离是11,现在经过V4,最短路径变成了5+3=8,本来V0到V3的最短路径是8,现在,最短路径变成了5+2=7,故更新这几个顶点到V0的最短距离并将前驱改为4

则D数组的值为[0,1,4,7,5,8,11,14,*],final数组的值[1,1,1,0,1,0,0,0,0] P数组的值[0,0,1,4,2,4,4,4,0]

然后再从D数组里找一条距离V0权值最小的顶点,由于V0、V1、V2、V4已求出最短路径,则不参与最小权值的比较了,这样就可以得到剩下的里面权值最小的是7,对应的顶点下标3,即顶点V3加入,则说明求出了V0到V3的最短路径就是7,修改final[3]=1,再根据加入的V3更新V0到其他点的最短距离,原本V0到V6的最短路径是11,现在经过V3从V0到V6的最短路径是7+3=10,将顶点6的前驱修改为3

则D数组的值为[0,1,4,7,5,8,10,14,*],final数组的值[1,1,1,1,1,0,0,0,0] P数组的值[0,0,1,4,2,4,3,4,0]

然后再从D数组里找一条距离V0权值最小的顶点,由于V0、V1、V2、V4、V3已求出最短路径,则不参与最小权值的比较了,这样就可以得到剩下的里面权值最小的是8,对应的顶点下标5,即顶点V5加入,则说明求出了V0到V5的最短路径就是8,修改final[5]=1,再根据加入的V5更新V0到其他点的最短距离,原本V0到V7的最短路径是14,但由于V5的加入,最短路径变成了8+5=13,将顶点V7的前驱修改为5

则D数组的值为[0,1,4,7,5,8,10,13,*],final数组的值[1,1,1,1,1,1,0,0,0] P数组的值[0,0,1,4,2,4,3,5,0]

 

 

然后再从D数组里找一条距离V0权值最小的顶点,由于V0、V1、V2、V4、V3、V5已求出最短路径,则不参与最小权值的比较了,这样就可以得到剩下的里面权值最小的是10,对应的顶点下标6,即顶点V6加入,则说明求出了V0到V6的最短路径就是10,修改final[6]=1,再根据加入的V6更新V0到其他点的最短距离,原本V0到V8的最短路径为 无穷,但由于V6的的加入变成了10+7=17,原本V0到V7的最短路径是13,但由于V6的加入变成了10+2=12,故更新源点到这两个顶点的最短路径并将前驱修改为6

则D数组的值为[0,1,4,7,5,8,10,12,17],final数组的值[1,1,1,1,1,1,1,0,0] P数组的值[0,0,1,4,2,4,3,6,6]

然后再从D数组里找一条距离V0权值最小的顶点,由于V0、V1、V2、V4、V3、V5、V6已求出最短路径,则不参与最小权值的比较了,这样就可以得到剩下的里面权值最小的是12,对应的顶点下标7,即顶点V7加入,则说明求出了V0到V7的最短路径就是12,修改final[7]=1,再根据加入的V7更新V0到其他点的最短距离,原本V0到V8的最短路径是17,但是由于V7的加入变成了12+4=16,故修改最短路径并将V8的前驱改为7 

则D数组的值为[0,1,4,7,5,8,10,12,16],final数组的值[1,1,1,1,1,1,1,1,1] P数组的值[0,0,1,4,2,4,3,6,7]

 

然后再从D数组里找一条距离V0权值最小的顶点,由于V0、V1、V2、V4、V3、V5、V6、V7已求出最短路径,则不参与最小权值的比较了,这样就可以得到剩下的里面权值最小的是16,对应的顶点下标8,即顶点V8加入,则说明求出了V0到V8的最短路径就是16,修改final[8]=1,此时所有的顶点均已求出最短路径,不用继续更新了

则D数组的值为[0,1,4,7,5,8,10,12,16],final数组的值[1,1,1,1,1,1,1,1,0] P数组的值[0,0,1,4,2,4,3,6,7]

至此,我们就求出了V0到其他点的最短路径了,保存在了D数组里,那P数组是什么呢,让我们来分析一下,就拿8来说,P[8]=7,说明从V0到V8的前驱顶点是V7,P[7]=6说明从V0到V7的最短路径前驱顶点是V6,P[6]=3说明从V0到V6的最短路径的前驱顶点是V3,P[3]=4,说明从V0到V3的前驱顶点是V4,P[4]=2说明从V0到V4的前驱顶点是V2,P[2]=1说明从V0到V2的前驱顶点是1,P[1]=0说明从V0到V1的前驱就是V0,这样我们就逆着推出了从V0到V8的最短路径经过的顶点V0->V1->V2->V4->V3->V6->V7->V8 

 算法实现:

void ShortestPath_Dijkstra(MGraph& G){
	int v,w,k,min;
	int final[MAXVEX];
	for(int i=0;i<G.numVertexes;i++){//对几个辅助数组初始化
		final[i]=0;
		D[i]=G.arc[0][i];
		P[i]=0;
	}
	D[0]=0;
	final[0]=1;
	for(v=1;v<G.numVertexes;v++){//循环每次加入一个顶点
		min=INF;
		for(w=0;w<G.numVertexes;w++){//遍历所有顶点
			if(!final[w]&&D[w]<min){//从还未求出最短路径的顶点中找出一个最短的,并记录其下标
				min=D[w];
				k=w;
			}
		} 
		final[k]=1;//将该顶点标记为已求出到源点的最短路径
		for(w=0;w<G.numVertexes;w++){//更加新加入的顶点更新源点到其他点的最短路径
			if(!final[w]&&D[w]>min+G.arc[k][w]){//如果经过当前已求出最短路径的顶点到达比从V0直接到达短,就更新最短路径
				D[w]=min+G.arc[k][w];
				P[w]=k;
			}
		}
	}
	for(int i=0;i<G.numVertexes;i++) cout<<D[i]<<" ";
}

5.2.Floyd算法

该算法可用来所有顶点到所有顶点的最短路径,该算法是基于动态规划来求解的。

数组D[i][j]表示顶点i到j的最短路径,P[i][j]表示从顶点i到顶点j的最短路径的要经过前驱

直接用图的邻接矩阵将D数组初始化,而从顶点i到顶点j的前驱都是j本身,也就是相当于直接从Vi到达Vj。然后我们用一个三重循环来求最短路径,k代表的就是中转顶点的下标,v代表的就是起始顶点,w代表的就是结束顶点。对于顶点i,j,如果从i到k再到j的路径比直接从i到j的最短路径短,我们就更新从i到j的最短路径,起始与DijKstra根据已求出最短路径的顶点k去更新源点到其余顶点的最短路径一样,只不过该算法不一定是根据已求出的最短路径来更新,是任意顶点到任意顶点的最短路径都要用所有顶点作为中转顶点来更新一下

这个P数组如何记录路径的呢,还是以V8为例,P[0][8]=1,表示从V0到V8要经过V1,然后用1取代0得到P[1][8]=2,说明从V1到V8要经过顶点V2,用2取代1得到P[2][8]=4,说明从V2到V8要经过顶点V4,。。。这样就很容易得到最终的路径值:V0->V1-V2-V4->V3->V6->V7->V8

void ShortPath_Floyd(MGraph &G){//Floyd求最短路
	int P[MAXVEX][MAXVEX];
	int D[MAXVEX][MAXVEX];
	int v,w,k;
	for(v=0;v<G.vexnum;v++){
		for(w=0;w<G.vexnum;w++){
			D[v][w]=G.arc[v][w];
			P[v][w]=w;
		}
	}
	for(k=0;k<G.vexnum;k++){//三重循环,看从顶点V到顶点w的最短路径是否经过k中转之后可以变的更短
		for(v=0;v<G.vexnum;v++){
			for(w=0;w<G.vexnum;w++){
				if(D[v][w]>D[v][k]+D[k][w]){
					D[v][w]=D[v][k]+D[k][w];
					P[v][w]=P[v][k];
				}
			}
		}
	}
}
void printPath(MGraph &G,int D[MAXVEX][MAXVEX],int P[MAXVEX][MAXVEX]){//打印所有顶点到所有顶点的最短路径
	for(int v=0;v<G.vexnum;v++){
		for(int w=0;w<G.vexnum;w++){
			cout<<"(V"<<v<<",V"<<w<<")"<<D[v][w]<<":";
			int k=P[v][w];
			cout<<"path:"<<v;
			while(k!=w){
				cout<<"->"<<k;
				k=P[k][w];
			}
			cout<<"->"<<w<<" "<<endl;
		}
	}
	cout<<endl;
}

5.3.边的权值相同的图的最短路径(广度优先搜索)

对于无权图或者所有边的权值相同的如,求最短路径就是经过的边数,最短路径就是经过最少的边,因此可以考虑用广度优先搜索来实现,因为广度优先搜索是一层层来搜索,求从i到j的最短路径,只要中层序遍历的过程,遍历到了j,此时求得的路径就是最短的。一次只要稍微修改一下图的广度优先搜索就能找到i到j的最短路径啦,但是为了知道路径,我们要借助一个辅助数组front来保存遍历时经过的顶点的前驱其实就是上一层的顶点,所以每一个顶点到源点的最短路径其实就是上一层顶点到源点的顶点数+1(这里我们假设所有边的权值都是1);

以无权图为例来求最短路径,初始时front数组的值为-1,dis数组的值为-1,队列queue为空,假设我们要求V0到V7的最短路径。

 首先,将V0入队,然后开始广度优先搜索,V0的前驱是0,使用front[0]=0,V0到V0的最短路径是0,所以dis[0]=0,队列queue=[0];

然后当队列不空,就将队首元素出队,遍历队首的邻接点,将所有邻接点入队并修改邻接点的front和dis。V0出队,V0的邻接点V1,V5入队,且V1,V5的前驱是V0,V1和V5到V0的顶点数就是V0到V0的路径长度+1,front数组的值[0,0,-1,-1,-1,0,-1,-1,-1],dis数组的值[0,1,-1,-1,-1,1,-1,-1,-1] queue=[1,5]。因为1和5都不是我们要找的终点,所以继续。

 

 队列不空,队首出队,1出队,遍历1所有邻接点,将未被访问过的邻接点V2,V8,V6入队,这几个顶点的前驱都是V1,所以这几个顶点到V0的路径就是V1到V0的路径加一,为2。front数组的值[0,0,1,-1,-1,-1,1,-1,1],dis数组的值[0,1,2,-1,-1,-1,2,-1,2] queue=[5,2,8,6]。因为2,8和6都不是我们要找的终点,所以继续。

 

队列不空,将队首元素出队,即5出队,将5的所有未被访问的邻接点入队,即V7和V4入队,所以V7和V4的前驱都是V5,则V7和V4到V0的最短路径就是V5到V0的最短路径加一,即1+1=2此时 front数组的值[0,0,1,-1,5,-1,1,5,1],dis数组的值[0,1,2,-1,2,-1,2,2,2] queue=[2,8,6,7,4]。我们已经遍历到了我们要找的终点7,所以此次遍历结束,我们得到了V0到V7的最短路径就是D[7]=2。

那怎么从front数组得到遍历的过程的最短路径呢,就那V0到V7来说,front[7]=5,所以说明从V0到V7要经过的前驱是V5,再有front[5]=0可以知道,从V0到V5的最短路径的前驱就是V0,所以从V0到V7的最短路径是V0->V5->V7 

算法实现:

void shortestPath(GraphAdjList &G,int u,int v,bool *visited,int *front,int* dis){
	int queue[MAXVEX];//辅助队列
	int hh=-1;//队首指针
	int tt=-1;//队尾指针
	queue[++hh]=u;//将源点入队列
	visited[u]=true;
	front[u]=0;
	dis[u]=0;
	while(tt<=hh){//当队列不空的时候
		int t=queue[++tt];//队首出队
		EdgeNode *p=G.adjList[t].firstedge;
		while(p){//遍历该顶点的所有邻接点
			int x=p->adjvex;
			if(!visited[x]){//如果邻接点没有访问过就入队
				queue[++hh]=x;
				visited[x]=true;
				front[x]=t;//该顶点的前驱就是t,因为它是t的邻接点
				dis[x]=dis[t]+1;//该顶点到源点的最短路就是上一层到源点的最短路加一
			//	cout<<x<<" "<<dis[x]<<" "<<t<<" "<<dis[t]<<endl;
			}
			if(x==v){//当我们遍历到了顶点v,说明找到了最短路径
				return;
			}
			p=p->next;
		}
	}
}

6.拓扑排序

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,称为AOV网。AOV网中的弧表示活动之间存在的某种制约关系。设G=<V,E>是一个具有n个顶点的有向图,V中的的顶点序列v1,v2,...,vn满足若从Vi到Vj有一条路径,则在顶点序列中顶点Vi必在顶点Vj之前。则我们称这样的顶点序列为一个拓扑序列

所谓拓扑排序就是对一个有向图构造拓扑序列的过程

基本思路:每次从AOV网中选择一个入度为0的顶点,输出后删除该顶点以及以此顶点为尾的弧,重复此步骤,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止。

由于拓扑排序要删除顶点,所以我们用邻接表来存储,在原来的顶点表中增加一个入度,存储结构如下:

typedef struct EdgeNode{//边表结点 
	int adjvex;
	struct EdgeNode *next;
}EdgeNode;
typedef struct VextexNode{//顶点表结点 
	int in;//入度 
	int data;
	EdgeNode *firstedge;
}AdjList[MAXVEX];
typedef struct{//图 
	AdjList adjList;
	int numVextexes,numEdges;
}graphAdjList,*GraphAdjList;

因为要从入度为0的顶点开始,为了不每次都遍历顶点表,用一个辅助栈来存放入度为0的顶点。

算法实现步骤:

  1. 初始化一个辅助栈,将所有入度为0的顶点入栈
  2. 当栈不空的时候,将栈顶元素出栈
    1. 删除该顶点,打印顶点信息
    2. 记录拓扑序列的顶点个数的变量+1
    3. 遍历该顶点的所有邻接点,并将所有邻接点的入度减一
    4. 如果邻接点有入度为0的顶点,入栈
  3. 最后看是否所有顶点都已加入拓扑序列,
    1. 是,则说明可以构成拓扑序列
    2. 不是,则说明该图不存在拓扑序列

算法实现:

bool TopologicalSort(GraphAdjList &G){
	int *stack;
	int count=0;//用来记录加入拓扑序列的顶点个数
	stack=new int[G->numVextexes];//初始化栈
	int top=-1;//栈顶指针
	for(int i=0;i<G->numVextexes;i++){
		if(G->adjList[i].in==0){//遍历所有顶点,如果存在入度为0的顶点,入栈
			stack[++top]=i;
		}
	}
	while(top>=0){
		int idx=stack[top--];//栈顶元素出栈
		EdgeNode *s=G->adjList[idx].firstedge;
		cout<<G->adjList[idx].data<<"->";
		count++;//拓扑序列的顶点数加一
		while(s){//遍历顶点的所有邻接点
			int x=s->adjvex;
			if(!(--G->adjList[x].in)){//如果删除该点,邻接点有入度为0的顶点,入栈
				stack[++top]=x;
			}
			s=s->next;
		}
	}
	if(count==G->numVextexes) return true;
	return false;
}

 今天整理了数据结构图论的部分算法,希望能帮到在学数据结构的你呀~原创不易,如转载,注明出处,有错误也欢迎指正。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值