图的存储及遍历

本文介绍了图的四种存储方式:邻接矩阵、邻接表、十字链表和多重表,详细阐述了邻接矩阵和邻接表的存储结构及其实现。接着,通过C语言实现了无向图的邻接矩阵和邻接表创建,以及深度优先搜索(DFS)和广度优先搜索(BFS)的遍历算法。此外,提供了队列的实现以支持BFS。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.图的存储

1.介绍

图的常见存储方式有邻接矩阵存储、邻接表存储,又可扩展为十字链表存储、多重表存储。

  • 邻接矩阵存储:对于N个顶点的图,需创建一个N个空间的一维数组及N*N个空间的二维数组。一维数组负责存储每个顶点信息,二维数组则负责存储每条边的信息。以存储无向图为例,当存储有权值时,会先初始化每条边,使其为无穷,实际实现往往取一个较大值即可,然后再把Vi,Vj之间的边的权值表示在二维数组对应的第[i][j]个位置上;由于采用无向图,故邻接矩阵的特点为对称矩阵且对角线为0,若采用有向图,则对称关系被打破。
  • 邻接表存储:对N个顶点的图,邻接表存储需要创建N个空间的数组,且每个空间都会存放一个链表节点(数据域与邻边节点指针)。而邻边节点又存放了邻点下标和关于头节点的另一个邻边节点指针。整体看上去是一个数组,每个数组元素又是一个单链表。
  • 十字链表存储:较为复杂,为了解决邻接表中有向图,在求节点邻节点及头节点困难的问题,可升级为十字链表。
    十字链表对邻接表中的数组进行升级,可以同时储存出度节点及入度节点以及当前信息,同时邻节点也具有四个变量,且分为出度邻节点及入度邻节点。
    出/入度邻节点前两个变量用来存储头节点索引即当前索引,后两个变量用来存储与头节点相邻的另外的出/入度邻节点指针。

2.实现

头文件(包含遍历):

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include "dynamic queue.h"
#define MVNum 100
#define Infinite 32767
/*邻边矩阵存储*/
typedef struct {
	char vexs[MVNum];//顶点表
	int arcs[MVNum][MVNum];//邻接矩阵
	int vexnum, arcnum;//顶点及边的个数
}AMGraph;
/*查询顶点所在位置*/
int SearchSiteByMVex(char V, AMGraph* G);
/*创建无向网*/
void CreatUDGByMatrix(AMGraph* G);


/*邻接表存储*/
typedef struct ArcNode {
	int adjvex, weight;//邻节点下标、权值
	ArcNode* nextarc;//指向头节点的另一个邻节点
}ArcNode;
typedef struct VNode {
	char data;//主节点信息
	ArcNode* firstarc;//指向邻节点的指针
}VNode, ADjlist[MVNum];
typedef struct {
	ADjlist vertices;//存储各个节点信息的列表
	int vexnum, arcnum;//顶点数量、邻边数量
}ALGraph;
/*查询顶点所在位置*/
int SearchSiteByLVex(char data, ALGraph* G);
/*建立邻接表*/
void CreatUDGByList(ALGraph* G);


/*遍历图*/
/*创建一个标志数组----邻接矩阵*/
bool* CreatMVisited(AMGraph G);
/*创建一个标志数组---邻接表*/
bool* CreatLVisited(ALGraph G);
/*DFS---邻接矩阵*/
void DFS(AMGraph G, int V, bool* visited);
/*BFS---邻接表*/
void BFS(ALGraph G, int V, bool* visited);

源文件:

#include "graph.h"

