C语言数据结构基础 图 的基本操作

        线性表中结点之间的关系是一一对应的,也就是每个结点仅存在一个前驱和一个后继,当然首尾或许不存在前驱或后继;树是按分层关系组织的结构,树中结点之间关系是一对多,即一个双亲结点可以有许多孩子结点,但每个结点仅有一个前驱。图是比树更一般,更复杂的非线性数据结构。图结点中的关系是任意的,每个结点之间都可以产生联系,即图中元素之间是多对多的关系。

        下面就对图的基本定义与操作进行演示。

一.图的定义

01-01.图的基本解释

        图(Graph)是一种网状数据结构,由许多结点以及结点间的关系构成。

        当然,一个结点也算图,就是一个结点无法蕴含太多关系罢了。

        其形式化定义如下:

        Graph=(V,R)

        V={ x | x∈DataObject }

        R={ VR }

        VR={ <x,y> | P(x,y)∩(x,y∈V) }

        DataObject 为一个集合,该集合内所有元素具有相同的特性。V中的数据元素通常称为顶点(Vertex),VR是两顶点之间的关系的集合。P(x,y)表示x和y之间有特定的关联属性P。通俗点就是都具有R关系的顶点V构成了图Graph。

        若<x,y>∈VR,则<x,y>表示从顶点x到顶点y的一条弧(Arc),并称x为此弧的弧尾(Tail)或起始点,称y为弧头(Head)或终端点。这样的有方向的图称为有向图。(就如上方的图一样,是由有方向的弧连接起来的结点)

        若<x,y>∈VR,必有<y,x>∈VR,即VR是对称关系,这时箭头就可以忽略了,用无序对(x,y)来代替两个有序对,表示x和y之间的一条边(Edge),此时的图称为无向图。(如下)

       

01-02.图的基本术语

         在了解了图的基本概念后,需要对图的一些基本术语进行了解。

        01.完全图,稀疏图与稠密图

        这些概念听起来感觉像矩阵一样,什么稀疏矩阵,稠密矩阵。这些形容词描述的都是这个图或矩阵中元素的疏密程度。

        设 n 表示图中顶点个数,用 e 表示图中边或弧的数目,并且不考虑图中每个顶点到其自身的边或弧(不考虑自身成环),即若<Vi,Vj> ∈ VR,则 Vi ≠ Vj。此时,对于无向图而言,其边数e的取值范围就是0~n(n-1)/2. 有n(n-1)/2 条边时,说明图中每个顶点之间都相互连接,也就是每个顶点都有n-1条边与其连接,此时,这样的无向图就被称作无向完全图(每个顶点都连接完全)。相应的,如果有向图的每个顶点的联系都满足,即各个顶点相互连接,则其边数范围e的取值就在0~n(n-1),而此时的有向图就是有向完全图。 

        稀疏图:边数很少的图(e<nlogn)

        稠密图:介于稀疏图与完全图之间

        02.子图

        设有两个图G=(V,{E})和图G`=(v`,{E`}),若V` 包含于V,并且E`包含于E,则称图G`为G的子图。(也就是说拿  原图中的元素  构建的新图,上限就是原图,下限就是一个结点)

        下面拿刚才的有向图作为G,分析其子图

                    

        

        03.邻接点

        对于无向图G=(V,{E}),如果边(v,v`)∈E,则称顶点v,v`互为邻接点,即v,v`相邻接。边(v,v`)依附于顶点v和v`,也叫边(v,v`)与顶点v和v`相关联。对于有向图G=(V,{A})而言,若弧<v,v`>∈A,则称顶点v邻接到顶点v`,顶点v`邻接自顶点v,或者说弧<v,v`>与顶点v,v`相关联。(其实就是在无向图中,两个通过一条弧相连的顶点相互邻接,邻接就是邻着的,在有向图中,就有了方向,由弧尾邻接到弧头,反过来就不是邻接了)

        

        04.度,入度和出度

        对于无向图而言,顶点v的度就是与v相关联的边的数目,记作TD(v)。就拿上面无向图的邻接的图来讲,A和B的度都是1,而C的度是0.

        不过在有向图中,度就要划分为出度和入度两部分了。

        出度:以顶点v为起点,也就是弧尾,的弧的数目称为该顶点v的出度,记作OD(v);

        入度: 以顶点v为终点,也就是弧头,的弧的数目称为该顶点的入度,记作ID(v)。

        其度的数目就是其出度与入度之和,即 TD(v) = OD(v)+ID(v) 。

        如在上面 有向图的邻接中 A的出度为1,入度为1,其度为2;

        05.路径与回路

        无向图G=(V,{E} )中,从顶点v到v`的路径是一个顶点序列 v0,v1,v2……vn,其中(vi-1,vi)∈E,1≤i≤n。如果图G是有向图,则其路径也是有向的,只能从弧尾指向弧头,顶点序列应满足<vi-1,vi>∈E,1≤i≤n。

        路径的长度是指路径上经过的弧或边的数目。

        如果在一个路径中,其第一个顶点和最后一个顶点相同,即v=v`,则称此路径为一个回路或环。

        如果表示路径的顶点序列中的顶点互不相同,也就是没有成环,这样的路径称作简单路径,

