【数据结构】——图的概念和存储

一、图的概念

一、图的表示:

图G由俩个集合V和E组成,即为G=(V,E),其中V是顶点的有限非空集合,E是V中顶点偶对的有限集,这些顶点偶对称为边(或弧)。

二、图的分类:

(1)根据边有无方向有向图无向图

有向图: 图中所有的边都是有向边(弧),有向边的起点称为弧尾,有向边的终点称为弧头。

 无向图:图中所有的边都是无向边。注意:一条无向边 相当于 俩条方向相反的有向边。

(2)根据边是否带权值带权图无权图 

 带权图:图中的边都附有一个权值(可为整数,实数,负数等)。注意:边的权值都相等的图 相当于 无权图。

 无权图:图中的边不附有一个权值。

(3)根据图中是否带自边或重边:简单图非简单图  

简单图:图中不存在自边(自己到自己的边)和重边(重复的边)。 

非简单图:图中存在自边或重边。如下图,B顶点存在自边,顶点A到顶点C存在重边。

(4)根据图中是否存在环(回路)带环图无环图  

带环图: 图中存在环(回路)。

无环图:图中不存在环(回路)。如下图,实际上不存在环,因为从顶点2出发都无法回到顶点2.

 (5)完全图

图中的每俩个顶点间都存在一条边。完全有向图:存在n*(n-1)条边;完全无向图:存在n*(n-1)/2条边。 

 (6)顶点数目固定,根据边的数目分为:稠密图稀疏图 

 稠密图:当图接近于完全图或通常认为图中边数大于n*log(n) (n为顶点数)的图。

 稀疏图:当图含较少的边或通常认为图中边数小于n*log(n) 的图。

三、图的概念:

(1)端点和邻接点:构成一条边的俩个顶点为端点,他们俩个互为邻接点。
(2)子图: 

存在俩个图G=(V,E)和G'=(V',E'),若V'是V的子集,E'是E的子集,且E'是V'中的顶点偶对的有限集,则G'是G的子图。 

(3)度:

 无向图中:顶点的度就是一个顶点所连的边数。

有向图中:分为顶点的出度入度:出度为以该顶点为弧尾的边的数目;入度为以该顶点为弧头的边的数目。顶点的度为出度+入度之和。

有向图还是无向图,图中所有的顶点的度之和一定为偶数。有n条边,度之和为2n。

(4)路径和路径长度:

路径:在图G中,从顶点i到顶点j的一条路径即为途中经过的顶点序列(i,......,j)。

路劲长度:在无权图中,路径长度为起点到终点所经过的边的数目;在带权图中,路径长度为起点到终点经过的边的权值之和。 

(5)环/回路:

若一条路径从起点出发,经过若干点,最终回到起点,则这条路径称为环(回路)。若一条环中只存在起点和终点是相同的顶点,则这条环称为简单环或简单回路。

特殊的回路:

欧拉环路:经过图中的各个边一次仅且一次的环,其长度为图中的边的总数。 

哈密尔顿环路:经过图中的所有顶点一次仅且一次的环,其长度为构成环的边的数目。

(6)连通、连通图、连通分量、生成树(均为无向图中存在): 

连通:若顶点x与y之间存在可相互抵达的路径(无论是直接或间接),则称为连通。

连通图:图中任意俩个顶点都连通的图。

连通分量即极大联通子图。对于连通图来说,只存在一个极大连通子图(连通分量)即它本身;对于非连通图来说,存在多个极大联通子图(即多个连通分量),每个连通分量都是一个连通图。

生成树即极小连通子图。即在无向连通图中,选n-1条边使得图中的全部n个顶点连通。

(7)强连通、强连通图、强连通分量(均为有向图中存在): 

强连通:若顶点x与y之间存在可项目抵达的路径(无论是直接或间接),则称为强连通。

强连通图:图中任意俩个顶点都强连通的图。

强连通分量即极大强连通子图。对于强连通图来说,只存在一个极大强连通子图(强连通分量)即它本身;对于非强连通图来说,存在多个极大强连通子图(即多个强连通分量),每个强连通分量都是一个强连通图。

二、图的存储

图有五种存储方式:边集数组,邻接矩阵,邻接表,十字邻接表,多重邻接表。其中十字邻接表只针对有向图,多重邻接表只针对无向图。而一般常用的是邻接矩阵和邻接表。

