图的建立、广度优先遍历和深度优先遍历
图分为有向图和无向图,再根据是否有权重又可以分为有权重图和无权重图。图常用的表示方式有邻接矩阵和邻接表。这里我们处理的图是有向、无权重图,采用的表示方式是邻接表。
图的数据保存在文件中,比如:
a 1 b
b 2 c e
c 1 f
d 2 c f
e 1 a
f 0
其中,第一个元素表示图中节点的名字,第二元素表示其可以直接到达的节点个数,后面紧跟着直接可以达到的节点。
我们采用的表示方式是邻接表,邻接表首先针对图中的节点定义一个数组,用来记录每个节点,数组中的每个节点元素后面跟着一个链表,在该链表中记录着其可以直接到达的节点。
节点的定义有以下几个部分:节点的名字,指向下一个节点的指针,是否被访问的标示符。
节点名字保存原始的字符串,这样在表示节点时,直接用字符串表示即可。也可以建立字符串到数字的映射以及数字到字符串的映射,即字符串和数字之间的双向映射,这里我们没有利用数字指代字符串来表示节点,而是直接使用的字符串。
表示图中节点是否被访问的标示符,我们将其放在节点中,也可以另外建立一个节点是否被访问的数组,如果我们放在节点中,那么链表中的节点也含有该标示符,但是我们只关注邻接表数组中的标示符,链表中的标示符不考虑,不过这样造成了链表中的标示符闲置,浪费了空间,这样做仅仅是为了描述方便。
图的遍历需要对图中节点记录是否已经被访问了,因为图中有可能存在环,即便不会倒回去,也有可能造成循环访问,如果添加了访问标识符,可以避免循环访问的情况。树的遍历则不需要添加访问标识符,因为书中不存在环,不会导致循环访问,而且不管图的遍历还是树的遍历,都不存在倒回去的情形。
上述文件中描述的有向图为:
我们想根据给定的节点,输出其可以达到的其他节点。这是一个图遍历问题,可以采用广度优先遍历也可以采用深度优先遍历。
图的广度优先遍历类似于树的广度优先遍历,也是利用队列进行遍历,不同点在于图是用邻接矩阵或邻接表等表示,树是采用其特有的树结构来表示。不过树也可以用图的方式来表示,因为树本身就可以看作为图,图也可以用树来表示,图和树之间的差别就在于图比树多了一些边,树比图少了一些边。图的遍历和树的遍历差别在于图为了防止循环访问的情形,需要一个节点访问标识符,而树不需要。
图的深度优先遍历同样也类似于树的深度优先遍历。也是多了一个节点访问标示符。
广度优先遍历需要借助于队列来实现,因为广度优先遍历的逻辑符合队列先进先出的特点。而深度优先遍历需要借助于栈来实现,因为深度优先遍历的逻辑符合栈后进先出的特点。注意在深度优先遍历的过程中有两访问方式,第一种是在按照入栈的顺序访问,第二种是按照出栈的顺序访问。而队列的入队列和出队列顺序都是一样的。在实际实现的深度优先遍历中并不需要显式的栈,而是采用的函数递归调用,借助于函数递归调用中参数的隐式的栈。深度优先遍历虽然没有使用显式的栈,但是由于递归调用,还是采用了符合栈的逻辑特点。
下面我们将给出具体的程序实现,其中主要包含以下几个部分:
1.图节点的定义和生成
2.图的表示方式——邻接表
3.队列的定义和操作函数的实现
4.一些模块函数的封装
5.设置和查询节点的访问标识符
6.根据节点名字查找节点在邻接表数组中的索引
7.读取数据文件,并建立图对应的邻接表,并打印图
8.图的广度优先遍历
9.图的深度优先遍历
10.相关已建立结构的释放
11.测试
相关细节请查看代码和注释说明。
// 图的建立、广度遍历和深度遍历 #include <stdio.h> #include <stdlib.h> #include <string.h> #define M (100 + 1) // 定义节点结构体 typedef struct node_t { char* name; // 节点名 int visited; // 表示是否被访问,0表示未被访问,1表示被访问 struct node_t* next; // 指向下一个节点 } NODE; // 实现一个队列,用于后续操作 typedef struct queue_t { NODE** array; // array是个数组,其内部元素为NODE*型指针 int head; // 队列的头 int tail; // 队列的尾 int num; // 队列中元素的个数 int size; // 队列的大小 } QUEUE; // 内存分配函数 void* util_malloc(int size) { void* ptr = malloc(size); if (ptr == NULL) // 如果分配失败,则终止程序 { printf("Memory allocation error! "); exit(EXIT_FAILURE); } // 分配成功,则返回 return ptr; } // 字符串赋值函数 // 对strdup函数的封装,strdup函数直接进行字符串赋值,不用对被赋值指针分配空间 // 比strcpy用起来方便,但其不是标准库里面的函数 // 用strdup函数赋值的指针,在最后也是需要free掉的 char* util_strdup(char* src) { char* dst = strdup(src); if (dst == NULL) // 如果赋值失败,则终止程序 { printf ("Memroy allocation error! "); exit(EXIT_FAILURE); } // 赋值成功,返回 return dst; } // 对fopen函数封装 FILE* util_fopen(char* name, char* access) { FILE* fp = fopen(name, access); if (fp == NULL) // 如果打开文件失败,终止程序 { printf("Error opening file %s! ", name); exit(EXIT_FAILURE); } // 打开成功,返回 return fp; } // 实现队列的操作 QUEUE* QUEUEinit(int size) { QUEUE* qp; qp = (QUEUE*)util_malloc(sizeof (QUEUE)); qp->size = size; qp->head = qp->tail = qp->num = 0; qp->array = (NODE**)util_malloc(size * sizeof (NODE*)); return qp; } // 入队列 int QUEUEenqueue(QUEUE* qp, NODE* data) { if (qp == NULL || qp->num >= qp->size) // qp未初始化或已满 { return 0; // 入队失败 } qp->array[qp->tail] = data; // 入队,tail一直指向最后一个元素的下一个位置 qp->tail = (qp->tail + 1) % (qp->size); // 循环队列 ++qp->num; return 1; } // 出队列 int QUEUEdequeue(QUEUE* qp, NODE** data_ptr) { if (qp == NULL || qp->num <= 0) // qp未初始化或队列内无元素 { return 0; } *data_ptr = qp->array[qp->head]; // 出队 qp->head = (qp->head + 1) % (qp->size); // 循环队列 --qp->num; return 1; } // 检测队列是否为空 int QUEUEempty(QUEUE* qp) { if (qp == NULL || qp->num <= 0) { return 1; } return 0; } // 销毁队列 void QUEUEdestroy(QUEUE* qp) { free(qp->array); free(qp); } // 以上是队列的有关操作实现 // 生成图中的节点 NODE* create_node() { NODE* q = NULL; q = (NODE*)util_malloc(sizeof (NODE)); q->name = NULL; q->visited = 0; q->next = NULL; return q; } // 设置访问标示visited void set_visited(char name[M], NODE* graph, int n) { int i = 0; for (i = 0; i < n; ++i) { if (strcmp(name, graph[i].name) == 0) { graph[i].visited = 1; return; } } } // 查找是否已经被访问,返回0表示未被访问,1表示被访问 int is_visited(char name[M], NODE* graph, int n) { int i = 0; for (i = 0; i < n; ++i) { if (strcmp(name, graph[i].name) == 0) { if (graph[i].visited == 1) // 被访问 { return 1; } else // 未被访问 { return 0; } } } return 0; } // 根据节点名,返回节点在邻接表中的索引 int find_index(char name[M], NODE* graph, int n) { int i = 0; for (i = 0; i < n; ++i) { if (strcmp(name, graph[i].name) == 0) { return i; } } return -1; } // 读取文件,建立邻接表 void read_file(NODE** graph, int* count, char* filename) { char name[M], adj[M]; int n = 0, i = 0, j = 0; FILE* fp = NULL; NODE* p1 = NULL, *p2 = NULL; *graph = (NODE*)util_malloc(M * sizeof (NODE)); *count = 0; fp = util_fopen(filename, "r"); // 打开文件 while (fscanf(fp, "%s %d", name, &n) != EOF) { (*graph)[i].name = util_strdup(name); (*graph)[i].visited = 0; (*graph)[i].next = NULL; p1 = &((*graph)[i]); for (j = 0; j < n; ++j) { fscanf(fp, "%s", adj); p2 = create_node(); p2->name = util_strdup(adj); //// 与文件中的节点顺序相反 //p2->next = p1->next; //p1->next = p2; //按照文件中的节点顺序 p1->next = p2; p1 = p2; } ++i; } *count = i; // 总共i个节点 fclose(fp); // 读取完毕 } void print_graph(NODE* graph, int n) { int i = 0; NODE* p = NULL; for (i = 0; i < n; ++i) { fprintf(stdout, "%s ", graph[i].name); p = graph[i].next; while (p != NULL) // not if (p != NULL) { fprintf(stdout, "%s ", p->name); p = p->next; } fprintf(stdout, " "); } } // 根据给定的节点查找到其能到达的其他节点 // 广度优先遍历 void func(char name[M], NODE* graph, int n) { NODE* p1 = NULL, *p2 = NULL; int index = 0, i = 0; QUEUE* q = NULL; // 将访问标识都置为0 for (i = 0; i < n; ++i) { graph[i].visited = 0; } q = QUEUEinit(100); // 初始化队列 index = find_index(name, graph, n); fprintf(stdout, "Reachable node:"); if (graph[index].next == NULL) { fprintf(stdout, "- "); return; } // 如果后面有节点 p1 = &(graph[index]); // 将该节点入队列 QUEUEenqueue(q, p1); // 该节点算作已经被访问了 graph[index].visited = 1; while (QUEUEempty(q) == 0) // 如果队列不为空 { // 出队列 QUEUEdequeue(q, &p1); p1 = p1->next; // ※这一步保证每次都不访问队列中的节点 while (p1 != NULL) { index = find_index(p1->name, graph, n); // 查找该节点的索引 if (graph[index].visited == 1) // 如果已经被访问过 { // 不做处理 ; } else // 如果还没有被访问 { // 输出该节点 fprintf(stdout, "%s ", p1->name); // 将该节点设置为被访问过 graph[index].visited = 1; // 将该节点入队列 QUEUEenqueue(q, &graph[index]); } p1 = p1->next; } } fprintf(stdout, " "); // 消毁队列 QUEUEdestroy(q); } // 深度优先遍历 void function2(char name[M], NODE* graph, int n, int* flag) { int index = 0; NODE* p1 = NULL, *p2 = NULL; index = find_index(name, graph, n); graph[index].visited = 1; // 一开始就被设置被访问,所以后面的设置visited可以忽略 p1 = graph[index].next; // 这一步很关键,不考虑邻接表数组中的元素,而是直接考虑数组中的元素后面链表中的元素 if (p1 == NULL) { return; } index = find_index(p1->name, graph, n); if (graph[index].visited == 1) { return; } while (p1 != NULL && graph[index].visited != 1) { *flag = 1; // 设置存有后续节点标识 fprintf(stdout, "%s ", p1->name); // 设置访问标示 index = find_index(p1->name, graph, n); // graph[index].visited = 1; // 这里可以被忽略,因为在函数开始出被设置了 p2 = &graph[index]; // 下一个深度的节点 function2(p2->name, graph, n, flag); p1 = p1->next; if (p1 != NULL) { index = find_index(p1->name, graph, n); // p1变了,index也要变 } else { break; } } } // 对深度优先遍历function2封装 void func2(char name[M], NODE* graph, int n) { int i = 0, flag = 0; for (i = 0; i < n; ++i) // 重置访问标示 { graph[i].visited = 0; } fprintf(stdout, "Reachable node-2:"); function2(name, graph, n, &flag); if (flag == 0) { fprintf(stdout, "-"); } fprintf(stdout, " "); } // 消毁图 void free_graph(NODE* graph, int n) { int i = 0; NODE* p1 = NULL, *p2 = NULL; if (graph == NULL) { return; } for (i = 0; i < n; ++i) { p1 = graph[i].next; free(graph[i].name); while (p1 != NULL) { p2 = p1->next; free(p1->name); free(p1); p1 = p2; } } free(graph); } int main(int argc, char* argv[]) { NODE* graph = NULL; char name[M]; int count = 0; if (argc != 2) { fprintf(stderr, "Missing parameters! "); exit(EXIT_FAILURE); } read_file(&graph, &count, "data.txt"); // 打印邻接表 // print_graph(graph, count); fprintf(stdout, ">Vertex:"); fscanf(stdin, "%s", name); while (strcmp(name, "end") != 0) { func(name, graph, count); func2(name, graph, count); fprintf(stdout, ">Vertex:"); fscanf(stdin, "%s", name); } free_graph(graph, count); return EXIT_SUCCESS; }