相应的,如果只是第一个顶点与最后一个顶点相同,其余顶点不重复的回路称作简单回路。

        06.连通图

        在无向图G=( V,{E} )中,若从vi到vj 有路径相通,则称顶点vi与顶点vj是连通的。如果对于图中的任意两个顶点vi,vj ∈V,vi,vj都是连通的,则称该无向图为连通图。 而无向图中的极大连通子图称为该无向图的连通分量。

        在有向图G=( V,{ E } )中,若对于每对顶点vi,vj∈V,并且vi≠vj,从vi到vj都有路径,则称该有向图为 强连通图,相应的,有向图的极大连通子图称为有向图的强连通分量。

下面用一个无向图及其连通分量来掩饰,有向图需要在此基础上注意箭头方向,只有弧尾到弧头是连通的。

        07-权与网

        在实际应用中,图的边或弧上往往具有一定意义的数有关,即每一条边都有与它相关的数,称作权,这些权可以表示从一个顶点到另一个顶点的距离或耗费等信息。这种带权的图称为赋权图或网。

二.图的存储结构

        了解了图的定义后,可能会想,我们应如何将这顶点与顶点的关系存储起来。实际上,图的存储方法有很多,如邻接矩阵,邻接表,邻接多重表,十字链表等。每种方法各有利弊,需要根据实际问题选择合适的存储方式。

02-01.邻接矩阵表示

        图的邻接矩阵表示法(Adjacency Matrix )也称作数组表示法。它采用两个数组来表示图:一个一维数组存储所有顶点信息,另一个二维数组存储各顶点间的关系,这个二维矩阵也被称作邻接矩阵,而且邻接其实也说明了,存储的这些关系都是邻接关系。

        若G是一具有n个顶点的无权图,G的邻接矩阵具有如下性质的n×n的矩阵A:

        

如下:

        若图G是一个有n个顶点的网,则它的邻接矩阵是具有如下性质的n×n 矩阵A2:

如下:

存储实现

        既然已经明确了邻接矩阵在计算机内的存储方式,现在用代码实现一下:

const int MaxSize = 20;
const int InFinity = 99; // 用来表示极大值,也就是正无穷
// 图的有向无向区别于<A,B>与<B,A>的值是否都有;图和网区别于权值,也就是<A,B>=?
// 将枚举类又定义为新的数据类型,专门用来表示存储的图的种类,DG有向图,DN有向网,UDG无向图,UDN无向网
typedef enum{DG=0,DN,UDG,UDN} GraphKind; 
typedef char Datatype;             // 数据类型

