数据结构学习笔记11——图

1.图的认知

1.1图的定义

图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中边的条数。

注:线性表可以是空表,树可以是空树,但是图不能是空图。

图的定义
顶点与边的关系:可以有顶点没有边,但是不能有边无顶点
顶点与边的关系

1.2图的逻辑结构应用

图在现实生活中具有许多逻辑应用

  • 铁路网,V:车站,E:铁路
    铁路网

  • 好友关系,V:英雄,E:战友,队长…
    好友关系

  • 知识关系:V:知识点,E:联系
    知识点图

1.3无向图与有向图

1.3.1无向图

若E是无向边(简称边)的有限集合时,则称图G为无向图。边是顶点的无序对,记作(v,w)或(w,v),对无向图来说(v,w)=(w,v),其中v,w为顶点,v,w也互为邻接点。边(v,w)与顶点w和v相关联。

无向图

1.3.2有向图

若E是有向边(简称弧)的有限集合时,则称图G为有向图。弧是顶点的有序对,记作<v,w>,其中v,w为顶点,w称为弧头,v称为弧尾,<v,w>称为从顶点v到顶点w的弧。<v,w>≠<w,v>
有向图

1.4简单图与多重图

1.4.1简单图

简单图的定义:

  1. 不存在重复的边。
  2. 不存在顶点到自身的边。

简单无向图

1.4.2多重图

多重图的定义:

  1. 存在重复边
  2. 存在顶点到自身的边
    多重图

1.5度

1.5.1无向图的度

对于无向图来说,顶点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=1nTD(vi)=2E
无向图的度d

1.5.2有向图的度

对于有向图来说,具有出度和入度。
入度是以顶点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=1nID(vi)=i=1nOD(vi)=E
有向图的度

1.6 顶点之间的关系

  • 例图1

路径
例图2
在这里插入图片描述

  1. 路径:顶点Vp到顶点Vq之间的路径,如A到E:(A,B,E),(A,B,D,E),注意:有向图路径也是有向的

  2. 回路:第一个顶点和最后一个顶点相同的路径称为回路(或称为环),如例图2中<A,B,E,C,A>

  3. 简单路径 :顶点不重复出现的路径即为简单路径,如例图1中的(A,D,C,E)

  4. 简单回路:除首尾顶点外,其他顶点不重复的回路。

  5. 路径长度:路径上边的数目,如例图1中(A,D,C,E)经过3条边,则路径长度为3。

  6. 点到点的距离:顶点U出发到顶点V的最短路径(若存在),则称为从U到V的距离,若不存在,则称为

  7. 连通:无向图中,若顶点v到w的路径存在,则称v和w是连通的,反之为不连通的。

  8. 强连通:有向图中,若顶点v到w的路径存在,顶点w到v的路径也存在,称顶点v和w是强连通的。

1.7连通图与强连通图

  • 连通图
    若图中任意两个顶点都是连通的,则称图G为连通图,反之则为非连通图。
    连通图

  • 强连通图
    若图中任何一对顶点都是强连通的,则称图G为强连通图。
    强连通图

  • 性质
    对于n个顶点的无向图G1:若G1是连通图,最少有n-1条边;若G1是非连通图,边数最多有 C n − 1 2 C_{n-1}^{2} Cn12
    对于n个顶点的有向图G2,若G2是强连通图,最少有n条边(形成回路)。

1.8子图

图与子图
两个图中,图G1=(V1,E1)和G2=(V2,E2),若V2是V1的子集,E2是E1的子集,则称G2是G1的子图

子图
若子图G3中包含G1的所有顶点(即V(G1)=V(G3)),则称G3为G1的生成子图(对边无要求)。有向图亦是如此。

1.9连通分量

  • 无向图中的极大连通子图称为连通分量

无向图G1

极大连通子图要求子图必须连通,并且包含尽可能多的顶点和边。

连通分量
通俗理解:G2为大陆铁路网,G3为韩国铁路网,G4为日本铁路网

  • 有向图的极大强连通子图称为强连通分量

强连通图

强连通分量

1.10生成树与生成森林

  • 生成树

连通图的生成树是包含图中全部顶点的一个极小连通子图(即边尽可能少)。
如果图的顶点数为n,则图的生成树则有n-1条边。对生成树来说,少一条边则会变成非连通图,加上一条边则会形成回路。
生成树

  • 生成森林
    在非连通图中,连通分量的生成树构成了非连通图的生成森林。

