图的概念与五种结构实现 (详细定义与源码实现)

,图的术语多的让人绝望,但我们要沉下心来慢慢琢磨透它.
首先我们来看一下图的定义:

图是由顶点的有穷非空集合和顶点之间边的集合组成,
通常表示为: G (V,E) ,其中,G表示为一个图,V是图G中顶点的集合
E是图G中边的集合.

其次,我们知道线性表中数据元素叫元素,树中将数据元素叫结点,而图中数据元素
我们则称之为顶点.线性表中可以没有数据,称为空表.树中没有结点,叫做空树.而对于图来说,则强调了顶点集合V有穷非空.但边集可以为空.

接下来我们来认识一些图的种类:

无向边: 若定点Vi到Vj之间没有方向,则称这条边为无向边,用无序偶对(Vi,Vj)来表示.
无向图: 若图中任意两个顶点之间的边都是无方边,则称改图为无向图.
无向完全图: 在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图.
有向边: 若从顶点Vi到Vj的边有方向,则称这条边为有向边,也称为弧.用有序偶对<Vi,Vj>来表示,Vi表示弧尾,Vj表示弧头.
有向图: 若图中任意两个顶点之间的边都是有向边,则称该图为有向图.
有向完全图: 在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称为有向完全图.
简单图: 在图中,若不存在顶点到其自身的边,且同一条边不重复出现,这样的图为简单图.
稀疏图: 图中边或弧很少
稠密图: 图中边或弧很多.
权: 与图的边或弧相关的数叫做权
网: 带权的图叫做网

我们刚了解一些图的分类,让我们继续了解顶点与边的关系,为下面的代码实现打下理论基础.

对于无向图G=(V,{E}) 如果边(v,v’)属于E,则称顶点v和v’互为邻接点,即v和v’相邻接.
边(v,v’)依附于顶点v和v’ 或者(v,v’)与顶点v,v’相关联.
顶点的度是和v相关联的边的数目,记为TD(v)

对于有向图,以顶点v为头的弧的数目称为v的入度,记为ID(v)
以v为尾的弧的数目称为v的出度 记为OD(v)
顶点v的度为TD(v)=ID(v)+OD(v).
边=出度=入度

无向图G 中顶点v 到顶点v’的路径 是一个顶点序列.路径的长度是路径上的边或弧的数目
第一个顶点到最后一个顶点相同的路径称为回路或环.
序列中顶点不重复的路径称为简单路径.
除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环

连通图的相关术语:

在无向图G中,如果从顶点v到顶点v’有路径,则称v和v’是联通的.
对于图中任意两个顶点V1,V2属于E v1和v2都是联通的,则称为G是联通图.

无向图中的极大联通子图称为连通分量
1.子图 2 连通 3,含有极大顶点数的连通子图包含依附于这些顶点的所有边.
在有向图G中,如果对于每一对v,v’ 从存在相反的路径,则称G是强联通图
有向图的极大连通子图称做有向图的强连通分量.

接下来我们来看看联通图的生成树定义

一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有 足以构成一棵树的n-1条边.
如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1则是一颗有向树.

到此为止我们关于图的一些基本概念都已经讲完了,接下来我们来用代码来实现图.

一.邻接矩阵

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

定义如下:在这里插入图片描述
有了这个矩阵,我们就能知道:
**1.判定任意两点是否有边无边很容易.
2.要知道某个顶点的度,其实就是这个顶点Vi在邻接矩阵的第i行的元素之和.
3.求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点.

接下来我们来看有向图的邻接矩阵
在这里插入图片描述

对于有向图的邻接矩阵我们可以知道
1.顶点vi的入度为vi列各数之和,顶带你vi的出度为vi行的各数之和.
2.判断顶点vi到vj之间是否有弧,只需查找矩阵中arc[i][j]是否为1即可

我们在上面提到了网,我们来继续讨论网的实现

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

#define MAXPOINT 100   //最大顶点数
#define MAX     1000000    //表示无穷
typedef struct Graph
{
	int  visited[MAXPOINT];    //下标对应的顶点是否被访问,在图的遍历中会用到. 
	int point[MAXPOINT];       //顶点数组
	int arc[MAXPOINT][MAXPOINT];  //邻接数组
	int numPoint, numEdge;        //顶点数目, 边的数目
}Graph;

有了这个结构定义,我们构造一个图,其实就是给顶点边和边表输入数据的过程.我们来看看无向网图的创建代码.

