图的存储结构

图在内存中存储方式有很多种,最经典的包括邻接矩阵、邻接表、逆邻接表和十字链表。
图的存储结构相比较线性表与树来说就复杂很多。
回顾⼀下之前学过的,对于线性表来说,是⼀对⼀的关系,所以⽤数组或者链表均可简单存放。 树是⼀对多的关系,所以我们要将数组和链表的特性结合在⼀起才能更好的存放。
那么图,是多对多的情况,图上的任何⼀个顶点都可以被看作是第⼀个顶点,任⼀顶点的邻接点之
间也不存在次序关系。
因为任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间
的关系(内存物理位置是线性的,图的元素关系是平⾯的)。如果用多重链表来描述倒是可以做到,但是纯粹⽤多重链表导致的浪费是无法想像的(如果各个顶点的度数相差太大,就会造成巨⼤的浪费)。
1. 邻接矩阵
图的邻接矩阵是用两个数组来表示,⼀个⼀维数组存储图中的顶点信息,⼀个⼆维数组(我们将这
个数组称之为邻接矩阵)存储图中的边的信息。
1.1 无向图的邻接矩阵
我们可以设置两个数组,顶点数组为vertex[4]={V0,V1,V2,V3},边数组arc[4] [4]这个二维数组为对称矩阵(0表示不存在顶点间的边,1表示顶点间存在边)。

 有了这个二维数组的对称矩阵,我们可以很容易从图中获取如下信息:

判定任意两顶点是否有边无边;
可以轻松知道某个顶点的度,其实就是这个顶点Vi在邻接矩阵中第i⾏(或第i列)的元素之和;
求顶点Vi的所有邻接点就是将矩阵中第i行元素扫描⼀遍, arc[i] [j]为1就是邻接点。

因为无向图的邻接矩阵是一个对称矩阵,因此实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素。

1.2 有向图的邻接矩阵

 如图所示,顶点数组为vertex[4]={V0,V1,V2,V3},弧数组arc[4] [4]是⼀个矩阵,这个矩阵的有向图并不对称,例如由V0到V3有弧,得到arc[0] [3]=1,⽽V3到V0没有弧,因此arc[3] [0]=0。

入度:Vi的入度正好是Vi列的各数之和

出度:Vi的出度为Vi行的各数之和

1.3 带权图的邻接矩阵
带权图中的每⼀条边上带有权值,邻接矩阵中的值则为权值。当两个顶点之间没有边时,则用无穷大表示。

 代码:

构建下面这个无向图

 

base.h

#ifndef BASE_H
#define BASE_H
#define MaxNodeNum	10  //规定最大的顶点个数
#define INF 1E4         //规定无穷大值(这个值比图中所有边都大即可)
#endif

matrixGraph.h

#ifndef MATRIX_GRAPH_H
#define MATRIX_GRAPH_H
/* 图的邻接矩阵存储方式
 * - 描述顶点集合、边的集合
 * - 顶点用一维数组描述,边用二维数组表示
 * */
#include "../base.h"
// 邻接矩阵图的顶点结构(定义一个顶点)
typedef struct {
	int no;			// 顶点的编号(索引)
	char *show;		// 图中顶点的显示数据(比如V1,V2,V3),指针指向了一个常量空间,考试时可以不用写
}MatrixVertex;

// 邻接矩阵边的类型,用int来描述,即可以描述权值和是否有边(定义一个边)
typedef int MatrixEdge;

// 邻接矩阵表示的图
typedef struct {
	MatrixVertex vex[MaxNodeNum];			// 存储顶点的信息
	int	nodeNum;							// 约束实际顶点的数量,邻接矩阵遍历时的最大值
	MatrixEdge edges[MaxNodeNum][MaxNodeNum];		// 邻接矩阵定义边的情况
	int edgeNum;							// 定义边的个数
	int directed;							// 判断是否为有向图(比如值为1时有向,值为0时无向)
}MGraph;//MGraph表示邻接矩阵