无向图 - 连通分量 - 生成森林
无向图->连通分量
将三个连通分量化为极小连通子图则为生成森林。
生成森林

1.11带权图(网)

边的权:在图G中,每条边都可以标上具有某种含义的数值,该数值称为边的权值。
带权图:边上带有权值的图称为带权图,或网。
带权图

1.12其他特殊图

  1. 无向完全图:任意两个顶点都存在边,边的总数为
    C n 2 C_{n}^{2} Cn2
    图如下:
    无向完全图

  2. 有向完全图:任意两个顶点之间都存在方向相反的两条弧。
    边的总数为
    2 C n 2 2C_{n}^{2} 2Cn2
    图如下:
    有向完全图

  3. 稀疏图:边数很少的图
    稀疏图

  4. 稠密图
    稠密图

  5. 树:不存在回路,且连通的无向图。n个顶点的数,必有n-1条边。
    树

  6. 有向树:一个顶点的入度为0,其余顶点都为1的有向图。
    有向树

2图的存储

2.1邻接矩阵法

2.1.1邻接矩阵

  • 无向图

G1

由例图可得图G1的邻接矩阵:
邻接矩阵

从中可知,若要求B的度,则将B的行(或列)相加,也即遍历行或者列就可以得到某个顶点的度。
注意,无向图邻接矩阵关于对角线对称。

  第 i 个 节 点 的 度 =   第 i 行 ( 或 列 ) 的 非 零 元 素 个 数   . \ 第i个节点的度 = \ 第i行(或列)的非零元素个数\,.  i= i().

  • 有向图(网)
    在有向图的基础上,再加上每条边的权值便形成了带权图(或称网)

由例图可得图G2的邻接矩阵:

网的邻接矩阵
对于网来说,不能到达的顶点距离为无穷。

2.1.2 邻接矩阵的性质

以如下不带权的无向图G3为例,

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=01001011010101100100101101010110
则有:
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]AD33
注:邻接矩阵的方式适合用于存储稠密图,对于稀疏图来说,会造成极大的空间浪费。其空间复杂度为O(|V|^2)。

2.1.3邻接矩阵代码实现

以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;
}

运行结果如下:
运行结果

2.2邻接表法

2.2.1理解邻接表

  • 无向图
    仍以无向图G1为例,则获得邻接表如下:
    无向图G1邻接表
  • 有向图
    有向图邻接表

邻接表法采用数组与链表结合的方式来实现图的数据结构。
邻接表较为适合存储稀疏图。
对于无向图,邻接表法整体空间复杂度为O(|V|+2|E|)。
对于有向图,整体空间复杂度为O(|V|+|E|)。

2.2.2 邻接表代码实现

同样以无向图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;
}

运行结果如下:
邻接表运行结果

2.3十字链表

2.3.1理解十字链表

十字链表用于存储有向图,有向网。
如图D1

图D1
对于十字链表来说,分为顶点结点弧节点两部分。

  • 顶点节点
datafirstinfirstout

firstin:该顶点作为弧头的第一条弧。
firstout:该顶点作为弧尾的第一条弧。
如:

A<C,A><A,B>
  • 弧节点
tailVexheadVexinfo
hlinktlink

tailVex:弧尾顶点编号。
headVex:弧头顶点编号。
info:权值
hlink:弧头相同的下一条弧。
tlink:弧尾相同的下一条弧。

由此可推出图D1的十字链表如下图所示:
十字链表
注:^表示NULL,空间复杂度:O(|V|+|E|)

2.3.2 十字链表代码实现

#include <stdio.h>
#include <stdlib.h>

#define ZERO 0
#define MAXVECNUM 15 //最大顶点数
#define INFINITY 65535//无穷,注意,指向自身的边有时候也会定义为无穷

typedef char VertexType;//顶点类型
typedef int EdgeType;//边上的权值,带权图使用


//十字链表法数据结构 Orthogonal List

//顶点节点
typedef struct VexNode {
	VertexType data;//顶点信息
	struct VexNode* firstIn, * firstOut;//顶点的第一条入边和出边
}VexNode, VexList[MAXVECNUM];

//弧节点
typedef struct ArcNd {
	int tailIndex, headIndex;//弧头弧尾的顶点编号
	struct ArcNd* hLink, * tLink;//与弧头弧尾相同的下一条弧
	EdgeType info;//权值
}*Arc,ArcNd;