typedef struct ArcNode {
	int adiacent;   // 存储图中,两点是否相邻的结果
	int weight;     // 存储网中两点之间权值
}AN;

typedef struct Adiacent_Matrix {
	Datatype vertex[MaxSize];            // 顶点数组
	AN arcnode[MaxSize][MaxSize];        // 邻接矩阵
	GraphKind kind;                      // 图的种类
	int vexnum, arcnum;                  // 顶点数,弧数
}AM;

  邻接矩阵的大小只与结点数量有关,它的大小一直为顶点数n的平方( n²),不过在查找任意两个顶点之间的联系上,可以直接进行(无序的优势)。

初始化实现
// 因为这个图存储的是顶点字符,并没有存储其在数组中的位置,以此函数遍历找到顶点对应数组下标,比较耗时
int LocateVertex(AM* M, Datatype v) {
	int num = 0;
	for (int i = 0; i < M->vexnum; i++) {
		if (M->vertex[i] == v) {
			num = i; break;
		}
	}
	return num;
}
// 有向网实现
// 当然这些图像类型主要区别在于 顶点之间的数字关系,有向图,哪里通,哪里一,无向图,两顶点之间都为1,图与网也只是加上了权值代替表示连接的1罢了
void Creat_DN(AM* M) {
	printf("请输入所要构建的图的顶点数以及弧数:\n");
	scanf(" %d,%d", &M->vexnum, &M->arcnum);
	// 进行初始化
	for (int i = 0; i < M->vexnum; i++)
		for (int j = 0; j < M->vexnum; j++)
			M->arcnode[i][j].adiacent =InFinity;
	printf("请输入顶点:\n");
	for (int i = 0; i < M->vexnum; i++) { // 输入顶点
		scanf(" %c", &M->vertex[i]);
	}

	printf("输入一条弧的权值,以及其弧尾和弧头,:\n");
	Datatype v1, v2;
	int weight;
	for (int i = 0; i < M->arcnum; i++) {
		int x, y;
		scanf("%d,%c,%c", &weight, &v1, &v2);
		x = LocateVertex(M, v1);
		y = LocateVertex(M, v2);
		M->arcnode[x][y].adiacent = 1;
		M->arcnode[x][y].weight = weight;
		printf("Next\n");
	}

}

02-02.邻接表表示

        与线性表可以说一样,因为不是用顺序表存储成矩阵,就是用链表串联起来。

        图的邻接矩阵表示法(即图的数组表示法)在应对比较稠密,也就是顶点之间弧数多的图,存放计较省空间,反之,对于稀疏图来讲,这就很浪费空间了。邻接表(Adjacency List)就是图的链式存储结构,它就克服了邻接矩阵所需空间大且固定的弊端,只保存有相邻关系的顶点信息,所以它所需要的存储空间取决于顶点间弧的数目。在邻接表中,对图中的每个顶点建立一个带头结点的边链表,可以参考多栈共享的思路。因此一个图的邻接表由表头结点表与边表两部分组成。

        ① 表头结点表: 由所有表头结点依顺序结构的形式存储,便于随机访问每个顶点及与其联系的边,因此他需要一个数据域存储顶点元素,一个指针域存放其邻接的边。

        ②边表: 存储弧的两端顶点,并且将其余的边连接在一起,指针域指向下个边。

        这只是基础的定义,要根据实际情况扩充数据结构满足自己需求。

        当这两种结构构建好之后,就是将其组装,一个表头结点链接上一列表结点。

存储实现
#define M 20
// 将枚举类又定义为新的数据类型,专门用来表示存储的图的种类,DG有向图,DN有向网,UDG无向图,UDN无向网
typedef enum { LG = 0, LN, ULG, ULN } GraphKind;