/* 邻接矩阵图的初始化,先初始化顶点集
 * num		:  顶点的个数
 * names	:  顶点显示的字符串,以字符指针来保存,上层空间的值有效
 * directed :  是否为有向图
 * edgeValue:  初始化边的权值
 * */
void initMGraph(MGraph *g, int num, char *names[], int directed, int edgeValue);
/* 初始化顶点之后,添加边的信息
 * x		:	起始顶点编号
 * y		:	终止顶点编号
 * w		:	该边的权值
 * */
void addMGraphEdge(MGraph *g, int x, int y, int w);

#endif

matrixGraph.c

#include <string.h>
#include "matrixGraph.h"

//判断是否有边
int isEdge(int weight) {
    //边的权值大于0小于无穷大,即有边
	if (weight > 0 && weight < INF)
		return 1;
	return 0;
}

//初始化图
void initMGraph(MGraph *g, int num, char **names, int directed, int edgeValue) {
	g->directed = directed;   //图是否有向
	g->edgeNum = 0;           //边的数量为0
	g->nodeNum = num;         //顶点数
	memset(g->vex, 0, sizeof(g->vex));  //用memset初始化顶点数组
	memset(g->edges, 0, sizeof(MatrixEdge) * MaxNodeNum * MaxNodeNum); //初始化边的数组
	// 初始化顶点
	for (int i = 0; i < num; ++i) {
		g->vex[i].no = i; //顶点编号(索引),从0开始
		g->vex[i].show = names[i]; //顶点显示
		for (int j = 0; j < num; ++j) {
			g->edges[i][j] = edgeValue;
		}
	}
}
// 简单图 不能有自环 不能有重边
void addMGraphEdge(MGraph *g, int x, int y, int w) {
    //判断下标x,y是否越过边界
	if (x < 0 || x > g->nodeNum)
		return;
	if (y < 0 || y > g->nodeNum)
		return;
    //不能有重边,故两个顶点之间没有边时,才可以添加
	if (!isEdge(g->edges[x][y])) {
		g->edges[x][y] = w;
        //判断是否为无向图,若为无向图,需要再对[y][x]赋值
		if (g->directed == 0) {
			g->edges[y][x] = w;
		}
		g->edgeNum++; //边的个数加1
	}
}

main.c

#include <stdio.h>
#include "matrixGraph.h"

//定义一个函数,构建下图
static void setupMatrixGraph(MGraph *g1) {
	char *nodeNames[] = {"V1", "V2", "V3", "V4",
						 "V5", "V6", "V7", "8"};  //构造所有顶点的显示行为
	initMGraph(g1, 8, nodeNames, 0, 0);           //调用初始化函数,g1图,8个顶点,顶点的显示形式,无向图,边无权值
	addMGraphEdge(g1, 0, 1, 1);                   //调用添加边的函数,向g1图中添加边(V1,V2),其中V1的索引为0,V2的索引为1,图是无权图,故边权值为1
	addMGraphEdge(g1, 0, 2, 1);
	addMGraphEdge(g1, 1, 3, 1);
	addMGraphEdge(g1, 1, 4, 1);
	addMGraphEdge(g1, 2, 5, 1);
	addMGraphEdge(g1, 2, 6, 1);
	addMGraphEdge(g1, 3, 7, 1);
	addMGraphEdge(g1, 4, 7, 1);
	addMGraphEdge(g1, 5, 6, 1);
}

int main() {
	MGraph g1;
	setupMatrixGraph(&g1);                  //调用图g1的函数

	printf("have %d num!\n", g1.edgeNum);   //g1图有多少条边
	return 0;
}

运行结果正确,图g1共有9条边

2. 邻接表