(1)邻接矩阵:

邻接矩阵中,存顶点采用一维数组,同时使下标相当于顶点的编号;存边采用二维数组,数组中的下标就是每个点的编号,如[i][j]意为编号为i的顶点到编号为j的顶点的边。

//用邻接矩阵存储无向带权图,则邻接矩阵存储的值可以是权值
#include<iostream>
#include <algorithm>
const int inf = 100001;//认为权值不超过10000,设置一个大点的inf用于初始化e数组,表示开始图中无边,而存储的权值代表一定有边
char v[105];//这里把顶点设置的少一点
int e[105][105];

int n, m;//顶点个数和边的个数

int find(char x);

int main()
{
	std::fill(&e[0][0], &e[0][0] + 105 * 105, inf);
	std::cin >> n >> m;
	getchar();
	for (int i = 1; i <= n; ++i)
	{
		std::cin >> v[i]; //存储n个顶点
	}
	char x, y; int w,xi, yi;
	for (int i = 1; i <= m; ++i)
	{
		getchar();
		std::cin >> x >> y >> w; //设置边和权值
		
		xi = find(x);//查找顶点的编号
		yi = find(y);
		//一条无向边 相当于 俩条方向相反的有向边
		e[xi][yi] = w;
		e[yi][xi] = w;
	}

	//试图寻找一个无向图顶点的度
	getchar();
	std::cin >> x;
	xi = find(x);
	int d = 0;//度
	//对于无向图临界矩阵还有个性质就是满足对称性
	for (int i = 1; i <= n; ++i)
	{
		/*if (e[xi][i] != inf)  //可以按列来找
			d++;*/
		if (e[i][xi] != inf)  //也可以按行来找
			d++;
	}
	std::cout << d << std::endl;

	return 0;
}

int find(char x)
{
	int i;
	for (i = 1; i <= n; ++i)
	{
		if (v[i] == x)break;
	}
	return i;
}

 邻接矩阵的缺点就是不适合存稀疏图,毕竟它开的二维空间很大,如果边少,造成了浪费。

(2)邻接表:

邻接表存图的顶点采用结构体数组,数组的下标也认为是顶点编号。存边采用的是链表,即链表中存储的是以同一个顶点为弧尾到达的不同的终点的编号。

//邻接表存无向带权图
#include<iostream>
typedef struct ENode {
	int adj;//终点编号
	int w;//此边的权值
	ENode* next;
}ENode;

struct Graph {
	char data;//顶点数据
	ENode* first;//以该顶点为弧尾的出边的链表
}g[105];

int n, m;

int find(char x);

int main()
{
	std::cin >> n >> m;
	getchar();
	for (int i = 1; i <= n; ++i)
	{
		std::cin >> g[i].data;
		g[i].first = NULL;
	}
	char x, y; int w, xi, yi;
	for (int i = 1; i <= m; ++i)
	{
		getchar();
		std::cin >> x >> y >> w;
		xi = find(x);//查找顶点编号
		yi = find(y);

		//求x------》y的出边链表
		ENode* e = new ENode;
		e->w = w;
		e->adj = yi;
		e->next = g[xi].first;
		g[xi].first = e; //采用的是头插法

		//求y-------》x的出边链表
		e = new ENode;
		e->w = w;
		e->adj = xi;
		e->next = g[yi].first;
		g[yi].first = e;
	}
	//求无向带权图的度
	getchar();
	std::cin >> x;
	xi = find(x);
	ENode* p = g[xi].first;
	int d = 0;
	while (p != NULL)
	{
		d++;
		p = p->next;
	}
	std::cout << d << std::endl;

	return 0;
}

int find(char x)
{
	int i;
	for (i = 1; i <= n; ++i)
	{
		if (g[i].data == x)
			break;
	}
	return i;
}

(3)十字邻接表:只适合有向图

 由于邻接表存储有向图,必须要建一个出边链表(邻接表)和入边链表(逆邻接表)。而当存储一个边时,会使得这组数据信息被重复存储俩次(比如A--->B 存储的是a的出边链表和b的入边链表,而当B--->A存储的是b的出边链表和a的入边链表)。此时十字邻接表就是把邻接表和逆邻接表结合在一次,因此十字邻接表只适合有向图中。