/*邻边矩阵存储*/
/*查询顶点所在位置*/
int SearchSiteByMVex(char V, AMGraph* G) {
	for (int i = 0; i < G->vexnum; i++) {
		if (V == G->vexs[i])
			return i;
	}
	printf("\n顶点数组无法查询当前值,将返回-1");
	return -1;
}
/*创建无向网*/
void CreatUDGByMatrix(AMGraph* G) {
	//先输入要创建的顶点数、边数
	printf("\n输入顶点数量>> ");
	scanf("%d", &G->vexnum);
	getchar();
	printf("\n输入边的数量>> ");
	scanf("%d", &G->arcnum);
	getchar();
	//输入顶点信息
	for (int i = 0; i < G->vexnum; i++) {
		printf("\n第%3d个顶点为>> ", i);
		scanf("%c", G->vexs + i);
		getchar();
	}
	//初始化矩阵
	for (int i = 0; i < G->vexnum; i++) {
		for (int j = 0; j < G->vexnum; j++) {
			G->arcs[i][j] = Infinite;
		}
	}
	int weight;
	char V1, V2;
	//输入两个顶点的边的权值
	for (int i = 0; i < G->arcnum; i++) {
		int x = -1, y = -1;
		while (x == -1) {
			printf("\n输入顶点V1>> ");
			scanf("%c", &V1);
			getchar();
			x = SearchSiteByMVex(V1, G);
		}
		while (y == -1) {
			printf("\n输入顶点V2>> ");
			scanf("%c", &V2);
			getchar();
			y = SearchSiteByMVex(V2, G);
		}
		printf("\n输入权值>> ");
		scanf("%d", &weight);
		getchar();
		G->arcs[x][y] = weight;
		G->arcs[y][x] = weight;
	}
	//展示
	printf("\n顶点表数据>>\n");
	for (int i = 0; i < G->vexnum; i++) {
		printf("%3c", G->vexs[i]);
	}
	printf("\n邻边矩阵数据>>\n");
	for (int i = 0; i <= G->vexnum; i++) {
		if (!i)
			printf(" 索引 ");
		else
			printf("\n%-6c", G->vexs[i - 1]);//输出行索引
		for (int j = 0; j < G->vexnum; j++) {
			if (!i)
				printf("%-6c", G->vexs[j]);//输出列索引
			else
				printf("%-6d", G->arcs[i - 1][j]);
		}
	}
}


/*邻接表存储*/
/*查询顶点所在位置*/
int SearchSiteByLVex(char data, ALGraph* G) {
	for (int i = 0; i < G->vexnum; i++) {
		if (data == G->vertices[i].data)
			return i;
	}
	printf("\n顶点数组无法查询当前值,将返回-1");
	return -1;
}

/*建立邻接表*/
void CreatUDGByList(ALGraph* G) {
	//先输入要创建的顶点数、边数
	printf("\n输入顶点数量>> ");
	scanf("%d", &G->vexnum);
	getchar();
	printf("\n输入边的数量>> ");
	scanf("%d", &G->arcnum);
	getchar();
	//输入顶点信息
	for (int i = 0; i < G->vexnum; i++) {
		printf("\n第%3d个顶点为>> ", i);
		scanf("%c", &G->vertices[i].data);
		getchar();
		G->vertices[i].firstarc = NULL;
	}
	//输入两个节点的边的权值
	char V1, V2;
	for (int i = 0; i < G->arcnum; i++) {
		int x = -1, y = -1;
		while (x == -1) {
			printf("\n输入顶点V1>> ");
			scanf("%c", &V1);
			getchar();
			x = SearchSiteByLVex(V1, G);
		}
		while (y == -1) {
			printf("\n输入顶点V2>> ");
			scanf("%c", &V2);
			getchar();
			y = SearchSiteByLVex(V2, G);
		}
		ArcNode* node1 = (ArcNode*)malloc(sizeof(ArcNode));//创建两个临时节点,分别用来辅助两个有同边的节点把对方信息使用头插法存入自身firstarc中
		ArcNode* node2 = (ArcNode*)malloc(sizeof(ArcNode));
		node1->adjvex = y;//先存储另一个节点的下标
		node2->adjvex = x;
		node1->nextarc = G->vertices[x].firstarc; //采用头插法插入邻边信息
		node2->nextarc = G->vertices[y].firstarc;
		G->vertices[x].firstarc = node1;
		G->vertices[y].firstarc = node2;
		printf("\n输入权值>> ");
		int weight;
		scanf("%d", &weight);
		getchar();
		G->vertices[x].firstarc->weight = weight;//两个节点由于共有同一条边,故存储相同的权值
		G->vertices[y].firstarc->weight = weight;
	}
	//展示
	printf("\n邻接链表信息>> \n");
	for (int i = 0; i < G->vexnum; i++) {
		printf("%节点%d数据:%c", i, G->vertices[i].data);
		ArcNode* tmpnode = G->vertices[i].firstarc;
		while (tmpnode) {
			printf("  邻边权值:%d//邻点索引:%d", tmpnode->weight, tmpnode->adjvex);
			tmpnode = tmpnode->nextarc;
		}
		printf("\n");
	}
}

