图G(Graph)由顶点集V(Vertex)和边集E(Edge)组成,记为G=(V,E),V(G)表示图G中顶点的有限非空集:E(G)表示图G中顶点之间的关系(边)集合。若V={v1,v2,…vn},则 |V| 表示图G中顶点的个数,也称为图的阶,E={(u,v)|u∈V,v∈V},用|E|表示图G中边的条数。
注:线性表可以是空表,树可以是空树,但是图不能是空图。
顶点与边的关系:可以有顶点没有边,但是不能有边无顶点
图在现实生活中具有许多逻辑应用
铁路网,V:车站,E:铁路
好友关系,V:英雄,E:战友,队长…
知识关系:V:知识点,E:联系
若E是无向边(简称边)的有限集合时,则称图G为无向图。边是顶点的无序对,记作(v,w)或(w,v),对无向图来说(v,w)=(w,v),其中v,w为顶点,v,w也互为邻接点。边(v,w)与顶点w和v相关联。
若E是有向边(简称弧)的有限集合时,则称图G为有向图。弧是顶点的有序对,记作<v,w>,其中v,w为顶点,w称为弧头,v称为弧尾,<v,w>称为从顶点v到顶点w的弧。<v,w>≠<w,v>。
简单图的定义:
不存在重复的边。不存在顶点到自身的边。多重图的定义:
存在重复边存在顶点到自身的边对于无向图来说,顶点v的度是指与该顶点连接的边的条数,记作TD(v)。 通常称度为1的节点为悬挂点,与悬挂点关联的边为悬挂边。如下图的A和(A,D)
图的总度数=边的数量x2 ∑ i = 1 n T D ( v i ) = 2 ∣ E ∣ \sum_{i=1}^{n}TD(v_{i})=2|E| i=1∑nTD(vi)=2∣E∣
对于有向图来说,具有出度和入度。 入度是以顶点v为终点的有向边的数目,记作ID(v)。(即弧头(箭头)所指的顶点) 出度是以顶点v为起点的有向边的数目,记作OD(v)。(即弧尾所在的顶点,或理解为射线的起始位置) 有向图顶点的度指的是入度和出度之和。 T D ( v i ) = I D ( v i ) + O D ( v i ) TD(v_{i})=ID(v_{i})+OD(v_{i}) TD(vi)=ID(vi)+OD(vi) 对有向图来说,入度之和=出度之和=边的数量 ∑ i = 1 n I D ( v i ) = ∑ i = 1 n O D ( v i ) = ∣ E ∣ \sum_{i=1}^{n}ID(v_{i})=\sum_{i=1}^{n}OD(v_{i})=|E| i=1∑nID(vi)=i=1∑nOD(vi)=∣E∣
例图2
路径:顶点Vp到顶点Vq之间的路径,如A到E:(A,B,E),(A,B,D,E),注意:有向图路径也是有向的
回路:第一个顶点和最后一个顶点相同的路径称为回路(或称为环),如例图2中<A,B,E,C,A>
简单路径 :顶点不重复出现的路径即为简单路径,如例图1中的(A,D,C,E)
简单回路:除首尾顶点外,其他顶点不重复的回路。
路径长度:路径上边的数目,如例图1中(A,D,C,E)经过3条边,则路径长度为3。
点到点的距离:顶点U出发到顶点V的最短路径(若存在),则称为从U到V的距离,若不存在,则称为∞。
连通:无向图中,若顶点v到w的路径存在,则称v和w是连通的,反之为不连通的。
强连通:有向图中,若顶点v到w的路径存在,顶点w到v的路径也存在,称顶点v和w是强连通的。
连通图 若图中任意两个顶点都是连通的,则称图G为连通图,反之则为非连通图。
强连通图 若图中任何一对顶点都是强连通的,则称图G为强连通图。
性质 对于n个顶点的无向图G1:若G1是连通图,最少有n-1条边;若G1是非连通图,边数最多有 C n − 1 2 C_{n-1}^{2} Cn−12 对于n个顶点的有向图G2,若G2是强连通图,最少有n条边(形成回路)。
两个图中,图G1=(V1,E1)和G2=(V2,E2),若V2是V1的子集,E2是E1的子集,则称G2是G1的子图。
若子图G3中包含G1的所有顶点(即V(G1)=V(G3)),则称G3为G1的生成子图(对边无要求)。有向图亦是如此。
极大连通子图要求子图必须连通,并且包含尽可能多的顶点和边。
通俗理解:G2为大陆铁路网,G3为韩国铁路网,G4为日本铁路网
有向图的极大强连通子图称为强连通分量。连通图的生成树是包含图中全部顶点的一个极小连通子图(即边尽可能少)。 如果图的顶点数为n,则图的生成树则有n-1条边。对生成树来说,少一条边则会变成非连通图,加上一条边则会形成回路。
生成森林 在非连通图中,连通分量的生成树构成了非连通图的生成森林。无向图 - 连通分量 - 生成森林 将三个连通分量化为极小连通子图则为生成森林。
边的权:在图G中,每条边都可以标上具有某种含义的数值,该数值称为边的权值。 带权图:边上带有权值的图称为带权图,或网。
无向完全图:任意两个顶点都存在边,边的总数为 C n 2 C_{n}^{2} Cn2 图如下:
有向完全图:任意两个顶点之间都存在方向相反的两条弧。 边的总数为 2 C n 2 2C_{n}^{2} 2Cn2 图如下:
稀疏图:边数很少的图
稠密图
树:不存在回路,且连通的无向图。n个顶点的数,必有n-1条边。
有向树:一个顶点的入度为0,其余顶点都为1的有向图。
由例图可得图G1的邻接矩阵:
从中可知,若要求B的度,则将B的行(或列)相加,也即遍历行或者列就可以得到某个顶点的度。 注意,无向图邻接矩阵关于对角线对称。
第 i 个 节 点 的 度 = 第 i 行 ( 或 列 ) 的 非 零 元 素 个 数 . \ 第i个节点的度 = \ 第i行(或列)的非零元素个数\,. 第i个节点的度= 第i行(或列)的非零元素个数.
有向图(网) 在有向图的基础上,再加上每条边的权值便形成了带权图(或称网)由例图可得图G2的邻接矩阵:
对于网来说,不能到达的顶点距离为无穷。
以如下不带权的无向图G3为例,
设图G3的邻接矩阵为A3,则A3^n 的元素 :A3^n[i][j]=顶点 i 到顶点 j 的长度为 n 的路径数目。 例: A 2 = ( 0 1 0 0 1 0 1 1 0 1 0 1 0 1 1 0 ) ( 0 1 0 0 1 0 1 1 0 1 0 1 0 1 1 0 ) A_{}^{2}= \begin{pmatrix} 0_{}& 1_{}& 0_{}& 0_{}\\ 1_{}& 0_{}& 1_{}& 1_{}\\ 0_{}& 1_{}& 0_{}& 1_{}\\ 0_{}& 1_{}& 1_{}& 0_{} \end{pmatrix} \begin{pmatrix} 0_{}& 1_{}& 0_{}& 0_{}\\ 1_{}& 0_{}& 1_{}& 1_{}\\ 0_{}& 1_{}& 0_{}& 1_{}\\ 0_{}& 1_{}& 1_{}& 0_{} \end{pmatrix} A2=⎝⎜⎜⎛0100101101010110⎠⎟⎟⎞⎝⎜⎜⎛0100101101010110⎠⎟⎟⎞ 则有: A 2 [ 1 ] [ 4 ] = a 11 a 14 + a 12 a 24 + a 13 a 34 + a 14 a 44 = 1 A_{}^{2}[1][4]= a_{11}a_{14}+a_{12}a_{24}+a_{13}a_{34}+a_{14}a_{44}=1 A2[1][4]=a11a14+a12a24+a13a34+a14a44=1 a11a14代表从顶点A->A再从A->D的路径 a12a24代表从顶点A->B再从B->D的路径 以此类推,a13a34=A->C,C->D;a14a44=A->D,D->D 从而可知上式代表从A到D,路径长度为2仅有1条路。 A 3 [ 1 ] [ 4 ] : 求 从 A 到 D 长 度 为 3 的 路 径 有 几 条 , 需 3 个 矩 阵 相 乘 A_{}^{3}[1][4]: 求从A到D长度为3的路径有几条,需3个矩阵相乘 A3[1][4]:求从A到D长度为3的路径有几条,需3个矩阵相乘 注:邻接矩阵的方式适合用于存储稠密图,对于稀疏图来说,会造成极大的空间浪费。其空间复杂度为O(|V|^2)。
以2.1.1无向图G1为例,实现邻接矩阵代码如下:
#include <stdio.h> #include <stdlib.h> #define ZERO 0 #define MAXVECNUM 15 //最大顶点数 #define INFINITY 65535//无穷 //注意,指向自身的边有时候也会定义为无穷 typedef char VertexType;//顶点类型 typedef int EdgeType;//边上的权值,带权图使用, //邻接矩阵 Adjacency Matrix Graph typedef struct { VertexType Vex[MAXVECNUM];//顶点表 EdgeType Edge[MAXVECNUM][MAXVECNUM];//邻接矩阵 int vexNum, edgesNum;//图的顶点数,边(弧)的数目 }MGraph, * MG; //创建邻接矩阵 void CreateMGraph(MG G) { int i, j, k, w; printf("请输入顶点数目和边的数目:\n"); //scanf("%d%d", &G->vexNum, &G->edgesNum); G->vexNum = 6;//测试用数据 G->edgesNum = 7;//测试用数据 //遍历读取顶点,建立顶点表 printf("输入所有顶点:"); for (i = 0; i < G->vexNum; i++) { scanf("%s", &G->Vex[i]); } //邻接矩阵初始化 for (i = 0; i < G->vexNum; i++) { for (j = 0; j < G->vexNum; j++) { //G->Edge[i][j] = INFINITY;//网的初始化 G->Edge[i][j] = 0;//不带权无向图初始化 } } for (k = 0; k < G->edgesNum; k++) { printf("请输入边(vi,vj)的下标(i,j)和权重w\n");//无向图权值(w)默认为1; //scanf("%d%d", &i, &j); scanf("%d%d%d", &i, &j, &w); G->Edge[i][j] = 1; G->Edge[j][i] = G->Edge[i][j];//无向图的邻接矩阵关于对角线对称 } } /打印邻接矩阵 void printGraph(MG G) { int i, j, k; printf("\t"); for ( i = 0; i < G->vexNum; i++) { printf("%c\t", G->Vex[i]); } printf("\n"); for (i = 0; i < G->vexNum; i++) { for (j = i; j <= i; j++) { printf("%c\t",G->Vex[j]); for ( k = 0; k < G->vexNum; k++) { printf("%d\t", G->Edge[i][k]); } printf("\n"); } } } int main() { MG g = (MG)malloc(sizeof(MGraph)); CreateMGraph(g); printGraph(g); system("pause"); return 0; }运行结果如下:
邻接表法采用数组与链表结合的方式来实现图的数据结构。 邻接表较为适合存储稀疏图。 对于无向图,邻接表法整体空间复杂度为O(|V|+2|E|)。 对于有向图,整体空间复杂度为O(|V|+|E|)。
同样以无向图G1为例,实现代码如下:
#include <stdio.h> #include <stdlib.h> #define ZERO 0 #define MAXVECNUM 15 //最大顶点数 #define INFINITY 65535//无穷,注意,指向自身的边有时也会定义为无穷 //邻接表法 Adjacency List //弧表节点 typedef struct ArcNode { int adjVertex;//弧(边)指向哪个节点 struct ArcNode* pnext;//指向下一条弧(边) }ArcNode; //顶点节点 typedef struct VertexNode{ VertexType data;//顶点信息 ArcNode* firstEdge;//第一条弧(边) }VNode, AdjList[MAXVECNUM]; typedef struct { AdjList adj_list; int vexNum, arcNum;//图的节点数和弧数 }ALGraph; //函数1 建立邻接表 void CreateAdjListGraph(ALGraph *AG) { printf("请输入节点个数,边的个数:\n"); //scanf("%d%d", &AG->vexNum, &AG->arcNum); AG->vexNum = 6;//测试用 AG->arcNum = 7;//测试用 //建立顶点表 printf("请输入所有顶点:\n"); for (size_t i = 0; i < AG->vexNum; i++) { scanf("%s", &AG->adj_list[i].data); AG->adj_list[i].firstEdge = NULL; } //建立边表 int i, j; for (size_t m = 0; m < AG->arcNum; m++) { printf("请输入边(vi,vj)的顶点下标i,j\n"); scanf("%d%d", &i, &j); //建立顶点i到j的边 //1.申请边(弧)节点i并初始化 ArcNode *Ei = (ArcNode*)malloc(sizeof(ArcNode)); memset(Ei, 0, sizeof(ArcNode)); //2.定义邻接节点 Ei->adjVertex = j; //3.头插移动指针 if (NULL == AG->adj_list[i].firstEdge) { AG->adj_list[i].firstEdge = Ei; } else { Ei->pnext = AG->adj_list[i].firstEdge; AG->adj_list[i].firstEdge = Ei; } //申请节点j并初始化,下同 ArcNode* Ej = (ArcNode*)malloc(sizeof(ArcNode)); memset(Ej, 0, sizeof(ArcNode)); Ej->adjVertex = i;//对无向图来说,i,j互为各自的邻接节点 if (NULL == AG->adj_list[j].firstEdge) { AG->adj_list[j].firstEdge = Ej; } else { Ej->pnext = AG->adj_list[j].firstEdge; AG->adj_list[j].firstEdge = Ej; } } } //函数2 打印图的数据 void printALGraph(ALGraph *AG) { int i, j, k; printf("Data\tHead\n"); for (i = 0; i < AG->vexNum; i++) { for (j = i; j <= i; j++) { printf("%c\t", AG->adj_list[j]); int k = 0; ArcNode* Acur; Acur = AG->adj_list[i].firstEdge;//头节点 //遍历链表进行打印 while (k < AG->vexNum) { if (NULL == Acur) { printf("∞\t"); } else { printf("%d\t", Acur->adjVertex); Acur = Acur->pnext; } k++; } printf("\n"); } } } int main() { ALGraph G; CreateAdjListGraph(&G); printALGraph(&G); system("pause"); return 0; }运行结果如下:
十字链表用于存储有向图,有向网。 如图D1
对于十字链表来说,分为顶点结点与弧节点两部分。
顶点节点 datafirstinfirstoutfirstin:该顶点作为弧头的第一条弧。 firstout:该顶点作为弧尾的第一条弧。 如:
A<C,A><A,B> 弧节点 tailVexheadVexinfohlinktlinktailVex:弧尾顶点编号。 headVex:弧头顶点编号。 info:权值 hlink:弧头相同的下一条弧。 tlink:弧尾相同的下一条弧。
由此可推出图D1的十字链表如下图所示: 注:^表示NULL,空间复杂度:O(|V|+|E|)
以图D2测试,运行结果1: 以图D1为例,运行结果2:
邻接多重表常用于存储无向图。
提示:这里可以添加要学的
后续逐步更新