typedef char DataType;  //顶点类型
// 弧
typedef struct XiaBiao_Weight_Arc {
	DataType arc; // 弧头对应下标
	int weight; // 此弧对应的权值
	struct XiaBiao_Weight_Arc* next; // 其他的弧
}ArcNode;
// 顶点
typedef struct {
	DataType vertex; // 顶点
	ArcNode* arc;    // 弧 弧头

}VertexNode;

// 邻接表
typedef struct {
	VertexNode vertexs[M]; // 存储顶点
	int vernum;              // 存储实际顶点数目
	int arcnum;              // 弧的数目
	GraphKind kind;          // 图的种类
}AdjList;
初始化实现

        这其实与邻接矩阵就差不多了

// 顶点查找
// 给定所要查找的元素,在图中遍历出它对应的下标并返回此值
int LocateVertex(AdjList* AL, DataType ver) {
	int n = 0;
	for (int i = 0; i < AL->vernum; i++) { // 遍历所有顶点
		if (AL->vertexs[i].vertex == ver) { // 直到找到要找的顶点
			return i;                       // 返回其下标
		}
		if (AL->vertexs[i].vertex != NULL) ++n; // 如果没有,就把一个空位置下标返回,方便存新元素
	}
	return n;
}


// 00-初始化一个有向网
AdjList *CreatLN() {
	AdjList* AL = (AdjList*)malloc(sizeof(AdjList));
	AL->kind = LN;  // 种类 有向网   实际上区别在于弧的权值以及是否双向罢了
	printf("请输入顶点以及弧的总数:\n");
	scanf("%d,%d", &AL->vernum, &AL->arcnum);
	// 先来一波初始化
	for (int i = 0; i < AL->vernum; i++) { // 把所有顶点的位置预留出来,并进行初始化
		AL->vertexs[i].vertex = NULL;
		AL->vertexs[i].arc = NULL;
	}

	// 进行输入
	printf("请输入顶点:\n");
	for (int i = 0; i <AL->vernum; i++) { // 输入顶点
		scanf(" %c", &AL->vertexs[i].vertex);
	}
	for (int i = 0; i < AL->arcnum; i++) {
		DataType v1, v2;            // 接收弧尾及弧头
		int weight;                 // 如果是网,接收对应权值,没权值就默认1就欧克了
		int j, k; // ^ && ^
		printf("请输入弧尾,弧头,以及其对应的权值,用逗号分隔\n");
		scanf(" %c,%c,%d", &v1, &v2, &weight);
		j = LocateVertex(AL, v1);         // 找到弧头与弧尾对应的下标,在邻接表中,实际上只要有弧尾下标就够了
	//	k = LocateVertex(AL, v2);
		
		ArcNode* arcnode = (ArcNode*)malloc(sizeof(ArcNode)); // 申请一个弧空间存放弧头及弧长,也就是他们间的距离(权值)
		arcnode->next = AL->vertexs[j].arc;  // 用头插法了,头是固定的
		arcnode->arc = v2;                   // 头插法的赋值过程
		arcnode->weight = weight;
		AL->vertexs[j].arc = arcnode;

	}
	return AL;                            
}

02-03.十字链表

        十字链表(Orthogonal List) 是有向图的另一种链式存储结构,可以看作是将有向图的邻接表与其对应的逆邻接表(从弧头找到弧尾)结合起来,(其实邻接表存在很大的痛点,只能找到某一条弧的弧头,当然也可以通过直接在弧的数据结构中加入弧尾下标,主要是解决查找出入度的问题)。有向图中的每个顶点在十字链表中对应有一个结点,称为顶点结点。

02-04.邻接多重表

        邻接多重表(Adjacency Multi-list)是无向图的另一种存储结构,它能够提供更为方便的边处理信息。在无向图的邻接表表示法中,每条边在邻接表中都对应着两个结点,分别在i,j链表上。这在图进行一些操作上带来不便,如检测某条边是否被访问过,则需要同时找到该边的两个结点,而这两个结点又分别在两个边链表中。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值