图的存储结构

1,037 阅读5分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

图的存储结构

1 邻接矩阵

1.1 有向图和无向图

图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。设图中有n个顶点,则邻接矩阵是一个n×nn\times n的方阵,定义为:

arc[i][j]={1,(vi,vj)E<vi,vj>E0,其他arc[i][j]=\begin{cases}1,若(v_i,v_j)\in E或<v_i,v_j>\in E\\ 0,其他 \end{cases}

 无向图实例:

image-20220910195754692.png

 设置两个数组,顶点数组vertex[4]=v0,v1,v2,v3vertex[4]={v_0,v_1,v_2,v_3},边数组arc[4][4]arc[4][4]arc[0][1]=1arc[0][1]=1表示v0v_0v1v_1的边存在,arc[1][3]=0arc[1][3]=0表示v1v_1v3v_3的边不存在。

 有向图实例:

image-20220910200112988.png

 设置两个数组,顶点数组vertex[4]=v0,v1,v2,v3vertex[4]={v_0,v_1,v_2,v_3},边数组arc[4][4]arc[4][4]arc[1][0]=1arc[1][0]=1表示v1v_1v0v_0有弧,arc[0][1]=0arc[0][1]=0表示v0v_0v1v_1没有弧。

 在有向图中有入度和出度的概念,顶点v1v_1的入度为1,正是第v1v_1列各数的和。顶点v1v_1的出度为2,即第viv_i行各数之和。

1.2 网图

 每条边上带有权的图叫做网,假设该图有n个顶点,则邻接矩阵是一个n×nn\times n的方阵,定义为:

arc[i][j]={Wij,(vi,vj)E<vi,vj>E0,i=j,其他arc[i][j]=\begin{cases}W_{ij},若(v_i,v_j)\in E或<v_i,v_j> \in E\\ 0,若i=j \\ \infty,其他 \end{cases}

 这里的w0w_0表示(vi,vj) (v_i,v_j)<vi,vj> <v_i,v_j>上的权值,根据此定义,我们可作出网的邻接矩阵。

image-20220910202152248.png

1.3 邻接矩阵的代码实现

图的邻接矩阵存储结构如下:

typedef char VertexType;  // 顶点类型
typedef int EdgeType;  // 边上的权值类型
#define MAXVEX 100  // 最大顶点数
#define INFINITY 65535 // 无穷用65535代替
typedef struct{
    VertexType vexs[MAXVEX];  // 顶点表
    EdgeType arc[MAXVEX][MAXVEX];  // 邻接矩阵,边表
    int numNodes,numEdges;  // 图中当前的顶点数和边数
}MGraph;

无向网图的创建:

/* 建立无向网图的邻接矩阵表示 */
void CreateMGraph(MGraph *G)
{
	int i,j,k,w;
	printf("输入顶点数和边数:\n");
	scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
	for(i = 0;i <G->numNodes;i++) /* 读入顶点信息,建立顶点表 */
		scanf(&G->vexs[i]);
	for(i = 0;i <G->numNodes;i++)
		for(j = 0;j <G->numNodes;j++)
			G->arc[i][j]=GRAPH_INFINITY;	/* 邻接矩阵初始化 */
	for(k = 0;k <G->numEdges;k++) /* 读入numEdges条边,建立邻接矩阵 */
	{
		printf("输入边(vi,vj)上的下标i,下标j和权w:\n");
		scanf("%d,%d,%d",&i,&j,&w); /* 输入边(vi,vj)上的权w */
		G->arc[i][j]=w; 
		G->arc[j][i]= G->arc[i][j]; /* 因为是无向图,矩阵对称 */
	}
}

2 邻接表

2.1 邻接表存储结构

邻接矩阵对于边数相对顶点较少的图是存在对存储空间的极大浪费。我们可以对边或弧使用链式存储的方式来避免空间浪费的问题。将数组与链表相结合的存储方法称作邻接表。

邻接表的处理方法:

  1. 图中顶点用一个一维数组存储。对于顶点数组中,每个数据元素还需要存储指向第一个邻接表的指针,以便于查找该顶点的边信息。
  2. 图中每个顶点viv_i的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点viv_i的边表,有向图则称为顶点viv_i作为弧尾的出边表。

image-20220910205943765.png

对于带权值的网图,可以再边表结点定义中再增加一个weight的数据域,存储权值信息,如图所示。

image-20220910210246131.png

2.2 邻接表代码实现

图的邻接表存储结构如下:

typedef char VertexType;  // 顶点类型
typedef int EdgeType;  // 边上的权值类型

typedef struct EdgeNode /* 边表结点  */
{
	int adjvex;    /* 邻接点域,存储该顶点对应的下标 */
	EdgeType info;		/* 用于存储权值,对于非网图可以不需要 */
	struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;

typedef struct VertexNode /* 顶点表结点 */
{
	VertexType data; /* 顶点域,存储顶点信息 */
	EdgeNode *firstedge;/* 边表头指针 */
}VertexNode, AdjList[MAXVEX];

typedef struct
{
	AdjList adjList; 
	int numNodes,numEdges; /* 图中当前顶点数和边数 */
}GraphAdjList;

无向图的邻接表创建代码:

/* 建立图的邻接表结构 */
void  CreateALGraph(GraphAdjList *G)
{
	int i,j,k;
	EdgeNode *e;
	printf("输入顶点数和边数:\n");
	scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
	for(i = 0;i < G->numNodes;i++) /* 读入顶点信息,建立顶点表 */
	{
		scanf(&G->adjList[i].data); 	/* 输入顶点信息 */
		G->adjList[i].firstedge=NULL; 	/* 将边表置为空表 */
	}
	
	
	for(k = 0;k < G->numEdges;k++)/* 建立边表 */
	{
		printf("输入边(vi,vj)上的顶点序号:\n");
		scanf("%d,%d",&i,&j); /* 输入边(vi,vj)上的顶点序号 */
		e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
		e->adjvex=j;					/* 邻接序号为j */                         
		e->next=G->adjList[i].firstedge;	/* 将e的指针指向当前顶点上指向的结点 */
		G->adjList[i].firstedge=e;		/* 将当前顶点的指针指向e */               
		
		e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
		e->adjvex=i;					/* 邻接序号为i */                         
		e->next=G->adjList[j].firstedge;	/* 将e的指针指向当前顶点上指向的结点 */
		G->adjList[j].firstedge=e;		/* 将当前顶点的指针指向e */               
	}
}

3 十字链表

将邻接表与逆邻接表结合起来叫做十字链表,用于解决邻接表出度和入度的问题。

重新定义顶点表顶点结构如图所示:

image-20220910210830671.png

 firstin表示入边表头指针,指向该顶点的出边表中的第一个结点;firstout表示出边表头指针,指向该顶点的出边表中的第一个结点。

重新定义的边表结点结构如图所示:

image-20220910211053594.png

 tailvex是指弧起点在顶点表中的下标;headvex是指弧终点在顶点表中的下标;headlink是指入边表指针域,指向终点相同的下—条边;taillink是指边表指针域,指向起点相同的下一条边。如果是网还可以再增加—个weight域来存储权值。 、

实例如下:

image-20220910211613406.png

十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到以viv_i为尾的弧,也容易找到以viv_i为头的弧,因而容易求得顶点的出度和入度。而且它除了结构复杂一点外,其实创建图算法的时间复杂度是和邻接表相同的。因此,在有向图的应用中,十字链表是非常好的数据结构模型。