C语言——图(上)(图的定义及术语、存储结构及其遍历)

前言

本篇进入图的学习,继前篇树之后,将学习比树更加复杂的结构。有向图无向图或者是否闭合,都有广泛的应用。关于图在一些实际应用中的例子,本篇会给几个例子。(插图较多)

看完本篇,你将了解到:
(1)什么是图?什么是网?图的常用术语有哪些?
(2)图的相关操作(类比于树和线性表)
(3)图的表示方法(同样也有数组和链表两种),本篇将新介绍一个邻接表
(4)将重点讨论有向图和无向图的表示方法
(5)重点!!!图的遍历(常听到的深度优先搜索DFS及广度优先搜索BFS)

一、图的定义和术语

在线性表中,数据元素为一对一的关系,在图中,任意两个数据元素都可能相关

1.图的定义

(1)图G由顶点集V和关系集VR组成的二元组,记为:G=(V,VR)
(2)V(具有相同特征的数据元素的集合):图中的数据元素我们通常称为顶点,因此V也称为顶点(元素)的有穷非空集
(3)VR:两个顶点之间关系的集合
在这里插入图片描述

2.有向图、弧(有向边)

(1)若图G任意两顶点a,b之间的关系为有序对<a,b>,即<a,b>∈VR,则称<a,b>为从a到b的一条弧/有向边
(2)其中:a是<a,b>的弧尾(初始点)
b是<a,b>的弧尾(终止点)
(3)有向图:由顶点集合弧的集合组成的图
在这里插入图片描述

例:G1=(V,VR)
V={A,B,C,D,E}
VR={<A,C>,<A,D>,<C,D>,<B,E>,<E,B>}

3.无向图、边(无向边)

如果顶点间的关系是无序对,ab之间用()表示,称无序对表示顶点a与b的一条边
(1)无向图:由顶点集合与边的集合组成的图
(2)图中若a、b间有边,则称(a,b)表示a、b互为邻接点,(a,b)依附于a和b,(a,b)与a和b相关联

在这里插入图片描述

例:G2={V,VR}
V={V1,V2,V3,V4,V5,V6}
VR={(V1,V3),(V1,V5),(V3,V5),(V4,V6)}

4.完全图

(1)n:顶点的数目
(2)e:边或者弧的数目。
取值范围:0–n(n-1)/2 (若任意两个顶点间都有边,则e取最大值)
(3)有n个顶点和n(n-1)/2条边的无向图称为完全图

5.有向完全图

有n个顶点和n(n-1)条弧的有向图

在这里插入图片描述

6.网(Network)

边(弧)上加权的图,分为有向网和无向网

故有4种类型的图:有向图、无向图、有向网、无向网

7.图的常用术语

(1)子图:对图G=(V,VR)和G1=(V1,VR1),若V1是V的子集且VR1是VR的子集,则称G1是G的一个子图
在这里插入图片描述

G1,G2,G3均为G的子图,但G4不是
(2)度:无向图中与顶点v相关联的边(x,y)的数目,称为v的度
记作TD(v)或D(v)
无向图某顶点的度表示:该顶点有多少个邻接顶点
①出度OD(v):以顶点v为弧尾的弧的数目
②入度ID(v):以顶点v为弧头的弧的数目
在这里插入图片描述

	例:OD(A)=1		
	 	OD(B)=2
	 	OD(C)=0
	 	
	 	ID(A)=1		
	 	ID(B)=1
	 	ID(C)=1
	 	
	 	顶点的度: 
	 	TD(A)=OD(A)+ID(A)=2
	 	TD(B)=OD(B)+ID(B)=3
	 	TD(C)=OD(C)+ID(C)=1

(3)连通性的术语
顶点vi到vj有路径:存在一个顶点序列vi,vi1,vi2,…,vim,vj
其中(vi,vi1),(vi1,vi2),…,(vim,vj)是图的边或弧
(4)连通图及其分量(无向图G)
①若从顶点vi到vj有路径,则称二者是连通的
②连通图:图G中任意两顶点是连通的
③连通分量:无向图的极大连通子图
注:连通图的连通分量是自己,非连通图会有几个连通分量
在这里插入图片描述

(5)强连通图及强连通分量(有向图G)
①强连通图:图G中每对顶点vi,vj之间,从vi到vj,陈vj到vi都存在路径
②强连通分量:有向图的极大强连通子图
注:强连通图的强连通分量是自己
在这里插入图片描述
(6)生成树
连通图的极小连通子图 ,包含图中所有顶点和n-1条边
在这里插入图片描述

