图的邻接矩阵表示法&邻接表表示法

图数据结构的邻接矩阵与邻接表表示法解析
本文介绍了图数据结构的两种表示方法:邻接矩阵和邻接表。邻接矩阵使用二维数组表示顶点之间的关系,适合检查边的存在和计算顶点度,但对稀疏图存在空间浪费。而邻接表通过链表集合节省空间,尤其适用于稀疏图,但完全图使用邻接矩阵更为高效。

图表示是一种多对多的关系的数据结构。因为线性表表示的是一种一对一的关系的数据结构,树表示的是一种一对多的数据结构,所以图把线性表和树都包含在内。图由一个非空的有限顶点集合和一个有限边集合组成。当我们描述图的时候,一定要包含以下两个元素:

1、一组顶点:例如用Vertex表示顶点的集合。

2、一组边:用Edge表示边的集合,一条边是一组顶点对。

      •无向边 :(ij)∈EdgeijVertex。即双向的,即可从i走到j,也可以从j走到i

•有向边 :< ij >EdgeijVertex。即单向的(单行线),只可从i走到j或从j走到i

•图不考虑重边和自己连自己的情况。

初始化一个图是,可以一条边也没有,但是不能一个顶点也没有。

那么怎么用代码表示一个图?

第一种方法是邻接矩阵表示法:即用矩阵,一个二维数组来表示一个图。

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

#define MaxVertex 100  /*最大顶点数*/
#define INFINITY 65535 /*双字节无符号整数的最大值*/
typedef int Vertex; /*二维数组的下标表示顶点,为整型*/
typedef int WeightType; /*边的权值的类型*/ 
typedef char DataType; /*顶点存储的数据类型为字符型*/

/*图的数据结构*/
typedef struct GraphNode *PtrlGraphNode;
struct GraphNode {
	int numv; /*顶点数*/ 
	int nume; /*边数*/ 
	WeightType wt[MaxVertex][MaxVertex]; /*邻接矩阵*/
	DataType Data[MaxVertex]; /*存放顶点的数据*/
	/*很多情况下,顶点无数据,此时Data[]可以不用*/
};
typedef PtrlGraphNode Graph;/*用邻接矩阵存储的图*/

用一个二维数组来表示图中一组顶点对的关系,还有一个一维数组来存放顶点的数据。

/*边的数据结构*/
typedef struct EdgeNode *PtrlEdgeNode;
struct EdgeNode {
	Vertex V1, V2; /*有向边<V1, V2>*//*无向边(V1, V2)*/
	WeightType weight; /*权重*/
};
typedef PtrlEdgeNode Edge;

边的数据结构,一条边由一组顶点对和权重表示(即顶点对的关系)。

表示图的各个数据结构都建立好后,就开始建立一个图,思路是先创建一个有全部顶点但没有边的图,在逐条插入边。

/*建立一个有VertexNum个顶点但没有边的空图*/
Graph CreatGraph(int VertexNum) 
{
	Vertex i,j;
	Graph MyGraph;
	
	MyGraph=(Graph)malloc(sizeof(struct GraphNode));/*申请空间建立图*/
	MyGraph->numv=VertexNum;
	MyGraph->nume=0; //初始化边数为0; 
	
	/*对二维数组矩阵初始化*/
	for (i=0;i<MyGraph->numv;i++) {
		for (j=0;j<MyGraph->numv;j++) {
			MyGraph->wt[i][j]=INFINITY;
		}
	}
	return MyGraph;
}

CreatGraph函数中输入一个值来创建有Vertexnum个结点的图,然后初始化二维数组中的元素为无穷(或0)。

/*插入边*/
void InsertEdge(Graph MyGraph, Edge MyEdge) 
{
	/*插入边<V1, V2>*/
	MyGraph->wt[MyEdge->V1][MyEdge->V2]=MyEdge->weight;
	
	/*如果是无向图,还要加上插入边<V2, V1>*/
	MyGraph->wt[MyEdge->V2][MyEdge->V1]=MyEdge->weight;
}

然后插入边,把边的V1V2,也就是两个顶点(对应二维数组的两个下标),以及权值插入到对应二维数组的位置。

Graph BuildGraph() 
{
	Graph MyGraph; /*建图*/
	Edge MyEdge; /*建边*/ 
	Vertex v;
	int VertexNum,i;
	
	printf("请输入顶点个数");
	scanf("%d", &VertexNum); 
	MyGraph=CreatGraph(VertexNum);/*建立一个有numv个顶点,没有边的图*/
	
	printf("请输入边数:");
	scanf("%d", &MyGraph->nume);   /* 读入边数 */
	MyEdge=(Edge)malloc(sizeof(struct EdgeNode));/*建立边结点*/
	/*插入边:格式为:"起点->终点->权重"。插入到邻接矩阵*/ 
	for (i=0;i<MyGraph->nume;i++) {
		scanf("%d %d %d",&MyEdge->V1, &MyEdge->V2, &MyEdge->weight);
		InsertEdge(MyGraph, MyEdge);
	}
	
	/*如果顶点有数据,就读入数据*/
	for (i=0;i<MyGraph->numv;i++) {
		scanf("%c", &MyGraph->Data[i]);
	}
	
	return MyGraph;
}

