数据结构:图(基础概念及操作,图文解释)

图的定义

图(Graph)——图G是由两个集合V(G)和E(G)组成的,记为G=(V,E)其中:V(G)是顶点的非空有限集,E(G)是边的有限集合,边是顶点的无序对或有序对。

有向图

有向图——有向图G是由两个集合V(G)和E(G)组成
其中:V(G)是顶点的非空有限集,E(G)是有向边(也称弧)的有限集合,弧是顶点的有序对,记为<v,w>,v,w是顶点,v为弧尾,w为弧头
在这里插入图片描述

无向图

无向图——无向图G是由两个集合V(G)和E(G)组成
其中:V(G)是顶点的非空有限集,E(G)是边的有限集合,边是顶点的无序对,记为(v,w)或(w,v),并且(v,w)=(w,v)。
在这里插入图片描述

有向完全图——n个顶点的有向图最大边数是n(n-1)
无向完全图——n个顶点的无向图最大边数是n(n-1)/2
在这里插入图片描述

基本术语概念

——与图的边或弧相关的数

——带权的图

顶点的度

  • 无向图中,顶点的度为与每个顶点相连的边数;
  • 有向图中,顶点的度分成入度与出度;
    • 入度:以该顶点为头的弧的数目;
    • 出度:以该顶点为尾的弧的数目;

路径——路径是顶点的序列V={Vi0,Vi1,……Vin},满足(Vij-1,Vij)∈E 或 < Vij-1,Vij>∈E,(1<j≦n)

路径长度——沿路径边的数目或沿路径各边权值之和

回路——第一个顶点和最后一个顶点相同的路径

简单路径——序列中顶点不重复出现的路径

简单回路——除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路

连通——从顶点V到顶点W有一条路径,则说V和W是连通的

连通图——图中任意两个顶点都是连通部分

连通分量——非连通图的每一个连通部分

强连通图——有向图中,如果对每一对 Vi,Vj 从 Vi 到 Vj 和从 Vj 到 Vi 都存在路径。

在这里插入图片描述

常用图的存储结构

邻接矩阵法

邻接矩阵法——表示顶点间相联关系的矩阵
在这里插入图片描述
特点:

  1. 无向图的邻接矩阵对称,可压缩存储;有n个顶点的无向图需存储空间为n(n+1)/2;
  2. 有向图邻接矩阵不一定对称;有n个顶点的有向图需存储空间为n²;
  3. 无向图中顶点Vi的度TD(Vi)是邻接矩阵A中第i行元素之和;
  4. 有向图中, 顶点Vi的出度是A中第i行元素之和; 顶点Vi的入度是A中第i列元素之和;

邻接表法

实现:为图中每个顶点建立一个单链表,第i个单链表中的结点表示依附于顶点Vi的边(有向图中指以Vi为尾的弧)
在这里插入图片描述
特点:

  1. 无向图中顶点Vi的度为第i个单链表中的结点数;
  2. 有向图中 顶点Vi的出度为第i个单链表中的结点个数;顶点Vi的入度为整个单链表中邻接点域值是i的结点个数;
  3. 逆邻接表:有向图中对每个结点建立以Vi为头的弧的单链表;

图的基础操作

这里我采用的是邻接链表存储结构。图的遍历不管是广度遍历还是深度和树的遍历过程都非常相似,在这里我就很简单概述一下我的思路,如果不理解可以好好看一下我的另一篇文章------ 数据结构:树和二叉树(基础概念操作图文解释)同时不论是深度遍历还是广度遍历都需要辅助空间来标记某结点是否被访问过。

生成邻接链表

这里的代码很简单,唯一值得注意的是find函数以及输入的关系声明为char x,y;这里只是为了让整个程序有更好的适应性,即无论这个关系是数字与数字之间还是字母与字母之间程序都可以很好的运行。

int find(int n,char v)//求v在表头数组中的下标
{
	for(int i=1;i<=n;i++)
		if(m[i].data==v)
			return i;
	return -1;
}

void creat(int n1,int n2)//生成邻接表 
{
	char x,y;
	int z,n;
	jd* p;
	getchar();//吸收回车
	for(int i=0;i<n2;i++)
	{
		cin>>x>>y;
		getchar();
		z=find(n1,x);
		n=find(n1,y);
		
		p=(jd*)malloc(sizeof(jd));
		p->id=z;
		p->next=m[n].firstnode;//头插 
		m[n].firstnode=p;
		
		p=(jd*)malloc(sizeof(jd));
		p->id=n;
		p->next=m[z].firstnode;
		m[z].firstnode=p;
	}
}

深度遍历

思路: 每次先访问表头的第一个孩子结点,若未访问过,则访问这个以这个孩子结点为表头的第一个孩子节点,若访问过,则访问它的第二个孩子结点······,这个过程非常类似于树的深度遍历不断访问左孩子过程。