8.图的操作

(1)CreateCraph(&G,V,VR):根据顶点集V和关系集VR生成图
(2)DestroyCraph(&G):销毁图
(3)Locate(G,v):查找顶点u的位置
(4)GetVex(G,v):读取顶点v的信息
(5)PutVex(&G,v,value):给顶点v赋值
(6)FirstAdjVex(G,v):读v的第一个邻接点
(7)NextAdjVex(G,v,w):读v(相当于w)的下一个邻接顶点
(8)InsertVex(&G,v):插入顶点
(9)DeleteVex(&G,v):删除顶点
(10)InsertArc(&G,v,w):插入弧<v,w>
(11)DeleteArc(&G,v,w):删除弧<v,w>
(12)DFSTraverse(G,visit()):深度优先遍历图
(13)BFSTraverse(G,visit()):宽度优先遍历图

二、图的存储结构

1.数组表示法(易于判断两个顶点是否有关系)

(1)数组表示法:用两个数组分别存储数据元素(顶点)的信息和数据元素之间的关系
(2)顶点数组:用一维数组存储顶点(元素 )
(3)邻接矩阵:用二维数组存储顶点(元素)之间的关系(边或弧)

1)例1:无向图
在这里插入图片描述

①将4个顶点依次存放到顶点数组,保存所有顶点的信息
顶点数组vexs v1 v2 v3 v4
0 1 2 3
②用邻接矩阵(arcs)表示二者之间的关系:1表示顶点之间有关系,0表示顶点之间无关系
位置序号分别为i、j,若顶点vi和vj之间有边,则矩阵中aij和aji均赋值为1,否则为0
在这里插入图片描述

注:结论:
	①无向图的邻接矩阵是对称矩阵
	②易求顶点vi的度:第i行或第i列上的数字之和

2) 例2:有向图
在这里插入图片描述

①将3个顶点依次存放到顶点数组,保存所有顶点的信息
顶点数组vexs A B C
0 1 2
②用邻接矩阵(arcs)表示二者之间的关系:1表示顶点之间有关系,0表示顶点之间无关系
位置序号分别为i、j,若顶点vi和vj之间有弧,则矩阵中aij和aji均赋值为1,否则为0
在这里插入图片描述

注:结论:
	①有向图的邻接矩阵不一定是对称矩阵
	②求顶点vi的度:累加第i行元素,得初度OD(vi) 
					累加第i列元素,得入度OD(vi) 
				度 = 初度+入度 

(4)网
1)对于有向网和无向网,需要在邻接矩阵中加上关系的权值,如果两个顶点有邻接关系,
就用权值代替原来的1,否则就用∞代替0
例:如图
在这里插入图片描述

求度时,统计第i行(列)上非无穷大的数的个数

(5)数据类型定义
①约定图中可出现的顶点数目最大值(考虑到数组大小要合适)
②存放顶点数组、邻接矩阵、顶点数、图的类型、弧(边)数

//数据类型定义
#define MAX_VERTEX_NUM 20
typedef enum 
{
	DG,DN,UDG,UDN
}GraphKind;
typedef struct
{
	VertexType vexs[MAX_VERTEX_NUM];
	VRType arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
	int vexnum,arcnum;
	GraghKind kind;
}Mgraph;

2.邻接表表示法(边较少时效率高)

顺序+链式的物理存储结构,通过头结点数组保存顶点信息,用单链表保存顶点之间的关系
(1)无向图的邻接表
在这里插入图片描述
①为图G的每个顶点建立一个单链表,第i个单链表中的结点表示依附于顶点vi的边
②头结点数组,保存每一个顶点:(2个部分)顶点值+单链表头指针 (指向1个由所有邻接顶点的序号构成的单链表)
③若无向图G有n个顶点和c条边,需n个表头结点和2e个表结点
④无向图G的邻接表,顶点vi的度=第i个单链表的长度

(2)有向图的邻接表
①第i个单链表中的表结点的值为j,表示以顶点vi为尾的一条弧(vi,vj)
②若有向图G有n个顶点和e条弧,需n个表头结点和e个表结点
③顶点vi的入度:遍历全部单链表,统计结点值为i的结点数
④顶点vi的出度=第i个单链表的长度
在这里插入图片描述
在这里插入图片描述

(3)有向网的邻接表
表结点表示边或弧,就对表结点扩充一个属性域,至少包含:顶点序号、权值、下一表结点指针
在这里插入图片描述
在这里插入图片描述