//十字链表:只针对有向图。是对有向图存俩个链表的优化。
//即一条边只用一个结点表示,而这个结点插入到俩条链表中,这俩条链表类似相交构成十字链表。
//十字链表结点结构中有5个元素:起点(弧尾)tail,tnext指针(出边链表),终点(弧头)head,hnext(入边链表),w(权值)

//这里存储的是有向无权图
#include<iostream>
typedef struct ENode {
	int tail_i;
	int head_i;
	struct ENode* tnext; //同弧尾的下一条边,也就是tail的下一条出边
	struct ENode* hnext;//同弧头的下一条边,也就是head的下一条边
}ENode;

struct Graph {
	char data;
	ENode* firstOut;//出边链表
	ENode* firstIn;//入边链表
}g[105];

int n, m;

int find(char x)
{
	int i;
	for (i = 1; i <= n; ++i)
	{
		if (g[i].data == x)
			break;
	}
	return i;
}

int main()
{
	std::cin >> n >> m;
	getchar();
	for (int i = 1; i <= n; ++i)
	{
		std::cin >> g[i].data;
		g[i].firstIn = g[i].firstOut = NULL;//开始默认无边
	}
	char start, end;
	for (int i = 1; i <= m; ++i)
	{
		getchar();
		std::cin >> start >> end;

		//开始插入strat的出边链表和end的入边链表
		int xi = find(start);
		int yi = find(end);
		ENode* e = new ENode;
		e->tail_i = xi;//adj记录的是出边链表的终点
		e->head_i = yi;//stx记录的是入边链表的起点
		e->tnext = g[xi].firstOut; //先将出边结点指针 指向 起点顶点的出边链表指针
		g[xi].firstOut = e; //再将起点顶点的出边链表指针 指向 出边结点

		e->hnext = g[yi].firstIn; //先将入边结点指针 指向 终点顶点的入边链表指针
		g[yi].firstIn = e; //再将终点入边指针 指向 入边结点
	}
	int d1 = 0, d2 = 0;
	getchar();
	std::cin >> start;
	int xi = find(start);
	ENode *e = g[xi].firstOut;
	while (e != NULL)
	{
		d1++;
		e = e->tnext;
	}
	e = g[xi].firstIn;
	while (e != NULL)
	{
		d2++;
		e = e->hnext;
	}
	std::cout << "该顶点的出度为:" << d1 << " ; " << "入度为:" << d2 << std::endl;

	return 0;
}

(4)多重邻接表:只适合无向图

 多重邻接表与十字邻接表相反,它是对无向图的邻接表的优化,也是邻接表存无向图时数组重复存储俩次,并且多重邻接表的结构和十字邻接表的结构相似。

//多重邻接表-----对无向图邻接表的优化,结构类似十字链表

#include<iostream>
typedef struct ENode {
	int x_i;//端点x
	int y_i;//端点y
	struct ENode* xnext;//以端点x为起点的下一条边
	struct ENode* ynext;//以端点y为起点的下一条边
}ENode;

struct graph {
	char data;
	ENode* first;
}g[105];

int n, m;

int find(char x)
{
	int i;
	for (i = 1; i <= n; ++i)
	{
		if (g[i].data == x)
			break;
	}
	return i;
}

int main()
{
	std::cin >> n >> m;
	getchar();
	for (int i = 1; i <= n; ++i)
	{
		std::cin >> g[i].data;
		g[i].first = NULL;
	}
	char u, v; int ui, vi;
	for (int i = 1; i <= m; ++i)
	{
		getchar();
		std::cin >> u >> v;
		ui = find(u);
		vi = find(v);

		//建立以u为起点开始的出边
		ENode* e = new ENode;
		e->x_i = ui;
		e->xnext = g[ui].first;//xnext对应xi
		g[ui].first = e;

		//再连接以v为起点开始的出边
		e->y_i = vi;
		e->ynext = g[vi].first;//ynext对应yi
		g[vi].first = e;
	}
	getchar();
	std::cin >> u;
	ui = find(u);
	int d = 0;
	ENode* p = g[ui].first;
	while (p != NULL)
	{
		d++;
		if (p->x_i == ui) //需要判断下一个结点的那个端点编号等于我们需要的顶点编号
		{
			p = p->xnext;
		}
		else
		{
			p = p->ynext;
		}
	}
	std::cout << d << std::endl;
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值