void dfs(int v,int visit[10])//深度遍历 
{
	visit[v]=1;//标记访问过的位置
	jd* w;
	w=m[v].firstnode;//访问v结点的第一个孩子节点
	cout<<v;
	
	while(w!=NULL)//解决某结点分叉 
	{
		if(!visit[w->id])//如果w->id未访问过继续递归进入下一层
			dfs(w->id,visit);
		w=w->next;//如果访问过就访问w->id的兄弟节点
	}
}

广度遍历

思路: 顺序访问起始表头(这个表头是随意的)的所有孩子结点,入队列,按队列的存储顺序访问相应表头的所有孩子结点,相当于树的层次遍历。

void bfs(int v,int visit[10])//广度遍历
{
	visit[v]=1;//标记起始节点
	jd* w;
	queue<int> q;//声明对列
	q.push(v);//起始结点入队 
	
	while(!q.empty())
	{
		v=q.front();//取队头结点
		cout<<v;
		q.pop();//删除(更新)队头元素 
		w=m[v].firstnode;//访问v结点第一个孩子结点
		
		while(w!=NULL)//遍历与某一个元素所有有关系的结点 
		{
			if(!visit[w->id])//若此结点未被遍历过则入队 
			{
				q.push(w->id);//w->id结点入队
				visit[w->id]=1;//标记w->id结点
			}
			w=w->next;//访问w的兄弟结点
		}
	}
} 

图的连通性问题

思路: 如果一个图的所有结点都是连通的,那么for循环里的代码就只会执行一次,如果有多个连通分量,那么当一个连通分量的所有结点遍历完成后必然会进入for循环,然后继续遍历下一个连通分量,直到所有的结点都遍历完。

int travel(int n)
{
	int visit[10],sum=0;
	for(int j=1;j<10;j++)//初始化辅助数组为未访问状态
		visit[j]=0;
	for(int j=1;j<=n;j++)//连通性问题的核心
	{
		if(visit[j]==0)
		{
			bfs(j,visit);
			sum++; 
		}
	}
	return sum;
}

完整代码

#include <iostream>
#include <malloc.h>
#include <queue>
using namespace std;
typedef struct node//结点 
{
	int id;
	struct node* next;
}jd;

typedef struct head//表头 
{
	char data;
	struct node* firstnode;
}td;

td m[10];

int find(int n,char v) 
{
	for(int i=1;i<=n;i++)
		if(m[i].data==v)
			return i;
	return -1;
}

void creat(int n1,int n2)//生成邻接表 
{
	char x,y;
	int z,n;
	jd* p;
	getchar();
	for(int i=0;i<n2;i++)
	{
		cin>>x>>y;
		getchar();
		z=find(n1,x);
		n=find(n1,y);
		
		p=(jd*)malloc(sizeof(jd));
		p->id=z;
		p->next=m[n].firstnode;//头插 
		m[n].firstnode=p;
		
		p=(jd*)malloc(sizeof(jd));
		p->id=n;
		p->next=m[z].firstnode;
		m[z].firstnode=p;
	}
}

void dfs(int v,int visit[10])//深度遍历 
{
	visit[v]=1;
	jd* w;
	w=m[v].firstnode;
	cout<<v;
	while(w!=NULL)//解决某结点分叉 
	{
		if(!visit[w->id])
			dfs(w->id,visit);
		w=w->next;
	}
}

void bfs(int v,int visit[10])
{
	visit[v]=1;
	jd* w;
	queue<int> q;
	q.push(v);//起始结点入队 
	while(!q.empty())
	{
		v=q.front();
		cout<<v;
		q.pop();//删除队头元素 
		w=m[v].firstnode;
		while(w!=NULL)//遍历与某一个元素所有有关系的结点 
		{
			if(!visit[w->id])//若此结点未被遍历过则入队 
			{
				q.push(w->id);
				visit[w->id]=1;
			}
			w=w->next;
		}
	}
} 

int travel(int n)
{
	int visit[10],sum=0;
	for(int j=1;j<10;j++)
		visit[j]=0;
	for(int j=1;j<=n;j++)
	{
		if(visit[j]==0)
		{
			bfs(j,visit);
			sum++; 
		}
	}
	return sum;
}
int main()
{
	int n1,n2,visit[10]={0},s,e;
	char d;
	cin>>n1>>n2;
	for(int i=1;i<=n1;i++)//初始化表头 
	{
		cin>>m[i].data;
		m[i].firstnode=NULL;
	}
	creat(n1,n2);
	int m=travel(n1);
	cout<<m;
	bfs(1,visit);
 	return 0;
}

本篇博客讲的都是图最基本的概念和操作以及本人自己的一点理解,希望这篇博客能够帮助到屏幕前的小伙伴!!!我是孤城浪人,一名正在前端路上摸爬滚打的菜鸟,欢迎你的关注。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值