总体实现主要分四步骤:

邻接矩阵:

  1. 输入要实现的图的边数arcnum和顶点数vexnum,并输入各个顶点数据data
  2. 初始化邻接矩阵arcs[][],把每个元素权值设为无穷,若为无权图则设为0
  3. 输入两个顶点V1、V2的数据data,找到对应在一维数组中的下标,准备在二维数组上建立边
  4. 输入V1、V2连起来的边的权值weight

邻接表:

  1. 输入要实现的图的边数arcnum和顶点数vexnum,并输入各个顶点数据data
  2. 输入两个顶点V1、V2的数据data,找到对应在一维数组中的下标
  3. 采用单链表中的头插法对V1、V2的首边节点firstarc插入对方指针,并分别记下对方在一维数组的下标adjvex
  4. 在头插法插入的节点中输入对应边的权值

二.图的遍历

1.介绍

图的遍历又bfs广度优先遍历,dfs深度优先遍历

其中广度优先遍历类似于二叉树遍历中的层次遍历,是采用队列先把根节点旁边的都遍历一遍,在一层一层的往下扩展;而深度优先遍历则像二叉树中的栈遍历,上去先走到最底部,发现没路了再往后退退,看见旁边有路了又往最深处钻

2.实现

由于使用了队列,队列代码如下

头文件

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define QueTYPE char
#define QueCapcity 10
typedef struct DQueue {
	QueTYPE* queue;
	int top, bottom, length;
}DQueue;
/*初始化队列*/
void InitQueue(DQueue* Q);
/*入队*/
void EnterQueue(DQueue* Q, QueTYPE data);
/*出队*/
QueTYPE DepartQueue(DQueue* Q);
/*扩充队列*/
void ExpandQueue(DQueue* Q);
/*遍历队列*/
void TraverseQueue(DQueue* Q);
/*清空队列*/
void ClearQueue(DQueue* Q);

源文件

#include "dynamic queue.h"
int expandsize = 0;

/*初始化队列*/
void InitQueue(DQueue* Q) {
	Q->queue = (QueTYPE*)malloc(sizeof(QueTYPE) * QueCapcity);
	Q->top = Q->bottom = Q->length = 0;
}
/*入队*/
void EnterQueue(DQueue* Q, QueTYPE data) {
	if (Q->length < QueCapcity + expandsize) {
		Q->queue[(Q->top++) % (QueCapcity + expandsize)] = data;
		Q->length++;
	}
	else
		printf("\n队列已满,无法入队");
}
/*出队*/
QueTYPE DepartQueue(DQueue* Q) {
	if (Q->length) {
		Q->length--;
		return Q->queue[(Q->bottom++) % (QueCapcity + expandsize)];
	}
	else {
		printf("\n队列为空,无法出队");
		return '?';
	}
}
/*扩充队列*/
void ExpandQueue(DQueue* Q) {	
	do {
		printf("\n输入要扩充的数值(需为正整数)>>");
		scanf("%d", &expandsize);
		getchar();
	} while (expandsize < 0);
	Q->queue = (QueTYPE*)realloc(Q->queue, sizeof(QueTYPE) * (QueCapcity + expandsize));
}
/*遍历队列*/
void TraverseQueue(DQueue* Q) {
	printf("\n当前队列长度为>>%d", Q->length);
	for (int i = Q->bottom; i < Q->bottom + Q->length; i++) {
		printf("\n第 %d 个元素为>>%c", i - Q->bottom, Q->queue[i % (QueCapcity + expandsize)]);
	}
}
/*清空队列*/
void ClearQueue(DQueue* Q) {
	Q->length = Q->bottom = Q->top = 0;
	int choose = 0;
	printf("\n是否要释放队列:1.是 Other.否");
	scanf("%d", &choose);
	if (choose == 1)
		free(Q->queue);
}
/*Test*/
//int main() {
//	bool cycle = true;
//	DQueue Q;
//	InitQueue(&Q);
//	while (cycle) {
//		int choose = 0;
//		printf("\n入队:1,出队:2,遍历:3,扩充:4,清空:5,离开:6");
//		printf("\n请选择>>");
//		scanf("%d", &choose);
//		getchar();
//		switch (choose) {
//		case 1:
//			printf("\n输入入队数据>>");
//			QueTYPE data;
//			scanf("%c", &data);
//			getchar();
//			EnterQueue(&Q, data);
//			break;
//		case 2:
//			printf("\n出队数据为>>%c", DepartQueue(&Q));
//			break;
//		case 3:
//			TraverseQueue(&Q);
//			break;
//		case 4:
//			ExpandQueue(&Q);
//			break;
//		case 5:
//			ClearQueue(&Q);
//			break;
//		case 6:
//			printf("\nExiting···");
//			cycle = false;
//			break;
//		default:
//			break;
//		}
//	}
//	return 0;
//}

