数据结构之图的基本知识点

一.图的存储结构

1.邻接矩阵

typedef struct{
	int no;  //顶点编号
	char info; //顶点信息,一般没用
}VertexType;
typedef struct{
	int edges[maxSize][maxSize]; //用二维数组存储邻接矩阵,若为带权图则用float型
	int n,e; //n表示顶点数,e表示边数
	VertexType vex[masSize];  //存放节点信息
}MGraph;

2.邻接表

typedef struct ArcNode{
	int adjvex; //该边所指向的节点的位置
	struct ArcNode * nextarc;
	int info;
}ArcNode;
typedef struct{
	struct ArcNode * firstarc;
	char data; //顶点信息
}VNode
typedef struct{
	VNode adjlist[maxSize];
	int n,e; //n表示顶点数,e表示边数
}AGraph;

二.图的遍历算法

1.深度优先搜索遍历(DFS)

深度优先搜索类似于二叉树先序遍历,用递归可以解决。

//以邻接表为存储结构
int visit[maxSize]; //全局数组,记录节点是否被访问过,防止重复访问
void DFS(MGraph * G,int v){ //从v开始遍历
	visit[v]=1;
	visit(v); //访问v节点
	ArcNode * p = G->adjlist[v].firstarc; //找出v的相邻节点
	while(p!=null){
		if(visit[p->adjvex]==0){
			DFS(G,p->adjvex);
		}
		p=p->nextarc; //这里不能写else,可以自己想一下为什么
	}
}

2.广度优先搜索(BFS)

广度优先搜索类似于二叉树层次遍历,所以需要用到队列。

void BFS(MGraph * G,int v){
	int que[maxSize];
	int front = 0,rear = 0; //定义一个队列
	visit[v] = 1;
	visit(v);
	//节点入队
	rear = (rear+1)%maxSize;
	que[rear] = v;
	//当队列不为空
	while(front!=rear){
		//队首节点出队
		front=(front+1)%maxSize;
		j=que[front];
		ArcNode * p = G->adjlist[j].firstarc;
		//找出该节点的所有相邻节点并遍历
		while(p!=null){
			if(visit[p.adjvex]==0){
				visit[p.adjvex]=1;
				visit(p.adjvex);
				rear=(rear+1)%maxSize;
				que[rear]=p.adjvex;
			}
			p=p->nextarc;
		}
				
	}
}

3.注意

以上两种遍历方法是针对连通图的。对非连通图进行遍历,只需将上述遍历函数放在一个循环中,循环用来检测图中的每一个顶点,如果当前顶点没有被访问,则调用上述函数从这个顶点遍历,否则什么也不做。
其实,从某个给顶点开始进行DFS或者BFS,他会把这个顶点所在的极大连通子图遍历一遍,将这些顶点都设为访问过。那么没有访问过的顶点,就是另一个连通子图里的节点。

void dfs(AGraph * G){
	for(int i = 0;i<G->n;i++){
		if(visit[i]==0){
			DFS(G,i);
		}
	}
}
void bfs(AGraph * G){
	for(int i = 0;i<G->n;i++){
		if(visit[i]==0){
			BFS(G,i);
		}
	}
}

4.图的遍历的几个简单应用

4.1 设计一个算法,求不带权无向连通图G中距离顶点V最远的一个顶点。
解析:图的广度优先搜索遍历体现了由图中某个顶点开始,以由近到远层层拓展的方式遍历图中节点的过程,因此广度优先搜索遍历过程中的最后一个顶点一定是距离给定顶点最远的顶点。因此,只需要修改一个BFS即可。

int BFS(MGraph * G,int v){
	int que[maxSize];
	int front = 0,rear = 0; //定义一个队列
	visit[v] = 1;
	//visit(v);
	//节点入队
	rear = (rear+1)%maxSize;
	que[rear] = v;
	//当队列不为空
	while(front!=rear){
		//队首节点出队
		front=(front+1)%maxSize;
		j=que[front];
		ArcNode * p = G->adjlist[j].firstarc;
		//找出该节点的所有相邻节点并遍历
		while(p!=null){
			if(visit[p.adjvex]==0){
				visit[p.adjvex]=1;
				//visit(p.adjvex);
				rear=(rear+1)%maxSize;
				que[rear]=p.adjvex;
			}
			p=p->nextarc;
		}			
	}
	return j;
}