(4)有向网的逆邻接表
①若vi到vj有一条弧,使第j个单链表中的表结点的值为j,表示以顶点vi为尾的一条弧
②若有向图G有n个顶点和e条弧,需n个表头结点和e个表结点
③顶点vi的出度:遍历全部单链表,统计结点值为i的结点数
④点vi的入度=第i个单链表的长度
在这里插入图片描述
在这里插入图片描述

数据类型定义

//邻接表表示法的数据类型定义
#define MAX_VERTEX_NUM 20
typedef struct ArcNode//表结点类型定义,对网需要加权值属性
{
	int adjvex;//顶点位置编号
	struct ArcNode *nextarc;//下一个表结点指针
	InfoType *info; 
}ArcNode;

typedef struct VNode//头结点及其数组类型定义
{
	VertexType data;//顶点信息
	ArcNode *firstarc;//指向第一条弧 
}VNode,AdjList[MAX_VERTEX_NUM]; 

typedef struct//邻接表的类型定义
{
	AdjList vertices;//头结点数组
	int vexnum,arcnum;//顶点数、弧数
	GraphKind kind;//图的类型 
}ALGraph; 

十字链表表示法(针对有向图设计,可看成邻接表和逆邻接表的一种组合)

(1)每条弧有一个弧结点,若vi到vj有弧,则在结点中包括
弧结点: tailvex headvex hlink tlink
其中:tailvex:弧尾的位置
headvex:弧头的位置
hlink:指向下一条弧头相同(vj)的弧
tlink:指向下一条弧尾相同(vi)的弧
(2)每个顶点有一个顶点结点(通过顶点的结点数组保存有向图的顶点信息)
顶点结点: data firstin firstout
其中:data:顶点信息
firstin:指向以该顶点为弧头的第一条弧
firstout:指向以该顶点为弧尾的第一条弧

//有向图十字链表存储表示的数据类型定义
#define MAX_VERTEXT_NUM 20
typedef struct ArcBox
{
	int tailvex,headvex;//该弧的尾和头顶点的位置
	struct ArcBox *hlink,*tlink;//分别为弧头相同和弧尾相同的弧的链域
	 
}ArcBox; 

typedef struct VexNode
{
	VertexType data;
	ArcBox *firstin,*firstout;//分别指向该顶点的第一条入弧和出弧	 
}VexNode;

typedef struct 
{
	VexNode xlist[MAX_VERTEXT_NUM];//表头向量
	int vexnum,arcnum;//有向图的当前顶点数和弧数 
}OLGraph; 

在这里插入图片描述
在这里插入图片描述

①以邻接表为基础,扩展结点属性成起止结点序号
②再添加逆邻接表信息

4.(无向图)邻接多重表(针对无向图设计)

(1)每个顶点有一个头结点
头结点: data firstedge
其中:data:顶点信息
firstedge:指向第一条依附于该顶点的边
(2)每一条边有一个表结点
表结点:mark ivex jvex ilink jlink
其中:mark:标志域,可用以标记该条边是否被搜索过
ivex、jvex:该条边依附的两个顶点在顶点数组的位置
ilink:指向下一条依附于顶点vi的边
jlink:指向下一条依附于顶点vj的边

//(无向图)邻接多重表
#define MAX_VERTEXT_NUM 20
typedef enum {unvisited,visited} visitedlf;
typedef struct EBox
{
	visitedlf mark;//访问标记
	int jvex,jvex;//该边依附的两个顶点的位置
	struct EBox *ilink,*jlink;//分别指向依附于这两个顶点的下一条边	 
}EBox;

typedef struct VexBox
{
	VertexType data;
	EBox *firstedge;//指向第一条依附于该顶点的边 
}VexBox;

typedef struct
{
	VexBox adjmullist [MAX_VERTEXT_NUM];
	int vexnum,edgenum;//无向图的当前顶点数和边数 
}AMLGraph;  

在这里插入图片描述

三、图的遍历

1.图的深度优先搜索DFS(堆栈)

(1)
在这里插入图片描述
eg:以A开头,访问其一个邻接结点E,依次访问到H,发现H没有未被访问的邻接结点
则回退到F,F也没有,一路回退至A,从A继续访问B,一路访问至C,再一路回退到A
此时全部访问完毕
本次访问序列为:AEGFHBDC
注:由于改图是连通图,故一次DFS即可访问完,否则需深度优先搜索多次

//深度优先搜索遍历算法代码(假定结点序号从0开始)
boolean visited[MAX];
void DFSTraverse(Graph G,Status (*visit()))
{
	for (int v=0;v<G.vexnum;v++)//初始化各顶点未访问状态
	{
		visited[v] = false;
	}
	for (int v=0;v<G.vexnum;v++)
	{
		if (!visited[v])//从一个未访问的顶点开始 
			DFS(G,v,visit);
	} 
}