遍历代码如下

源文件

/*遍历图*/
/*创建一个标志数组---邻接矩阵*/
bool* CreatMVisited(AMGraph G) {
	bool* visited = (bool*)malloc(sizeof(bool) * G.vexnum);
	for (int i = 0; i < G.vexnum; i++) {
		visited[i] = false;
	}
	return visited;
}
/*DFS---邻接矩阵*/
void DFS(AMGraph G, int V, bool* visited) {//最开始访问第V个顶点
	visited[V] = true;
	printf("%2c", G.vexs[V]);
	for (int j = 0; j < G.vexnum; j++) {
		if (G.arcs[V][j] != 0 && visited[j] == false) 
			DFS(G, j, visited);
	}
}
/*创建一个标志数组---邻接表*/
bool* CreatLVisited(ALGraph G) {
	bool* visited = (bool*)malloc(sizeof(bool) * G.vexnum);
	for (int i = 0; i < G.vexnum; i++) {
		visited[i] = false;
	}
	return visited;
}
/*BFS---邻接表*/
void BFS(ALGraph G, int V, bool* visited) {
	DQueue Q;
	InitQueue(&Q);
	EnterQueue(&Q, G.vertices[V].data);
	visited[V] = true;
	printf("\n");
	while (Q.length) {//层次遍历,类似层次遍历二叉树
		char data = DepartQueue(&Q);
 		printf("%2c", data);//打印出队节点的信息
		V = SearchSiteByLVex(data, &G);
		ArcNode* node = G.vertices[V].firstarc;//创建临时节点存放第V个节点的首个邻边节点信息
		while (node) {//若邻边节点不为空,说明该节点有边,继续将没有被访问的邻边节点入队
			if (!visited[node->adjvex]) {//判断邻边节点是否已被访问
				EnterQueue(&Q, G.vertices[node->adjvex].data);
				visited[node->adjvex] = true;
			}
			node = node->nextarc;
		}
	}
}

上述均对无向图,邻接矩阵使用dfs,邻接表使用bfs,他们都需要用一个visited数组来观察是否节点被访问,visited的长度为图的顶点数量,每个空间的下标都对应了顶点在他们一维数组里的下标。当这些顶点在遍历过程中被访问了,就需要被记下来,以防后面遍历时不停访问。

逻辑如下:

  1. 对dfs,针对的是邻接矩阵。先根据输入的顶点下标V为根节点,然后把他对应在visited里的下标的元素设为true表示被访问。之后在二维数组关于V这一行进行遍历,发现有没被访问过的邻边节点,即对应在二维数组里的权值即不为无穷(有权图)又不为零(无权图),便把这个邻边节点采用递归的形式继续dfs。
  2. 对bfs,针对的是邻边表。同样现根据输入的定点下标V为根节点,然后把他标记为被访问。之后再把他入队,然后进入循环中(循环条件是队列长度不为零),然后出队获取其信息。之后就开始访问这个节点的没有被访问的各个邻边节点,把他们都入队,然后开始新一轮循环。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值