圖的建立、廣度優先遍歷和深度優先遍歷
圖分為有向圖和無向圖,再根據是否有權重又可以分為有權重圖和無權重圖。圖常用的表示方式有鄰接矩陣和鄰接表。這里我們處理的圖是有向、無權重圖,采用的表示方式是鄰接表。
圖的數據保存在文件中,比如:
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!\n"); 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!\n"); 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!\n", 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, "\n"); } } // 根據給定的節點查找到其能到達的其他節點 // 廣度優先遍歷 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, "-\n"); 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, "\n"); // 消毀隊列 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, "\n"); } // 消毀圖 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!\n"); 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; }