4.2 设计一个算法,判断无向图G是否是一颗树,若是树,返回1,否则返回0;
解析:一个无向图是一棵树的条件是这个图有n-1条边,而且这个图是连通的。那么我们就要判断两个东西:1.这个图是不是有n-1条边。这个可以在遍历时数一下有多少条边,然后判断即可。2.这个图是不是连通的。这个也可以在遍历时统计遍历的顶点数目,如果图是连通的,那么从某个顶点开始遍历,一定可以遍历到所有的顶点,统计的顶点数一定正好为n。

int visit[maxSize]; 
int cn; //遍历过程中遍历到的边数
int en;	//遍历过程中遍历到的顶点数
void DFS(AGraph * G,int v){ //从v开始遍历
	visit[v]=1;
	//visit(v); //访问v节点
	vn++;
	ArcNode * p = G->adjlist[v].firstarc; //找出v的相邻节点
	while(p!=null){
		en++;
		if(visit[p->adjvex]==0){
			DFS(G,p->adjvex);
		}
		p=p->nextarc; 
	}
}
int isTree(AGraph * G){
	cn=0;
	en=0;
	DFS(G,1);
	/*
		vn=G->n 说明图是连通的
		为什么要en/2呢?因为统计每个顶点的边时,两个点之间的边多加了一次,所以相当于				   每条边都多加了一次,所以最后要除以2
		所以en/2为实际统计出来的边数[
	*/
	if(vn==G->n&&G->n-1==(en/2)){
		return 1;
	}else{
		return 0;
	}	
}

4.3 图采用邻接表存储。设计一个算法,判断顶点i与顶点j之间是否有路径。
解析:以顶点i为起点开始遍历。遍历后如果visit[j]==0,说明顶点j没有被访问过,也就是说顶点i与顶点j之间没有路径

int check(AGrgph *G,int i,int j){
	DFS(G,i);
	if(visit[j]==0){
		return 0;
	}else{
		return 1;
	}
}

三.最小生成树

1.普利姆算法