void CreateGraph(Graph *G)
{
	int i, j, k,w;
	printf("请输入顶点数和边数:\n");
	scanf("%d %d", &G->numPoint, &G->numEdge);   //输入顶点数目和边数
	printf("请输入各个顶点的信息:\n");
	for (i = 0; i < G->numPoint; i++)
	{
		scanf("%d", &G->point[i]);   //输入每个顶点的信息.
	}
	for (i = 0; i < G->numPoint; i++)     //邻接矩阵初始化
	{
		for (j = 0; j < G->numPoint; j++)
		{
			if (i == j)
				G->arc[i][j] = 0;
			else
				G->arc[i][j] = MAX;

		}
	}
	for (k = 0; k < G->numEdge; k++)
	{
		printf("请输入边(Vi,Vj)的下标i,下标j和边的权重w:\n");   
		scanf("%d %d %d", &i, &j, &w);
		G->arc[i][j] = w;
		G->arc[j][i] = w;    
	}
}

再来个测试函数

void print(Graph *G)
{
	int i,j;
	for (i = 0; i < G->numPoint; i++)
	{
		printf("%-9d", G->point[i]);
	}
	printf("\n");
	for (i = 0; i < G->numPoint; i++)
	{
		for (j = 0; j < G->numPoint; j++)
		{
			printf("%-9d", G->arc[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	Graph G;
	CreateGraph(&G);
	print(&G);

	system("pause");
	return 0;

}

接下来我们来谈图的另外一种存储结构------邻接表

我们用邻接矩阵存储图很方便,但对于边数相对于顶点较少的图,这种结构是存在对存储空间的极大浪费,所以我们引用链式存储结构.我们把这种数组与链表相结合的存储方法称为邻接表
下图就为邻接表
链表叫为边表. 有向图则称为顶点vi作为弧尾的出边表.
在这里插入图片描述
这样的结构我们查找图地相关信息很方便.
比如,我们想知道某个顶点的度,就去查找这个顶点的边表中结点的个数.
若要判断顶点vi到vj是否存在边,只需要测试顶点vi的边表中adjvex是否存在结点vj的下标j就行.
若求顶点的所有邻接点,其实就是对此顶点的边表进行遍历.

我们还可以建立一个有向图的逆邻接表,即对每个顶点vi都建立一个链接为vi为弧头的表
在这里插入图片描述
可以很容易的算出某个顶点的入度或出度是多少,判断两顶点是否纯在弧也很容易实现.

接下来我们来实现-----邻接表
下面代码是无向图的邻接表的实现

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#define MAXPOINT 100
typedef struct EdgeNdoe   //边表结点
{ 
	int index;         //邻接点的下标.
	int weight;       //边的权值.
	struct EdgeNode * next;   //链域.指向下一个邻接点
}EdgeNdoe;

typedef struct Node   //顶点表结点
{
	int data;  //顶点信息
	EdgeNdoe * firstedge;   //边表头指针
}Node;
typedef struct Graph
{
	Node arr[MAXPOINT];
	int numPonit, numEdge;    //顶点数目,边的数目.
	int visited[MAXPOINT];     //顶点标记数组,图的遍历时有用
}Graph;
//创建邻接表
void CreateGraph(Graph *G)
{
	int i, j, k,w;
	EdgeNdoe * e;
	printf("请输入顶点的和边的数目:\n");
	scanf("%d %d", &G->numPonit, &G->numEdge);
	printf("请输入各个顶点的信息:\n");
	for(i=0;i<G->numPonit;i++)
	{
		scanf("%d", &G->arr[i].data);
		G->arr[i].firstedge = NULL;
	}
	for (k = 0; k < G->numEdge; k++)
	{
		printf("请输入(Vi,Vj)的Vi,Vj的下标和权值W:\n");
		scanf("%d %d %d", &i, &j, &w);
		e = (EdgeNdoe*)malloc(sizeof(EdgeNdoe));
		e->weight = w;
		e->index = j;
		e->next = G->arr[i].firstedge;
		G->arr[i].firstedge = e;


		e = (EdgeNdoe*)malloc(sizeof(EdgeNdoe));
		e->weight = w;
		e->index = i;
		e->next = G->arr[j].firstedge;
		G->arr[j].firstedge = e;
	}
}

到此为止,我们已经讲了图的两种最常见的存储结构.下面我们再来给出图的另外三种存储方式

十字链表
把邻接表和逆邻接表结合起来,就是十字链表.
我们重新定义顶点表的结构和边表的结构
下面的图能够很好的说明.
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
邻接多重表

讲了有向图的优化存储结构,对于无向图的邻接表,有没有问题呢?
当然是有的,如果我们压迫删除某一条边,那就意味着,需要找到这条边额两个边表结点进行操作,显然是非常繁琐的.
因此我们也仿照十字链表的方式,对边表结点的结构进行一些改造,

在这里插入图片描述

最后一种是边集数组

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

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值