YAZONG 我的开源

(数据结构)第六讲-图(6.1什么是图)

  , ,
0 评论0 浏览

6.1.1图的定义

图:强大的数据结构。

建立多对多的关系。

六度空间理论:人与人之间的关系,可以用边来表示。

全球13亿互联网用户之间的联系。

地铁两个站之间路径最短,最便宜的路线。

image.png

图中非常经典的最小生成树问题。

image.png

图的强大,是因为包括了线性表(一对一)和树(一对多)。

可以认为,线性表和图都是图的一个特殊情况。

image.png

边是顶点对:如果这条边是无向边,无所谓方向,可以从V到W,也一定可以从W到V,双向道路,可以用圆括号表示。

有向边:单行线,只能从V到W,不可以W到V,尖括号表示。

重边:两个顶点之间是无向边的话,默认只有一条,不可能有很多条。

自回路:当说道有向边时,一定从一个顶点到另一个顶点,不可能指回自己。

image.png

在介绍一个结构时,肯定先是从抽象数据类型定义开始的。

三要素:类型名称、数据对象集(什么东西构成)、操作集。

注意:图可以没有边,但至少有一个顶点。

image.png

无向图:图中里面所有边都是无所谓方向的。

有向图:图中里面的边可能单向,也可能双向,即:方向对图很重要。

每条边的数字:权重,可定义为这条路线的长度,带权重图的另外名称叫网络。

6.1.2邻接矩阵表示法

image.png

用二维数组去表示一个图,有N个顶点的话,假设这个顶点是从0到N-1来编号的,表示它们有一个非常简单的做法。

image.png

如果二维数组,某个坐标,那么右边用1表示,没边用0表示。

特点:

(1)因为不允许自回路的边,所以我们认为一个结点到他自己是不需要边的,所以这个对角阵不全都是0。

(2)矩阵是对称的。以这条对角线为基础,如果这个元素是1,那么跟他对称的这个元素的值一定是1,0同理。

比如G03,

如果是1的话,那么0和3之间是有一条边的。

当然,相对的3和0之间也有一条边的。
所以,G30这条边也是得是存在的。

这里把一条边存了两次,有必要吗?

邻接矩阵-无向图存储节省空间与元素位置

image.png

对于无向图,邻接矩阵中有一半空间是被浪费的。

image.png

真的只存储一半的元素,比如只存下三角,技巧用一维数组,存下三角矩阵的额元素,存法按行来,

A0=G00

从上到下,从左到右一行行的来存储,当然空间省了,带来的麻烦就是,元素不好找了,要判断ij两个元素之间是否右边的时候,如果用原来二维数组的存法,只要去看Gij是否等于1就可以了,现在Gij被藏在A里面,哪个位置?

image.png

比如一个元素处在Gij的位置,在A中到底是第几个元素呢?

image.png

数一下在它前面的个数,前面i-1行元素的总个数,再加上

image.png

这是列号。这边是应该有J个元素。

image.png

image.png

邻接矩阵-好处

image.png

现在知道图可以使用邻接矩阵来表示。

邻接矩阵-邻接点

image.png

邻接点:有直接的边跟它相连的所有的顶点,叫这个点的邻接点。

如何用邻接矩阵去找所有的邻接点呢?

简单。

对无向图,只要沿着这个矩阵的一行扫描过去,所有不是0的那些顶点就一定有边跟它直接相连的。

有向图,这个邻接矩阵不一定是对称的,所以不仅要扫描一行,还要扫描一列,才能找到根它有直接边相连的邻接点。

邻接矩阵-度

image.png

度:跟这个顶点相关的所有边的个数,对于有向图来说,俩概念,出度、入度。

如何计算任意一个顶点的度?

无向图:扫描一行或者这一列,非0元素的个数,把所有的1加起来就是度。

出度:扫描第i行1的个数,从Vi这个顶点发出去的所有的边。

当扫描第i列的时候,得到的就是所有指向Vi这个顶点的边的个数。

邻接矩阵-稀疏图与稠密图/完全图

image.png

在图里有很多很多的结点,一共只有两条边,有邻接矩阵存储时,此邻接矩阵长啥样呢?

有一大堆的0,非常浪费空间。

image.png

稀疏图:点很多,相对来说边很少,矩阵中会有大量的0元素,也就是无效的元素。

如果图不是很稀疏,稠密图。

完全图/稠密图:给N个结点,任意两个结点直接都有一条边,边数是达到极大的图。

相当于矩阵中所有的元素都是1,这么存就没有一个元素是浪费的,很核算。

稀疏图:浪费的不仅仅是空间的问题,还浪费时间。

用邻接矩阵,不得不把邻接矩阵中的所有元素都扫描一遍,数有多少个1,问题是1很少,这样浪费的不仅是空间,而且是时间。

6.1.3邻接表表示法