//从V0开始找最小生成树,sum记录最小生成树的权值
void Prim(MGraph G,int v0,int & sum){
	//vset数组记录节点是否添加进入
	//lowcost数组记录目前最小生成树内的节点到其他所有结点的所有边中的最小权值
	int vset[maxSize],lowcost[maxSize],v;
	int i,j,k,v0;
	v=v0;
	//初始化
	for(i=0;i<G.n;i++){
		vset[i]=0; //最初所有节点都不属于最小生成树内
		lowcost[i]=G.edges[v0][i];
	}
	vset[v0] = 1;//把当前节点放入最小生成树
	sum=0;[
	//加入剩余的n-1个节点
	for(i=0;i<G.n-1;i++){
		min = INF; //INF为一个无限大的值
		//找到候选边中的最小值
		for(j=0;j<G.n;j++){
			if(vset[j]==0&&lowcost[j]<min){
				min = lowcost[j];
				k=j;
			}
		}
		vset[k]=1;
		sum=sum+lowcost[k];
		v=k;
		//更新lowcost
		for(j=0;j<G.n;j++){
			if(vset[j]==0&&G.edges[v][j]<lowcost[j]){
				lowcost[j] = G.edges[v][j];
			}
		}
	}
	
}

算法主要用了双层for循环,所以时间复杂度为O(n方)

2.克鲁斯卡尔算法

四.最短路径

1.迪杰斯特拉算法

用于求图中某一顶点到其余各顶点的最短路径。
  dist[Vi]表示当前已找到的V0到每个终点Vi的最短路径的长度。它的初态为:若从V0到Vi有边,则dist[Vi]为边上的权值,否则置dist[Vi]=∞。
  path[Vi]中保存从V0到Vi最短路径上Vi的前一个顶点。Path[]的初态为:如果V0到Vi有边,则path[Vi]=V0,否则path[Vi]=-1。
  set[]为标记数组,set[Vi]=0表示Vi在T中,即没有被并入最短路径;set[Vi]=1表示Vi在S中,即已经被并入最短路径。set[]初态为set[V0]=1,其余元素全为0。

/*
	打印最短路径,path[]数组相当于一颗树,还是双亲表示法表示的树
	它只能输出从叶子节点到根节点的路径
	所以我们需要逆序输出一下
*/
void printPath(int path[],int a){
	int stack[maxSize];  //用栈实现逆序
	int top = -1;
	while(path[a]!=-1){
		stack[++top]=a;
		a=path[a];
	}
	stack[++top]=a; //可以自己模拟一下while循环的过程,最后少加了一个节点
	while(top!=-1){
		cout<<stack[top--]<<" ";
	}
}
void Dijkstra(MGraph G,int v,int dist[],int path[]){
	int set[maxSize];
	//初始化
	int min,i,j,u;
	for(i=0;i<G.n;i++){
		dist[i]=G.edges[v][i];
		set[i]=0;
		if(G.edges[v][i]<INF){ //说明V与I之间有边
			path[i]=v;
		}else{
			path[i]=-1;
		}
	}
	set[v]=1;path[v]=-1;
	//算法主体部分开始,加入剩余n-1个节点
	for(i=0;i<G.n-1;i++){
		min = INF;
		//从dist中找最短的边加入
		for(j=0;j<G.n;j++){
			if(set[j]==0&&dist[j]<min){
				min=dist[j];
				u=j;
			}
		}
		set[u]=1;
		//更新path数组
		for(j=0;j<G.n;j++){
			if(set[j]==0&&dist[u]+G.edges[u][j]<dist[j]){
				dist[j]=G.edges[u][j]+dist[u];
				path[j]=u;
			}
		}
	}
}

算法主体部分为双重循环,所以时间复杂度为O(n2)

2.弗洛伊德算法

用于求图中任意一对顶点之间的最短路径
A用来记录当前已经求得的任意两个顶点最短路径的长度,path用来记录当前两顶点间最短路径上要经过的中间顶点。

void printPath(int u,int v,int path[][maxSize],int A[][maxSize]){
	if(A[u][v]==INF) //uv之间没有最短路径
		return;
	if(path[u][v]==-1)
		cout<<u<<" "<<v;
	else{
		int mid = path[u][v];
		printPath(u,mid,path,A);
		printPath(mid,v,path,A);
	}
}
void Floyd(MGraph * g,int path[][maxSize],int A[][maxSize]){
	//初始化
	int i,j,k;
	for(i=0;i<g->n;i++)
		for(j=0;j<g->n;j++){
			A[i][j]=G->edges[i][j];
			path[i][j]=-1;
	}
	for(k=0;k<g->n;k++)
		for(i=0;i<g->n;i++)
			for(j=0;j<g->n;j++){
				if(A[i][j]>A[i][k]+A[k][j]){
					A[i][j] = A[i][k]+A[k][j];
					path[i][j]=k;
				}
			}
			
}

算法主体部分为三重循环,所以时间复杂度为O(n3)。

五.拓扑排序

/*
	修改邻接表数据结构
	给图节点加上每个节点的入度
*/
typedef struct{
	char data;
	int count; //节点入度
	ArcNode * firstArc;
}VNode;
int TopSort(AGrapht * G){
	ArcNode * p;
	int stack[maxSize];
	int top=-1;
	int i,j;
	int n = 0;//记录已经输出的节点个数
	//初始化,将所有入度为0的节点入栈
	for(i=0;i<G->n;i++){
		if(G->adjlist[i].count==0){
			stack[++top]=i;
		}
	}
	while(top!=-1){
		j=stack[top--];
		n++;
		cout<<j<<" ";
		p=G->adjlist[j].firstArc;
		/*把当前节点的相邻节点的入度全部减一
		相当于去掉当前节点到其他相邻节点的所有边
		如果减后有入度为0的节点就入栈
		*/
		while(p!=null){
			i=p->adjvex;
			G->adjlist[i].count--;
			if(G->adjlist[i].count==0){
				stack[++top]=G->adjlist[i].adjvex;
			}
			p=p->nextArc;
		}
	}
	if(n==G->n)
		return 1;//拓扑排序成功
	else
		return 0;//拓扑排序失败
	
}

时间复杂度:每个节点都要出一次栈,每条边都要被删除掉,所以时间复杂度为O(n+e).

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值