使用邻接矩阵存储的时候我们会发现⼀个问题,就是空间浪费问题。尤其是面对边数相对较少的稀疏图来说,这种结构无疑是存在对存储空间的极大浪费。
因此我们可以考虑另外⼀种存储结构方式,例如把数组与链表结合在⼀起来存储,这种方式在图结 构也适用 ,我们称为邻接表(Adjacency List)。
邻接表的处理方法是这样:
图中顶点用⼀个⼀维数组存储,可以较容易地读取顶点信息,方便。
图中每个顶点Vi的所有邻接点构成⼀个线性表,由于邻接点的个数不确定,所以我们选择用单链表来存储。

 2.1 无向图邻接表

 用邻接表存储无向图时,对一条边(V0,V1),需要在结点V0的点链表和结点V1的单链表中都要存储。如果要添加一条边或者删除一条边时,需要对边的两个结点所对应的单链表都做出相应的修改。比如删掉边(V2,V3),则需要将V2和V3所对应的单链表中的(V2,V3)边都删去。用邻接表存储无向图无疑是自找麻烦,所以无向图一般都用邻接矩阵存储。

2.2 有向图邻接表
对于有向图,我们先来看下把顶点当弧尾建立的邻接表,这样很容易就可以得到 每个顶点的出度
找到V0结点之后,对V0的链表计数,就可以知道V0的出度。但如果要得到V0的入度,需要对所有的结点和其对应的边表遍历

 为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建⽴⼀个有向图的逆邻接表(顶点作为弧头):如下图所示,可以很容易算得每个顶点的入度

 2.3 带权网络的邻接表

代码:

 base.h

#ifndef BASE_H
#define BASE_H
#define MaxNodeNum	10
#define INF 1E4
#endif

adjacentList.h

#ifndef ADJACENT_LIST_H
#define ADJACENT_LIST_H
/* 图的邻接表,在节点集合中,增加指向边的指针
 * 边节点里包含了下一个和首节点连接的边
 * */
#include "../base.h"

// 边的结构
typedef struct arcEdge{
	int no;					// 顶点指向的其他节点的编号
	int weight;				// 边的权重
	struct arcEdge *next;	// 顶点指向的下一条边
}ArcEdge;

// 顶点结构
typedef struct {
	int no;					// 顶点的编号
	char *show;				// 顶点显示内容
	ArcEdge *firstEdge;		// 当前的顶点指向的边
}ArcNode;

// 使用邻接表描述的图
typedef struct {
	ArcNode *nodes;			// 图中顶点的集合
	int nodeNum;			// 图中顶点的个数
	int edgeNum;			// 图中边的个数
	int directed;			// 是否有向
}AGraph;

// 产生n个节点的邻接表的图
AGraph *createAGraph(int n);

void releaseAGraph(AGraph *graph);

/* 初始化邻接表的图
 * */
void initAGraph(AGraph *graph, int num, char *names[], int directed);

void addAGraphEdge(AGraph *graph, int x, int y, int w);

#endif

adjacentList.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "adjacentList.h"

//产生n个节点的邻接表的图
AGraph *createAGraph(int n) {
	AGraph *graph = (AGraph *) malloc(sizeof(AGraph)); //申请空间
	if (graph == NULL) {
		fprintf(stderr, "malloc failed!\n");
		return NULL;
	}
	graph->edgeNum = 0; //边的个数
	graph->nodeNum = n; //顶点个数
	graph->nodes = (ArcNode *) malloc(sizeof(ArcNode) * n); //申请表头
	if (graph->nodes == NULL) {
		fprintf(stderr, "malloc node failed!\n");
		free(graph); //申请失败注意要释放之前申请的图
		return NULL;
	}
	// 初始化链表
	memset(graph->nodes, 0, sizeof(ArcNode) * n); //初始化头
	return graph;
}

//释放
void releaseAGraph(AGraph *graph) {
	ArcEdge *tmp; //申请临时指针
	int count = 0; //计数
	if (graph) {
		for (int i = 0; i < graph->nodeNum; ++i) {	// 遍历每一个节点
			ArcEdge *edge = graph->nodes[i].firstEdge;
			while (edge) {
                //备份思想
				tmp = edge;
				edge = edge->next;
				free(tmp);
				count++;
			}
		}
		printf("release %d edges!\n", count);
	}
}