image.png

邻接矩阵在表示稀疏图时,不方便,比较浪费空间和浪费时间,为解决此问题的方法是邻接表。

这样如果有大量0的话,那么这些0都不会出现在链表中,通过此方法来省空间。

image.png

这个小镇,如何用邻接表存储呢?

首先,为每一个结点开一个指针数组,一共10个结点,一个长度为10的数组,数组的元素是一个头指针。

image.png

0跟1和3相连。

所以串起来此链表。

image.png

0和3、2和5等都是相连的,以此类推,在这个链表上就会形成这样的邻接表来表示此图。

要注意的是,邻接表的表示法不是唯一的,因为在这个串上面,这些结点的顺序并不重要,需要仔细数一下这个图占了多少空间。

image.png

真的很节省空间吗?

在邻接矩阵的表示法中,每一条边占了一个整数的位置,那么每一条边是如何存储的呢?

5链表中有9,9链表中有5,所以一条边必定存了两遍。

存了顶点的整数编号,指针指向下一个,所以一条边占差不多两个整数。

而且对于网络来说,结构中还要增加权重的域,比如可以把1变成权重。

所以,如果要用邻接表的话,那么一定要够稀疏才划算。

image.png

第1个:

找Vi的所有邻接点,顺着Gi的链表一直走下去,就把它所有的邻接点都访问到了,没有任何不必要的零点。

第2个:

整个邻接表用了多少空间呢?

每个结点,如果不考虑权重的话,那么至少是需要两个域的。

(接”问题”)

第3个:

是否方便计算任意一个结点的度?取决于在说一个什么图?

无向图:只要访问顶点所代表的链表,数一下这个链表一共串了多少条边,就知道度是多少。

有向图:

出度计算:只能计算出度因为串在Vi对应的链表上面,只是从Vi往外指出去的那些边。

入度计算:正邻接表存的是邻接矩阵的每一行存成了一个链表,逆邻接表是把邻接矩阵的每一列再形成一个链表,那相应的这一列存的就是指向它自己的边,这边可以比较方便的计算入度。

第4个:

非常麻烦的问题。

图的表示不是说只能用邻接矩阵或者邻接表,用的方法取决于要解决的具体问题。

C语言代码 : 图的建立-邻接矩阵表示

#include<stdio.h>
/*
图的建立:邻接矩阵的表示
*/
int main(void){
	return 0;
}
/* 最大顶点数设为100 */
#define MaxVertexNum 100
/* ∞设为双字节无符号整数的最大值65535*/
#define INFINITY 65535
/* 用顶点下标表示顶点,为整型 */
typedef int Vertex;
/* 边的权值设为整型 */
typedef int WeightType;
/* 顶点存储的数据类型设为字符型 */
typedef char DataType;

/* 边的定义 */
typedef struct ENode *PtrToENode;
struct ENode{
	/* 有向边<V1, V2> */
	Vertex V1, V2;
	/* 权重 */
	WeightType Weight;
};
typedef PtrToENode Edge;
/* 图结点的定义 */
typedef struct GNode *PtrToENode;
struct GNode{
	int Nv;/* 顶点数 */
	int Ne;/* 边数   */
	WeightType G[MaxVertexNum][MaxVertexNum]; /* 邻接矩阵 */
	/* 注意:很多情况下,顶点无数据,此时Data[]可以不用出现 */
	DataType Data[MaxVertexNum];      /* 存顶点的数据 */
}
/* 以邻接矩阵存储的图类型 */
typedef PtrToENode MGraph;
/* 
初始化一个有VertexNum个顶点但没有边的图 
*/
MGraph CreateGraph(int VertexNum){
	Vertex V, W;
	MGraph Graph;
	/* 建立图 */
	Graph = (MGraph)malloc(sizeof(struct GNode));
	/* 顶点数 */
	Graph->Nv = VertexNum;
	/* 边数   */
	Graph->Ne = 0;
	/* 初始化邻接矩阵 */
    /* 注意:这里默认顶点编号从0开始,到(Graph->Nv - 1) */
    for(V = 0; V < Graph->Nv; V++){
    	for(W = 0; W < Graph->Nv; W++){
    		Graph->G[V][W] = INFINITY;
		}
	}
    return Graph;
}

/* 插入边 <V1, V2> */
void Insert(MGraph Graph, Edge E){
	Graph->G[E->V1][E->V2] = E->Weight;
	/* 若是无向图,还要插入边<V2, V1> */
	Graph->G[E->V2][E->V1] = E->Weight;
}