void DFS(Graph G,int v,Status (*visit()))//递归算法 
{
	visited[v] = true;
	visit(v);
	for (w=FirstAdjVex(G,v),w>=0;w=NextAdjVex(G,v,x))
	{
		if (!visited[w])//处理所有未访问的邻接顶点 
			DFS(G,w,visit);
	}
}	

(2)该算法的效率与图的存储结构有关
邻接矩阵:T(n)=O(n^2)
邻接表: T(n)=O(n+e)(找邻接点需O(e))

2.图的广(宽)度优先搜索BFS(为控制访问序列,用队列)

(1)如图
在这里插入图片描述

eg:从A出发,访问其所有相邻结点EFB,按这个次序访问E的所有未被访问的结点G,其次是FB
序列:AEFBGHDC

//广度优先搜索遍历算法
void BFSTtaverse(Graph G,Status (*visit())) 
{
	for (v=0;v<G.vexnum;v++)
	{
		visited[v]=false;//初始化visited数组为false 
	}
	InitQueue(Q);
	for (v=0;v<G.vexnum;v++)//按订单位置序号依次选择顶点
	{
		if (!visited[v])//遇到为访问过的顶点开始遍历 
		{
			visited[v] = true;//首先访问并标记起点v 
			visit(v);
			EnQueue(Q,v);//将v加入到q中 
			while (!QueueEmpty(Q))//若队列非空 
			{
				DeQueue(Q,u);//出队 
				for (w=FirstAdjVex(G,u),w>=0;w=NextAdjVex(G,u,w))//对u的所有相邻结点分析 
				{
					if (!visited[w])//若某个相邻结点未被访问 
					{
						visited[w] = true;//则访问并标记 
						visit(w);
						EnQueue(Q,w);//加入到q中 
					}
				}
			}
		}
	} 
}

(2)该算法的效率与图的存储结构有关

总结

1.图的相关术语定义比较复杂,重点在有向图和无向图,下篇中会详细讲到其应用。
2.图的操作可类比于树和线性表去记忆,大体结构相同。
3.掌握基本操作后,图的存储结构,针对有向图和无向图,我们采取了不同的方法来节省空间和时间。
4.图的遍历,深度优先搜索DFS和广度优先搜索BFS采取不同的结构有很多实现方法,这里仅给出其中一种。这两种算法是经典且常用的算法,建议多看看不同的代码。
5.尤其关注连通图这个概念。

如有错误,欢迎指正!
代码非原创。
(ps:一不小心又咕咕了半个多月)

  • 18
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
二叉树可以用链式存储结构或者数组存储结构来实现。下面是链式存储结构的实现: ```c typedef struct Node { int data; struct Node *leftChild; struct Node *rightChild; } Node; // 前序遍 void preOrder(Node *root) { if (root != NULL) { printf("%d ", root->data); preOrder(root->leftChild); preOrder(root->rightChild); } } // 中序遍 void inOrder(Node *root) { if (root != NULL) { inOrder(root->leftChild); printf("%d ", root->data); inOrder(root->rightChild); } } // 后序遍 void postOrder(Node *root) { if (root != NULL) { postOrder(root->leftChild); postOrder(root->rightChild); printf("%d ", root->data); } } ``` 其中,前序遍先访问根节点,再访问左子树,最后访问右子树;中序遍先访问左子树,再访问根节点,最后访问右子树;后序遍先访问左子树,再访问右子树,最后访问根节点。 如果使用数组存储结构,可以按照如下方式实现: ```c #define MAX_SIZE 100 int tree[MAX_SIZE]; // 前序遍 void preOrder(int root, int size) { if (root < size) { printf("%d ", tree[root]); preOrder(root * 2 + 1, size); preOrder(root * 2 + 2, size); } } // 中序遍 void inOrder(int root, int size) { if (root < size) { inOrder(root * 2 + 1, size); printf("%d ", tree[root]); inOrder(root * 2 + 2, size); } } // 后序遍 void postOrder(int root, int size) { if (root < size) { postOrder(root * 2 + 1, size); postOrder(root * 2 + 2, size); printf("%d ", tree[root]); } } ``` 数组实现中,左子节点的下标为 root * 2 + 1,右子节点的下标为 root * 2 + 2。下标从 0 开始,因此如果树的节点数为 n,数组的大小应该为 2^n - 1。
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬茶@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值