一.图的存储
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");
}
}
总体实现主要分四步骤:
邻接矩阵:
- 输入要实现的图的边数arcnum和顶点数vexnum,并输入各个顶点数据data
- 初始化邻接矩阵arcs[][],把每个元素权值设为无穷,若为无权图则设为0
- 输入两个顶点V1、V2的数据data,找到对应在一维数组中的下标,准备在二维数组上建立边
- 输入V1、V2连起来的边的权值weight
邻接表:
- 输入要实现的图的边数arcnum和顶点数vexnum,并输入各个顶点数据data
- 输入两个顶点V1、V2的数据data,找到对应在一维数组中的下标
- 采用单链表中的头插法对V1、V2的首边节点firstarc插入对方指针,并分别记下对方在一维数组的下标adjvex
- 在头插法插入的节点中输入对应边的权值
二.图的遍历
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的长度为图的顶点数量,每个空间的下标都对应了顶点在他们一维数组里的下标。当这些顶点在遍历过程中被访问了,就需要被记下来,以防后面遍历时不停访问。
逻辑如下:
- 对dfs,针对的是邻接矩阵。先根据输入的顶点下标V为根节点,然后把他对应在visited里的下标的元素设为true表示被访问。之后在二维数组关于V这一行进行遍历,发现有没被访问过的邻边节点,即对应在二维数组里的权值即不为无穷(有权图)又不为零(无权图),便把这个邻边节点采用递归的形式继续dfs。
- 对bfs,针对的是邻边表。同样现根据输入的定点下标V为根节点,然后把他标记为被访问。之后再把他入队,然后进入循环中(循环条件是队列长度不为零),然后出队获取其信息。之后就开始访问这个节点的没有被访问的各个邻边节点,把他们都入队,然后开始新一轮循环。