MGraph BuildGraph(){
	MGraph Graph;
	Edge E;
	Vertex V;
	int Nv, i;
	/* 读入顶点个数 */
	scanf("%d", &Nv);
	/* 初始化有Nv个顶点但没有边的图 */
	Graph = CreateGraph(Nv);
	/* 读入边数 */
	scanf("%d",&(Graph->Ne));
	/* 如果有边 */
	if(Graph->Ne != 0){
		/* 建立边结点 */
		E = (Edge)malloc(sizeof(struct ENode));
		/* 读入边,格式为"起点 终点 权重",插入邻接矩阵 */
		for(i = 0; i < Graph->Ne; i++){
			scanf("%d %d %d", &E->V1, &E->V2, &E->Weight);
			/* 注意:如果权重不是整型,Weight的读入格式要改 */
			InsertEdge(Graph, E);
		}
	}
	/* 如果顶点有数据的话,读入数据 */
	for(V = 0;V < Graph->Nv;V++){
		scanf("%c", &(Graph->Data[V]));
	}
	return Graph;
}

C语言代码 : 图的建立-邻接表表示

#include<stdio.h>
/*
图的建立:邻接表的表示
*/
int main(void){
	return 0;
}
/* 最大顶点数设为100 */
#define MaxVertexNum 100
/* 用顶点下标表示顶点,为整型 */
typedef int Vertex;
/* 边的权值设为整型 */
typedef int WeightType;
/* 顶点存储的数据类型设为字符型 */
typedef char DataType;
/* 边的定义 */
typedef struct ENode *PtrToENode;
struct ENode{
	/* 有向边<V1, V2> */
	Vertex V1, V2;
	/* 权重 */
	WeightType Weight;
}
typedef PtrToENode Edge;
/* 邻接点的定义 */
typedef struct AdjVNode *PtrToAdjVNode;
struct AdjVNode{
	/* 邻接点下标 */
	Vertex AdjV;
	/* 边权重 */
	WeightType Weight;
	/* 指向下一个邻接点的指针 */
	PtrToAdjVNode Next;
}
/* 顶点表头结点的定义 */
typedef struct Vnode{
	/* 边表头指针 */
	PtrToAdjVNode FirstEdge;
	/* 存顶点的数据 */
	DataType Data;
	/* 注意:很多情况下,顶点无数据,此时Data可以不用出现 */
} AdjList[MaxVertexNum]; /* AdjList是邻接表类型 */
/* 图结点的定义 */
typedef struct GNode *PtrToGNode;
struct GNode{
	/* 顶点数 */
	int Nv;
	/* 边数   */
	int Ne;
	/* 邻接表 */
	AdjList G;
}
/* 以邻接表方式存储的图类型 */
typedef PtrToGNode LGraph;
/* 初始化一个有VertexNum个顶点但没有边的图 */
LGraph CreateGraph(int VertexNum){
	Vertex V;
	LGraph Graph;
	/* 建立图 */
	Graph = (LGraph)malloc(sizeof(struct GNode));
	Graph->Nv = VertexNum;
	Graph->Ne = 0;
    /* 初始化邻接表头指针 */
    /* 注意:这里默认顶点编号从0开始,到(Graph->Nv - 1) */
    for(V = 0; V < Graph->Nv; V++){
    	Graph->G[V].FirstEdge = NULL;
	}
	return Graph;
}

void InsertEdge(LGraph Graph, Edge E){
	PtrToAdjVNode NewNode;
    /* 插入边 <V1, V2> */
    /* 为V2建立新的邻接点 */
    NewNode = (PtrToAdjVNode)malloc(sizeof(struct AdjVNode));
    NewNode->AdjV = E->V2;
    NewNode->Weight = E->Weight;
    /* 将V2插入V1的表头 */
    NewNode->Next = Graph->G[E->V1].FirstEdge;
    Graph->G[E->V2].FirstEdge = NewNode;
}

LGraph BuildGraph(){
	LGraph Graph;
	Edge E;
	Vertex V;
	int Nv, i;
	/* 读入顶点个数 */
	scanf("%d", &Nv);
	/* 初始化有Nv个顶点但没有边的图 */
	Graph = CreateGraph(Nv);
	/* 读入边数 */
	scanf("%d", &(Graph->Ne));
	/* 如果有边 */
	if(Graph->Ne != 0){
		/* 建立边结点 */
		E = (Edge)malloc(sizeof(struct ENode));
		/* 读入边,格式为"起点 终点 权重",插入邻接矩阵 */
		for(i = 0; i < Graph->Ne; i++){
			scanf("%d %d %d",&E->V1, &E->V2, &E->Weight);
			/* 注意:如果权重不是整型,Weight的读入格式要改 */
			InsertEdge(Graph, E);
		}
	}
	/* 如果顶点有数据的话,读入数据 */
	for(V = 0; V < Graph->Nv; V++){
		scanf("%c", &(Graph->G[V].Data));
	}
	return Graph;
}

标题:(数据结构)第六讲-图(6.1什么是图)
作者:yazong
地址:https://blog.llyweb.com/articles/2023/10/22/1697956273535.html