在计算机科学的浩瀚宇宙中,数据结构无疑是那把开启高效编程大门的关键钥匙。对于计算机专业的大学生们来说,数据结构课程是专业学习路上的一座重要里程碑,而其中的图结构更是充满魅力与挑战,像一幅神秘的画卷等待我们去展开。今天,就让我们结合一段精心优化的 C 语言代码,深入探索图结构中邻接矩阵的奥秘,相信这不仅能让你在课堂学习中游刃有余,更能助你在 CSDN 等技术平台上收获知识与成长。
一、图结构:数据世界的复杂网络
图结构是一种比线性表和树更为复杂的数据结构,它用于表示对象之间多对多的关系。在现实生活中,图结构的应用无处不在,比如社交网络中人与人的关系、交通网络中城市与道路的连接、互联网中网页之间的链接等等。图由顶点(Vertex)和边(Edge)组成,顶点代表对象,边则表示对象之间的联系。而邻接矩阵,就是图结构众多存储方式中的一种经典方法。
二、邻接矩阵:直观高效的存储方式
邻接矩阵是一种用二维数组来存储图的方式。对于一个具有 n个顶点的图,我们可以创建一个 n×n的二维数组,数组中的元素 arcs[i][j]用于表示顶点 i和顶点 j之间是否存在边。在无向图中,如果顶点 i和顶点 j之间有边相连,那么 arcs[i][j]=arcs[j][i]=1;如果没有边相连,则 arcs[i][j]=arcs[j][i]=0。这种存储方式简单直观,能够快速判断两个顶点之间是否有边,对于一些需要频繁查询边信息的操作非常高效。我们来看一段 C 语言代码,它实现了无向图邻接矩阵的创建、输出等功能。
#include <stdio.h>
#include <stdlib.h>
#define MAXVEX 16 /*顶点的最大个数*/
typedef char VexType; /*顶点的数据类型*/
typedef int AdjType; /*邻接矩阵的数据元素的类型*/
// 图的邻接矩阵存储结构
typedef struct {
VexType vexs[MAXVEX]; /*顶点表,存储顶点信息*/
AdjType arcs[MAXVEX][MAXVEX]; /*邻接矩阵,存储顶点之间的关系*/
int n; /*图的当前顶点数*/
} GraphMatrix;
// 创建一个无向图的邻接矩阵
GraphMatrix* CreateGraph(void) {
int i, j, k, e;
GraphMatrix *ga = (GraphMatrix *)malloc(sizeof(GraphMatrix));
if (ga == NULL) {
fprintf(stderr, "内存分配失败\n");
return NULL;
}
// 输入顶点数量并验证
do {
printf("请输入顶点的个数(1-%d):", MAXVEX);
if (scanf("%d", &(ga->n)) != 1 || ga->n < 1 || ga->n > MAXVEX) {
printf("输入错误,请输入1-%d之间的整数\n", MAXVEX);
while (getchar() != '\n'); // 清除输入缓冲区
ga->n = 0; // 重置以继续循环
} else {
break;
}
} while (1);
while (getchar() != '\n'); // 清除输入缓冲区
// 输入顶点信息
printf("请顺序地输入各顶点的信息(单个字符):\n");
for (i = 0; i < ga->n; i++) {
printf("顶点 %d: ", i);
ga->vexs[i] = getchar();
while (getchar() != '\n'); // 清除多余字符
}
// 初始化邻接矩阵
for (i = 0; i < ga->n; i++)
for (j = 0; j < ga->n; j++)
ga->arcs[i][j] = 0;
// 输入边的数量并验证
do {
printf("请输入边的个数(0-%d):", ga->n * (ga->n - 1) / 2);
if (scanf("%d", &e) != 1 || e < 0 || e > ga->n * (ga->n - 1) / 2) {
printf("输入错误,请输入有效的边数\n");
while (getchar() != '\n'); // 清除输入缓冲区
} else {
break;
}
} while (1);
while (getchar() != '\n'); // 清除输入缓冲区
// 输入边的信息并验证
printf("请输入与边相关联的两个顶点的序号(格式:i,j)\n");
for (k = 0; k < e; k++) {
do {
printf("边 %d: ", k + 1);
if (scanf("%d,%d", &i, &j) != 2 || i < 0 || i >= ga->n || j < 0 || j >= ga->n) {
printf("输入错误,请输入有效的顶点序号(0-%d)\n", ga->n - 1);
while (getchar() != '\n'); // 清除输入缓冲区
} else {
ga->arcs[i][j] = 1;
ga->arcs[j][i] = 1;
break;
}
} while (1);
while (getchar() != '\n'); // 清除输入缓冲区
}
return ga;
}
// 图的输出
void PrintGraph(GraphMatrix *ga) {
if (ga == NULL) {
printf("图为空\n");
return;
}
int i, j;
printf("\n顶点表为:\n");
for (i = 0; i < ga->n; i++)
printf("%4d:%c", i, ga->vexs[i]);
printf("\n\n邻接矩阵为:\n");
printf(" ");
for (i = 0; i < ga->n; i++)
printf("%4d", i);
printf("\n");
for (i = 0; i < ga->n; i++) {
printf("%2d:", i);
for (j = 0; j < ga->n; j++)
printf("%4d", ga->arcs[i][j]);
printf("\n");
}
}
// 释放图占用的内存
void DestroyGraph(GraphMatrix *ga) {
if (ga != NULL) {
free(ga);
}
}
int main() {
GraphMatrix *ga = CreateGraph();
if (ga != NULL) {
PrintGraph(ga);
DestroyGraph(ga); // 释放内存
}
return 0;
}
运行:
1. 数据结构定义
#define MAXVEX 16 /*顶点的最大个数*/
typedef char VexType; /*顶点的数据类型*/
typedef int AdjType; /*邻接矩阵的数据元素的类型*/
// 图的邻接矩阵存储结构
typedef struct {
VexType vexs[MAXVEX]; /*顶点表,存储顶点信息*/
AdjType arcs[MAXVEX][MAXVEX]; /*邻接矩阵,存储顶点之间的关系*/
int n; /*图的当前顶点数*/
} GraphMatrix;
这段代码首先定义了顶点的最大个数 MAXVEX ,以及顶点的数据类型 VexType 和邻接矩阵元素的数据类型 AdjType 。然后通过 typedef struct 定义了 GraphMatrix 结构体,它包含了存储顶点信息的数组 vexs 、存储邻接关系的二维数组 arcs ,以及记录当前顶点数的 n 。这个结构体是整个邻接矩阵存储图结构的核心。
2. 创建图函数 CreateGraph
GraphMatrix* CreateGraph(void) {
int i, j, k, e;
GraphMatrix *ga = (GraphMatrix *)malloc(sizeof(GraphMatrix));
if (ga == NULL) {
fprintf(stderr, "内存分配失败\n");
return NULL;
}
// 输入顶点数量并验证
do {
printf("请输入顶点的个数(1-%d):", MAXVEX);
if (scanf("%d", &(ga->n)) != 1 || ga->n < 1 || ga->n > MAXVEX) {
printf("输入错误,请输入1-%d之间的整数\n", MAXVEX);
while (getchar() != '\n'); // 清除输入缓冲区
ga->n = 0; // 重置以继续循环
} else {
break;
}
} while (1);
// 省略部分代码...
return ga;
}
在 CreateGraph 函数中,首先使用 malloc 函数为图结构体分配内存,并检查内存分配是否成功。接着,通过循环和 scanf 函数获取用户输入的顶点个数,并进行严格的输入验证,确保输入的顶点个数在合理范围内。如果输入错误,会提示用户重新输入,并清除输入缓冲区,避免影响后续输入。之后,按照类似的方式获取顶点信息、初始化邻接矩阵、输入边的个数和边的具体信息,每一步都进行了详细的输入验证,保证程序的健壮性。
3. 输出图函数 PrintGraph
void PrintGraph(GraphMatrix *ga) {
if (ga == NULL) {
printf("图为空\n");
return;
}
int i, j;
printf("\n顶点表为:\n");
for (i = 0; i < ga->n; i++)
printf("%4d:%c", i, ga->vexs[i]);
printf("\n\n邻接矩阵为:\n");
printf(" ");
for (i = 0; i < ga->n; i++)
printf("%4d", i);
printf("\n");
for (i = 0; i < ga->n; i++) {
printf("%2d:", i);
for (j = 0; j < ga->n; j++)
printf("%4d", ga->arcs[i][j]);
printf("\n");
}
}
PrintGraph 函数用于输出图的顶点表和邻接矩阵。首先检查图指针是否为空,若为空则提示图为空并返回。然后依次输出顶点表,展示每个顶点的序号和对应的字符信息。接着,以整齐美观的格式输出邻接矩阵,添加行列标题,使矩阵更加清晰易懂,方便我们直观地查看图中顶点之间的连接关系。
4. 内存释放函数 DestroyGraph
// 释放图占用的内存
void DestroyGraph(GraphMatrix *ga) {
if (ga != NULL) {
free(ga);
}
}
在程序使用完图结构后,需要释放动态分配的内存,避免内存泄漏。DestroyGraph 函数就是专门用于释放图结构体所占用的内存空间,只要传入的图指针不为空,就调用 free 函数进行释放,确保程序的内存管理合理有效。
三、邻接矩阵的优缺点与应用场景
优点
直观简单:邻接矩阵的存储方式非常直观,通过二维数组可以清晰地看到顶点之间的连接关系,易于理解和实现。
查询高效:判断两个顶点之间是否有边,只需要访问对应的数组元素,时间复杂度为 O(1),对于频繁查询边的操作非常高效。
方便处理带权图:如果要表示带权图,只需要将邻接矩阵中的元素值改为边的权值即可,实现起来相对简单。
缺点
空间浪费:无论图的边数多少,邻接矩阵都需要一个固定大小的二维数组,对于稀疏图(边数远小于顶点数平方的图),会造成大量的空间浪费。
更新操作复杂:在插入或删除边时,需要修改邻接矩阵中对应的两个元素,并且对于大规模图,这种更新操作的效率较低。
应用场景
由于邻接矩阵的特点,它适用于边数较多的稠密图,或者在需要频繁查询边信息、处理带权图的场景中。例如,在一些交通网络的最短路径算法实现中,如果网络连接较为紧密,使用邻接矩阵存储图结构可以方便地进行距离计算和路径查询。
四、学习数据结构的意义与成长之路
学习数据结构中的图结构以及邻接矩阵等存储方式,不仅仅是为了完成课程作业和考试,更重要的是培养我们的逻辑思维和问题解决能力。通过对代码的编写、调试和优化,我们能够深入理解数据在计算机中的组织和处理方式,学会如何选择合适的数据结构来解决实际问题。