图——基本概念与存储结构

基本术语

1.弧

<v,w>  v为弧尾,w为弧头,v->w

2.无向图,有向图

定义上看:

无向图:有弧<v,w>∈VR,必有<w,v>∈VR(VR为两个顶点之间关系的集合)。

有向图:有弧<v,w>∈VR,不一定有有<w,v>∈VR(VR为两个顶点之间关系的集合)。

邻接矩阵数值上看:

无向图:矩阵为对称矩阵(即a[i,j]=a[j,i])。

有向图:a[i,j]为有限值时(存在),a[j,i]不一定为有限值,可能设为无穷大代表不连通(不存在)。

边的数目上看:

3.完全图,稀疏图,稠密图

看边的数目而定,分别对应边的数目为(n(n-1)/2;<nlogn;>nlogn)

4.权,网

图的边的与它相关的数;带权的图称为网

5.度、入度、出度、

度:与顶点相关联的边的个数

有向图

入度:以该顶点为头的边的数目

出度:以该顶点为尾的边的数目

6.简单路径

顶点不重复出现的路径

7.连通,连通图,连通分量

连通:两个节点之间有路径

连通图:任意两个节点之间都有路径

8.连通图的生成树

含有图中所有的节点和能够构成一棵树的n-1条边

如果边数<n-1,则非连通;如果边数>n-1,则必然存在环

相关问题

路径问题

1.路径判断:如何判断图上两个节点之间是否存在路径?

2.最短路径:最短图上两个节点之间的最短路径?

环相关问题

3.环的判断:图上是否存在一个环?(在电路理论中,环的出现代表短路,所以会有检查)

4.一笔画问题:是否存在一个包含了没条边一次的环,即遍历完所有的边且没有重复

5.哈密顿路径:是否存在一个只用了每个节点一次的环?

节点相关问题

6.节点问题:是否存在一个可以连接所有节点的方法

7.是否图上的每两个节点之间都相通?

8.最小生成树问题:通过最短的边的集合连接所有的节点

连通关系的问题

9.是否存在一个节点,移除它会使得整个图不相通

10.能否在平面中画出这个图,且所有边不相交?

图的应用

图可以应用的范围很广,例如互联网,社交网络,生物工程中蛋白质之间的相互作用,科学机构之间的联系等等,所以要善于看到实际问题背后的数学模型,从而抽象出来,解决问题

图的实现与存储结构

1.顺序映象(X)

单个数组顺序存储的方式无法体现出图多对多的数据结构关系。

改进:二维邻接矩阵实现

数据结构:

数组1:存储顶点的信息,用数组下标来代表顶点,记录顶点数

(主要是将具有实际意义的节点化为数字索引方便处理,比如char类型的节点“A”“B”,或者实际互联网中各个服务器的ip地址/名称都化成‘0’‘1’……)

数组2:邻接矩阵记录各顶点的边关系/权值

原理:

二维矩阵,V*V个点,其数据为0/1,代表是否有边相连(不考虑权值情况下,考虑权值可以直接放权值,不连通就放无穷),如v和w两个节点连通,则【v,w】和【w,v】都设为1。

同时需注意【v,w】代表的是从v节点到w节点的权值或者导通关系,v为弧尾(起点),w为弧头(终点)。

空间复杂度:O(n^2),不好

时间复杂度:O(n^2),如果需要检查有多少条边时,不好

#define INT_MAX 100000(设定无穷大的值)
#define char  datatype(设定实际意义的节点的数据类型,方便范式处理)
#define vetnum 10     (设定节点数目)

//定义节点数组和邻接矩阵
datatype vet[vetnum];
int edge[vetnum][vetnum]={0};

//客户端输入节点和边的数据以初始化
for(int i=0;i<vetnum;i++)
{
    scanf("%c",&datatype[vetnum]);
}

for(int i=0;i<vetnum;i++)
{
    for(int j=0;j<vetnum;j++)
    {
        scanf("%d",&edge[i][j]);
    }
}

2.链式映象(X)

每个节点的度不确定,所以对于链表的结构体定义来说需要按度数最大的来设置,但这样太浪费空间;如果不这么操作,则方便处理问题。

3.邻接表(最好)

核心思路:

构造一个结构体数组,数组中每个元素代表对应索引的节点,其又是一个链表的开头,该链表即记录着这个节点的所有邻接点,如图所示:

优点:

空间复杂度:O(e),尤其适合稀释图

易于找到所有的邻接点

缺点:

在判断任意两个节点之间是否存在边的时候,需要遍历第i个链表,复杂度比邻接矩阵高