//封装十字链表结构
typedef struct {
	VexList Orth_list;
	int vexNum, arcNum;//顶点数量,弧的数量
}OrList;


//获取弧头节点
Arc getArcNode(int tailIndex, int headIndex)
{
	//初始化弧头节点
	Arc Anew = (Arc)malloc(sizeof(ArcNd));
	memset(Anew, 0, sizeof(ArcNd));
	if (Anew) {
		Anew->headIndex = headIndex; 
		Anew->tailIndex = tailIndex;
	}
	return Anew;
}

//获取弧节点数据
int getArcData(char data, OrList* G)
{
	for (size_t i = 0; i < G->vexNum; i++)
	{
		if (G->Orth_list[i].data == data) {
			return i;
		}
	}
	return -1;
}

//创建十字链表
void CreatOrthList(OrList* G) 
{
	int i, vexDatai, vexDataj;
	printf("请输入图的顶点和弧的数量:\n");
	scanf("%d%d", &G->vexNum, &G->arcNum);
	//读取顶点,建立顶点集
	for ( i = 0; i < G->vexNum; i++)
	{
		printf("请输入第%d个顶点的值:", i + 1);
		scanf("%s", &G->Orth_list[i].data);
		G->Orth_list[i].firstIn = G->Orth_list[i].firstOut = NULL;//初始化指针,置为空
	}
	//读取弧,建立顶点之间的关系
	for ( i = 0; i < G->arcNum; i++)
	{
		char vex1, vex2;
		char t1, t2, t3, t4;
		printf("请逐个输入弧<Vi,Vj>:");
		scanf("\n%c,%c%*c", &vex1, &vex2);
		//初始化弧节点
		vexDatai = getArcData(vex1, G);
		vexDataj = getArcData(vex2, G);
		Arc Atemp = getArcNode(vexDatai, vexDataj);
		//处理尾
		Atemp->tLink = G->Orth_list[vexDatai].firstOut;
		Atemp->tailIndex = vexDatai;
		G->Orth_list[vexDatai].firstOut = Atemp;
		//处理头
		Atemp->hLink = G->Orth_list[vexDataj].firstIn;
		Atemp->headIndex = vexDataj;
		G->Orth_list[vexDataj].firstIn = Atemp;
	}
}

int main() {
	OrList G;
	Arc A;
	CreatOrthList(&G);
	int i;
	for (i = 0; i < G.vexNum; i++)
	{
		printf("%c顶点的出度情况为:\n", G.Orth_list[i].data);
		A = G.Orth_list[i].firstOut;
		if (!A) {
			printf("NULL\n");
		}
		while (A)
		{
			printf("<%d->%d>\n", A->tailIndex, A->headIndex);
			A = A->tLink;
		}
		printf("%c顶点的入度情况为:\n",G.Orth_list[i].data);
		A = G.Orth_list[i].firstIn;
		if (!A) {
			printf("NULL\n");
		}
		while (A) {
			printf("<%d->%d>\n", A->tailIndex, A->headIndex);
			A = A->hLink;
		}
		printf("-------------------------------------------\n");
	}
}

图D2

以图D2测试,运行结果1:
十字链表运行结果
以图D1为例,运行结果2:
十字链表运行结果

2.4邻接多重表

邻接多重表常用于存储无向图。

提示:这里可以添加要学的


3.图的操作与遍历

3.1基本操作

Adjacent(G,x,y):判断图是否存在边<x,y>或(x,y)。
Neighbors(G,x):返回图G中与 x 节点相邻的边。
InsertVertex(G,x):图G中插入顶点x。
DeleteVertx(G,x):图G中删除顶点y。
AddEdge(G,x,y):图G中增加边<x,y>或(x,y)。
RemoveEdge(G,x,y):图G中删除边<x,y>或(x,y)。
FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若存在返回顶点号,若不存在返回 -1。
NextNeighbor(G,x,y):假设图G中顶点x的邻接点是y。返回除y之外,顶点x的下一个邻接点的顶点号,若不存在下一个邻接点则返回-1。
GetEdgeValue(G,x,y):获取图G中边<x,y>或(x,y)的权值。
SetEdgeValue(G,x,y):设置图G中边<x,y>或(x,y)的权值。

3.2图的遍历

后续逐步更新

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值