Graph[ n ][ n ]二维数组中,用n个顶点从0n-1编号。如果一组顶点对<  ij  >是图Graph中的边的话,就令Graph[ i ][ j ]=1,否则等于0

用邻接矩阵表示法表示出来的矩阵图,有以下特点:

1、矩阵上的对角线全都是0。因为图中的顶点对不会出现自己连自己的情况,所以对于矩阵图中Graph[ n ][ n ]一定是等于0

2、邻接矩阵表示法表示出来的矩阵无向图,一定是沿对角线对称的,也就是说一定是一个对称矩阵。这是因为,假设图中结点12是有一条边的,所以21之间也是有一条边的,所以Graph[ 1 ][ 2 ]Graph[ 2 ][ 1 ]一定是相等都等于1的。


用矩阵的方式,好处是容易检查某条边(顶点对)是否存在,容易找出一个顶点的邻接点,方便计算顶点的度。不过矩阵表示的不好在于,对于稀疏图来说,邻接矩阵的方式会浪费空间,因为稀疏图即边很少,也就是二维数组里很多元素都是0。矩阵在稀疏图的情况下浪费空间也浪费时间,比如我想统计稀疏图里一共有多少条边,把二维数组遍历一遍,统计元素1有多少个,问题是1很少,所以造成很多对0的遍历,是浪费时间的。

那么如何解决邻接矩阵浪费空间和时间的问题?我们知道线性表,线性表顺序存储需要实现分配好内存,这样有可能造成空间浪费或空间不够用,于是就有了链表的方法,同样图的表示方法也可以用链表的方式,就是邻接表表示法。

邻接表表示法,即用一个链表的集合,一个一维指针数组,依然用数组下标表示顶点。所以数组的大小就是图中顶点的个数。

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

#define MaxVertex 100  /*最大顶点数*/
typedef int Vertex; /*顶点的下标表示顶点*/ 
typedef int WeightType; /*边的权值类型为整型*/
typedef char DataType; /*顶点存储的数据的类型*/

/*边的数据结构*/
typedef struct EdgeNode *PtrlEdgeNode;
struct EdgeNode {
	Vertex V1, V2;  /* 向边<V1, V2>*/
	WeightType weight; /*权重*/
};
typedef PtrlEdgeNode Edge;

/*邻接点的数据结构*/ 
typedef struct PointNode *PtrlPointNode;
struct PointNode {
	Vertex index; /*邻接点的下标*/
	WeightType weight; /*边的权重*/
	PtrlPointNode Next; /*指向下一个邻接点的指针*/
};

/*顶点表头结点的数据结构*/
typedef struct HeadNode {
	PtrlPointNode HeadEdge; /*边的表头结点指针*/
	DataType Data; /*顶点的数据*/
} PointList[MaxVertex]; /*邻接表类型*/

/*图结点的数据结构*/ 
typedef struct GraphNode *PtrlGraphNode;
struct GraphNode {
	int numv; /*顶点数*/
	int nume; /*边数*/
	PointList PL; /*邻接表*/
};
typedef PtrlGraphNode  ListGraphNode; /*邻接表方式存储图*/

2629行,顶点的表头就是一个结构体指针数组。指针类型是第1822行的邻接点的数据结构。

/*创建一个有Vertexnum个顶点但没有边的图*/
ListGraphNode CreatGraph(int Vertexnum) 
{
	Vertex i;
	ListGraphNode MyGraph;
	
	MyGraph=(ListGraphNode)malloc(sizeof(struct GraphNode));
	MyGraph->nume=0;
	MyGraph->numv=Vertexnum;
	
	for (i=0;i<MyGraph->numv;i++) {
		MyGraph->PL[i].HeadEdge=NULL; //初始化数组里各个头结点 
	}
	return MyGraph;
}
/*插入边*/
void InsertEdge(ListGraphNode MyGraph, Edge E) 
{
	PtrlPointNode Node; /*邻接点*/
	/*插入边<V1, V2>*/
	/*为 V2建立新的邻接点*/ 
	Node=(PtrlPointNode)malloc(sizeof(struct PointNode));
	Node->index=E->V2; /*将V2插入V1的表头*/
	Node->weight=E->weight;
	Node->Next=MyGraph->PL[E->V1].HeadEdge;
	MyGraph->PL[E->V1].HeadEdge=Node;
	
	/*如果是无向图,还要插入边<V2, V1>*/
	Node=(PtrlPointNode)malloc(sizeof(struct PointNode));
	Node->index=E->V1; /*将V2插入V1的表头*/
	Node->weight=E->weight;
	Node->Next=MyGraph->PL[E->V2].HeadEdge;
	MyGraph->PL[E->V2].HeadEdge=Node;
}
/*创建图*/
ListGraphNode BuildGraph()
{
	ListGraphNode MyGraph;
	Edge E;
	Vertex i,j,numv;
	
	printf("请输入要读入的顶点个数:");
	scanf("%d", &numv);
	MyGraph=CreatGraph(numv);
	
	printf("请输入要读入的边数:");
	scanf("%d", &MyGraph->nume);
	if (MyGraph->nume!=0) {
		E=(Edge)malloc(sizeof(struct EdgeNode)); /*建立边结点*/
		/*读入边:格式为"起点 终点 权重"*/
		for (i=0;i<MyGraph->numv;i++) {
			scanf("%d %d %d", &E->V1, &E->V2, &E->weight);
			/*插入边到图里*/
			InsertEdge(MyGraph, E);
		}
	}
	
	/*如果顶点有数据的话,读入数据*/
	for (i=0;i<MyGraph->numv;i++) {
		scanf("%c", &MyGraph->PL[i].Data);
	}
	return MyGraph;
}