具体代码实现:

范式思维(API接口):

定义顶点和边权类型,使得各种类型的数据更容易兼容

typedef char VertexType;//顶点类型由用户定义
typedef int EdgeType;//边上的权值类型由用户定义

顶点信息的存储:

一维数组/单链表

typedef struct VertexNode
{
	VertexType val;//顶点信息,即下标(0~N-1)
	ENode* first_edge;//指针域,指向第一个邻接点
}VNode;

边信息的存储:

单链表

typedef struct EdgeNode
{
	int v_id;//邻接点的下标
	struct EdgeNode* next_edge;//指针,指向下一个邻接点
}ENode;

图信息的存储:
typedef struct Graph
{
	int vexnum;       //顶点数
	int edgenum;      //边数
	VNode vertexs[MAXVER];
}Graph;

度的获得:

无向图:

某顶点的度即是其邻接点组成的单链表中节点的数目

有向图:

某顶点的出度即是其邻接点组成的单链表中节点的数目

可建立有向图的逆邻接表(即对每个顶点Vi都建立一个以Vi为弧头的表)

变式:增加权

边表定义中增加一个weight数据域,存储权的信息

代码实现构建图:

oj题目描述:给出顶点信息和二维数组描述各边,要求构建图(在oj基本量的定义限制下应该是本人完成的最好的版本了)

void addEdge(Graph* G, int from, int to)
{
	if (G->vertexs[from].first_edge->v_id == -1)
	{
		G->vertexs[from].first_edge->v_id = to;
	}
	else
	{
		ENode* trace = G->vertexs[from].first_edge;
		while (trace->next_edge != NULL)
		{
			trace = trace->next_edge;
		}
		ENode* add = (ENode*)malloc(sizeof(ENode));
		add->v_id = to;
		trace->next_edge = add;
		add->next_edge = NULL;
	}
}//文心一言建议添加边的时候可以写成函数封装,一来避免了写两遍的冗杂,二来避免了改参数时出现漏改导致bug的问题(本人就是因为这个修改了好几次)

void creatGraph(Graph* G, VertexType ver[], VertexType edge[][2])
{
	//顶点信息初始化
	for (int i = 0; i < MAXVER; i++)
	{
		G->vertexs[i].val = ver[i];
		ENode* p = (ENode*)malloc(sizeof(ENode));
		G->vertexs[i].first_edge = p;//这里给每个节点都设置了一个邻接点,但是如果最后发现有节点根本没有邻接点呢,那不就是浪费空间了吗?但是这里这样设置的话感觉就是可以一般化处理,不用讨论,更加方便,如果大规模问题,可以最后再释放空间
		G->vertexs[i].first_edge->v_id = -1;
		G->vertexs[i].first_edge->next_edge = NULL;
	}

	for (int i = 0; i < MAXEDG; i++)
	{
		int pos1 = 0, pos2 = 0;
		int flag1 = 0, flag2 = 0;
		for (int j = 0; j < MAXVER; j++)
		{
			if (ver[j] == edge[i][0])
			{
				pos1 = j;
				flag1 = 1;
			}
			else if (ver[j] == edge[i][1])
			{
				pos2 = j;
				flag2 = 1;
			}
			if (flag1 != 0 && flag2 != 0)
			{
				break;
			}
		}

		addEdge(G, pos1, pos2);
		addEdge(G, pos2, pos1);
	}
	G->vexnum = MAXVER;
	G->edgenum = MAXEDG;
}

改进版本(文心一言)

文心一言提到该版本可以优化,该版本拖累性能的主要是在添加邻接点时需要遍历链表,所以可以通过添加邻接点时添加在链表头部,然后更新first_edge,实现不用遍历就可以添加结点,但问题时如果要进行DFS/BFS这类对顺序有要求的遍历的话,输出的结果就会不一样,所以要看问题是否对遍历的顺序有要求

void add_edge(GraphAdjList *graph, int vertex1, int vertex2) {  
    // ... 省略了有效性检查和其他代码 ...  
  
    // 为vertex1添加vertex2作为邻接点  
    EdgeNode *newNode = (EdgeNode *)malloc(sizeof(EdgeNode));  
    newNode->adjvex = vertex2;  
    newNode->next = graph->adjList[vertex1].first_edge; // 将新节点指向原链表的头部  
    graph->adjList[vertex1].first_edge = newNode; // 更新顶点的first_edge为新节点  
  
    // 如果是无向图,还需要为vertex2添加vertex1作为邻接点  
    // ... 相应的代码 ...  
}

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值