//初始化图
void initAGraph(AGraph *graph, int num, char **names, int directed) {
	graph->directed = directed;
	for (int i = 0; i < num; ++i) {			// 为数组空间的num个顶点进行初始化
		graph->nodes[i].no = i;
		graph->nodes[i].show = names[i];
		graph->nodes[i].firstEdge = NULL;   //初始时边为空
	}
}

//static静态方法产生一个边
static ArcEdge *createArcEdge(int y, int w) {
	ArcEdge *edge = (ArcEdge *) malloc(sizeof(ArcEdge)); //申请边空间
	edge->no = y;      
	edge->weight = w; //初始化权重
	edge->next = NULL; //刚开始为空
	return edge;
}

//添加边
void addAGraphEdge(AGraph *graph, int x, int y, int w) {
    //判断下标是否在规定范围内
	if (x < 0 || x >= graph->nodeNum || y < 0 || y >= graph->nodeNum)
		return;
	// 边节点采用头插法
	if (x == y)
		return;

	ArcEdge *edge = createArcEdge(y, w); //调用静态方法
    //先处理新节点,再处理老节点
	edge->next = graph->nodes[x].firstEdge;
	graph->nodes[x].firstEdge = edge;
	graph->edgeNum++; //边的个数加一
	if (graph->directed == 0) {		// 不是自环边(即x!=y),并且是无向图
		edge = createArcEdge(x, w); //无向图还得再创建一个边,下面同理
		edge->next = graph->nodes[y].firstEdge;
		graph->nodes[y].firstEdge = edge;
		graph->edgeNum++;
	}
}

main.c

#include "adjacentList.h"
#include <stdio.h>

static void setupGraph(AGraph *graph) {
	char *nodeNames[] = {"A", "B", "C", "D", "E"}; //顶点的表示
	initAGraph(graph, sizeof(nodeNames) / sizeof(nodeNames[0]),nodeNames, 1); //初始化图
	addAGraphEdge(graph, 0, 4, 1);
	addAGraphEdge(graph, 0, 3, 1);
	addAGraphEdge(graph, 0, 1, 1);
	addAGraphEdge(graph, 1, 4, 1);
	addAGraphEdge(graph, 1, 2, 1);
	addAGraphEdge(graph, 2, 0, 1);
	addAGraphEdge(graph, 3, 2, 1);
}

int main() {
	int n = 5;
	AGraph *graph = createAGraph(n);
	setupGraph(graph);
	printf("边数: %d\n", graph->edgeNum);
	releaseAGraph(graph);
	return 0;
}

运行结果无误

3. 十字链表

邻接表固然优秀,但也有不⾜,例如对有向图的处理上,有时候需要再建⽴⼀个逆邻接表
这个时候,我们可以想⼀下,我们是否可以把邻接表和逆邻接表结合起来呢?
这个就是十字链表。

十字链表的好处就是因为把邻接表和逆邻接表整合在了⼀起,这样既容易找到以Vi为尾的弧,也容
易找到以Vi为头的弧,因⽽容易求得顶点的出度和⼊度。
十字链表除了结构复杂⼀点外,其实创建图算法的时间复杂度是和邻接表相同的,因此,在有向图
的应用中,十字链表也是非常好的数据结构模型。
4. 邻接多重表
邻接表对边的操作显然很不⽅便,因此,我们可以仿照⼗字链表的⽅式,对边表结构进⾏改装,重
新定义的边表结构如下:
其中iVex和jVex是与某条边依附的两个顶点在顶点表中的下标。 iLink指向依附顶点iVex的下⼀条
边, jLink指向依附顶点jVex的下⼀条边。
5. 边集数组
边集数组是由两个⼀维数组构成,⼀个是存储顶点的信息,另⼀个是存储边的信息,这个边数组每
个数据元素由⼀条边的起点下标(begin)、终点下标(end)和权(weight)组成。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值