基本术语
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作为邻接点
// ... 相应的代码 ...
}