但是对于邻接表表示法来说,一条边必定是被存了两次的,例如顶点对< 5, 9 >,在指针数组5的链表里必定有9,在指针数组9的链表里也必定有5。所以用邻接表的话,表示的图要是稀疏图才算省空间,越稀疏越好。而对于完全图来说,用邻接矩阵的方式更方便后面的操作。

### 邻接矩阵邻接表的异同点及使用场景 #### 定义与基本特性 邻接矩阵是一种通过二维数组的方式,其中每个元素 \(a_{ij}\) 示顶点 \(v_i\) 和 \(v_j\) 是否存在边连接[^1]。对于无向而言,其邻接矩阵是对称的,因为如果 \(v_i\) 连接到 \(v_j\),那么 \(v_j\) 也必然连回到 \(v_i\)。而有向则不具有这种对称性质。 相比之下,邻接表采用链式数据结构来存储的信息。具体来说,每一个顶点对应一个单链,该链记录了与此顶点相邻的所有其他顶点。 --- #### 存储空间对比 - **邻接矩阵**:由于它是一个完整的 \(n \times n\) 的方阵(\(n\) 是顶点数量),即使稀疏也需要占用大量的内存资源。因此,在处理大规模稀疏时效率较低。 - **邻接表**:仅需保存实际存在的边信息,故当面对稀疏时更加节省空间。特别是当边的数量远小于可能的最大值 (\(O(n^2)\)) 时优势明显。 --- #### 查询操作性能分析 - 对于**邻接矩阵**: - 判断两点间是否存在一条直接相连的路径非常高效,只需访问对应的矩阵位置即可完成判定【时间复杂度为 O(1)】。 - 计算某一点的度数也很简单&mdash;&mdash;只需要累加相应行或者列上的数值总和即可得到结果。 - 使用**邻接表**的情况下: - 查找特定两条节点之间是否有连线相对较慢一些,因为它涉及到遍历整个列直到找到目标为止 【平均情况下时间为 O(k),k代当前结点关联的其它结点数目】; - 不过获取某个给定节点的所有邻居变得极为迅速,因为我们只要读取那个节点所指向的那个单独链条就可以了。 --- #### 更新成本考量 无论是增加还是删除边缘或顶点, - 在**邻接矩阵**里修改较为繁琐,尤其是增删行列的时候会牵扯到重新分配更大的连续区块并复制旧的数据过去等问题。 - 反观**邻接表**形式下的变动过程就要轻松得多,通常只涉及简单的指针调整或是新对象创建销毁动作而已。 --- #### 应用场合建议 基于以上讨论可以得出如下结论: | 特性/方法 | 邻接矩阵 | 邻接表 | |-----------|---------------------------------------|--------------------------------------| | 空间需求 | 较高 | 更低 | | 插入删除速度 | 缓慢 | 快速 | | 边查询 | 极快 (O(1)) | 中等(O(min(d, V))) | 所以一般推荐规则如下: - 如果是稠密型或者是频繁执行随机存取的任务环境之下更适合选用邻接矩阵方案; - 当遇到大型稀疏网络并且更关注动态维护能力的应用领域,则应该优先考虑利用邻接表实现策略. ```python # Python 实现例子展示两者差异 class AdjacencyMatrixGraph: def __init__(self, num_vertices): self.matrix = [[0]*num_vertices for _ in range(num_vertices)] def add_edge(self,u,v): self.matrix[u][v]=1; self.matrix[v][u]=1 class AdjacencyListGraph: def __init__(self,num_vertices): self.list=[[]for i in range(num_vertices)] def add_edge(self,u,v): self.list[u].append(v); self.list[v].append(u) graph_matrix=AdjacencyMatrixGraph(5) graph_list=AdjacencyListGraph(5) print(&quot;Initial Matrix:&quot;) print(graph_matrix.matrix,&quot;\n&quot;) print(&quot;After adding edges between vertices 0 and 1:&quot;) graph_matrix.add_edge(0,1) print(graph_matrix.matrix,&quot;\n&quot;) print(&quot;Initial List:&quot;) print(graph_list.list,&quot;\n&quot;) print(&quot;After adding edges between vertices 0 and 1:&quot;) graph_list.add_edge(0,1) print(graph_list.list) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值