多叉樹的設計、建立、層次優先遍歷和深度優先遍歷
早起曾實現過一個簡單的多叉樹《實現一個多叉樹》。其實現原理是多叉樹中的節點有兩個域,分別表示節點名以及一個數組,該數組存儲其子節點的地址。實現了一個多叉樹建立函數,用於輸入格式為A B。A表示節點的名字,B表示節點的子節點個數。建立函數根據用戶的輸入,首先建立一個新的節點,然后根據B的值進行深度遞歸調用。用戶輸入節點的順序就是按照深度遞歸的順序。另外,我們實現了一個層次優先遍歷函數。該函數用一個隊列實現該多叉樹的層次優先遍歷。首先將根節點入隊列,然后檢測隊列是否為空,如果不為空,將隊列出隊列,訪問出隊列的節點,然后將該節點的子節點指針入隊列,依次循環下去,直至隊列為空,終止循環,從而完成整個多叉樹的層次優先遍歷。
本文我們將還是介紹一個多叉樹,其內容和之前的實現差不多。
首先,用戶的多叉樹數據存儲在一個文件中,格式如下:
每行的第一個元素指定一個節點,其中第一行指定了該多叉樹的根節點。第二個元素表示該節點有幾個子節點,緊接着后面跟了幾個子節點。
根據以上數據文件,其對應的多叉樹應該是如下:
我們想得到結果是將書中的節點按深度進行輸出,比如先輸出深度最深的節點:x e j,然后輸出深度為2的節點:d f i,之后再輸出深度為1的節點:g cC z bBbB,最后輸出根節點:aA。
按照深度將節點輸出,很顯然是用層次優先遍歷的方法解決。層次優先遍歷的實現原理就是從根節點開始,利用隊列實現。
另外,我們想得到從根節點開始到葉子節點直接所有節點名字加起來最長的一個路徑,比如上面的樹中存在以下幾條路徑:
aA g d x
aA g d e
aA g d j
aA cC
aA z f
aA z i
aA bBbB
顯然,在這些路徑中,aA bBbB是所有路徑上節點名字加起來最長的一個路徑。求解從根節點到葉子節點上的所有路徑,利用深度優先遍歷更為合適。
下面我們討論一下多叉樹節點應該如何建立。首先多叉樹的節點應該如何定義,節點除了有自身的名字外,還要記錄其子節點有多少個,每個子節點在哪里,所以我們需要增加一個記錄子節點個數的域,還要增加一個數組,用來記錄子節點的指針。另外,還要記錄多叉樹中每個節點的深度值。
在讀取數據文件的過程中,我們順序掃描整個文件,根據第一個名字,建立新的節點,或者從多叉樹中找到已經有的節點地址,將后續的子節點生成,並歸屬於該父節點,直至掃描完整個數據文件。
讀取完整個文件后,也就建立了多叉樹,之后,我們利用隊列對多叉樹進行廣度優先遍歷,記錄各個節點的深度值。並將其按照深度進行輸出。
獲取從根節點到子節點路徑上所有節點名字最長的路徑,我們利用深度優先遍歷,遞歸調用深度優先遍歷函數,找到最長的那個路徑。
初次之外,還需定義隊列結構體,這里使用的隊列是循環隊列,實現相關的隊列操作函數。還有定義棧的結構體,實現棧的相關操作函數。另外對幾個內存分配函數、字符串拷貝函數、文件打開函數進行了封裝。需要注意的一點就是當操作完成后,需要對已經建立的任何東西都要銷毀掉,比如中途建立的隊列、棧、多叉樹等,其中還包含各個結構體中的指針域。
另外,函數測試是用戶在命令行模式下輸入程序名字后面緊跟數據文件的形式。
該程序的主要部分有如下幾點:
1.多叉樹節點的定義和生成一個新節點
2.數據文件的讀取以及多叉樹的建立
3.根據節點名字在多叉樹中查找節點的位置
4.多叉樹的層次優先遍歷
5.多叉樹的深度優先遍歷
6.隊列的定義以及相關操作函數實現
7.棧的定義以及相關操作函數實現
8.消毀相關已經建立好的隊列、棧、多叉樹等
9.測試模塊
下面我們給出相關的程序實現,具體細節可以查看代碼和注釋說明。
// 多叉樹的建立、層次遍歷、深度遍歷 #include <stdio.h> #include <stdlib.h> #include <string.h> #define M 100+1 // 宏定義,定義最大名字字母長度 // 定義多叉樹的節點結構體 typedef struct node_t { char* name; // 節點名 int n_children; // 子節點個數 int level; // 記錄該節點在多叉樹中的層數 struct node_t** children; // 指向其自身的子節點,children一個數組,該數組中的元素時node_t*指針 } NODE; // 對結構體重命名 // 實現一個棧,用於后續操作 typedef struct stack_t { NODE** array; // array是個數組,其元素為NODE*型指針 int index; // 指示棧頂元素 int size; // 棧的大小 } STACK; // 重命名 // 實現一個隊列,用於后續操作 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; } // 實現一些棧操作 // 棧的初始化 STACK* STACKinit(int size) // 初始化棧大小為size { STACK* sp; sp = (STACK*)util_malloc(sizeof (STACK)); sp->size = size; sp->index = 0; sp->array = (NODE**)util_malloc(size * sizeof (NODE*)); return sp; } // 檢測棧是否為空 // 如果為空返回1,否則返回0 int STACKempty(STACK* sp) { if (sp == NULL || sp->index <= 0) // 空 { return 1; } return 0; } // 壓棧操作 int STACKpush(STACK* sp, NODE* data) { if (sp == NULL || sp->index >= sp->size) // sp沒有被初始化,或者已滿 { return 0; // 壓棧失敗 } sp->array[sp->index++] = data; // 壓棧 return 1; } // 彈棧操作 int STACKpop(STACK* sp, NODE** data_ptr) { if (sp == NULL || sp->index <= 0) // sp為初始化,或者為空沒有元素 { return 0; } *data_ptr = sp->array[--sp->index]; // 彈棧 return 1; } // 將棧消毀 void STACKdestroy(STACK* sp) { free(sp->array); free(sp); } // 以上是棧的操作 // 實現隊列的操作 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; q = (NODE*)util_malloc(sizeof (NODE)); q->n_children = 0; q->level = -1; q->children = NULL; return q; } // 按節點名字查找 NODE* search_node_r(char name[M], NODE* head) { NODE* temp = NULL; int i = 0; if (head != NULL) { if (strcmp(name, head->name) == 0) // 如果名字匹配 { temp = head; } else // 如果不匹配,則查找其子節點 { for (i = 0; i < head->n_children && temp == NULL/*如果temp不為空,則結束查找*/; ++i) { temp = search_node_r(name, head->children[i]); // 遞歸查找子節點 } } } return temp; // 將查找到的節點指針返回,也有可能沒有找到,此時temp為NULL } // 從文件中讀取多叉樹數據,並建立多叉樹 void read_file(NODE** head, char* filename) { NODE* temp = NULL; int i = 0, n = 0; char name[M], child[M]; FILE* fp; fp = util_fopen(filename, "r"); // 打開文件 while (fscanf(fp, "%s %d", name, &n) != EOF) // 先讀取節點名字和當前節點的子節點個數 { if (*head == NULL) // 若為空 { temp = *head = create_node(); // 生成一個新節點 temp->name = util_strdup(name); // 賦名 } else { temp = search_node_r(name, *head); // 根據name找到節點 // 這里默認數據文件是正確的,一定可以找到與name匹配的節點 // 如果不匹配,那么應該忽略本行數據 } // 找到節點后,對子節點進行處理 temp->n_children = n; temp->children = (NODE**)malloc(n * sizeof (NODE*)); if (temp->children == NULL) // 分配內存失敗 { fprintf(stderr, "Dynamic allocation error!\n"); exit(EXIT_FAILURE); } // 如果分配成功,則讀取后面的子節點,並保存 for (i = 0; i < n; ++i) { fscanf(fp, "%s", child); // 讀取子節點 temp->children[i] = create_node(); // 生成子節點 temp->children[i]->name = util_strdup(child); // 讀子節點賦名 } } // 讀取完畢,關閉文件 fclose(fp); } // 實現函數1 // 將多叉樹中的節點,按照深度進行輸出 // 實質上實現的是層次優先遍歷 void f1(NODE* head) { NODE* p = NULL; QUEUE* q = NULL; // 定義一個隊列 STACK* s = NULL; // 定義一個棧 int i = 0; q = QUEUEinit(100); // 將隊列初始化大小為100 s = STACKinit(100); // 將棧初始化大小為100 head->level = 0; // 根節點的深度為0 // 將根節點入隊列 QUEUEenqueue(q, head); // 對多叉樹中的節點的深度值level進行賦值 // 采用層次優先遍歷方法,借助於隊列 while (QUEUEempty(q) == 0) // 如果隊列q不為空 { QUEUEdequeue(q, &p); // 出隊列 for (i = 0; i < p->n_children; ++i) { p->children[i]->level = p->level + 1; // 對子節點深度進行賦值:父節點深度加1 QUEUEenqueue(q, p->children[i]); // 將子節點入隊列 } STACKpush(s, p); // 將p入棧 } while (STACKempty(s) == 0) // 不為空 { STACKpop(s, &p); // 彈棧 fprintf(stdout, " %d %s\n", p->level, p->name); } QUEUEdestroy(q); // 消毀隊列 STACKdestroy(s); // 消毀棧 } // 實現函數2 // 找到從根節點到葉子節點路徑上節點名字字母個數最大的路徑 // 實質上實現的是深度優先遍歷 void f2(NODE* head, char* str, char** strBest, int level) { int i = 0; char* tmp = NULL; if (head == NULL) { return; } tmp = (char*)util_malloc((strlen(str) + strlen(head->name) + 1/*原程序中未加1*/) * sizeof (char)); sprintf(tmp, "%s%s", str, head->name); if (head->n_children == 0) { if (*strBest == NULL || strlen(tmp) > strlen(*strBest)) { free(*strBest); *strBest = util_strdup(tmp); } } for (i = 0; i < head->n_children; ++i) { f2(head->children[i], tmp, strBest, level + 1); } free(tmp); } // 消毀樹 void free_tree_r(NODE* head) { int i = 0; if (head == NULL) { return; } for (i = 0; i < head->n_children; ++i) { free_tree_r(head->children[i]); } free(head->name); // free(head->children); // 消毀子節點指針數組 free(head); } int main(int argc, char* argv[]) { NODE* head = NULL; char* strBest = NULL; if (argc != 2) { fprintf(stderr, "Missing parameters!\n"); exit(EXIT_FAILURE); } read_file(&head, argv[1]); fprintf(stdout, "f1:\n"); f1(head); f2(head, "", &strBest, 0); fprintf(stdout, "f2:\n %s\n", strBest); free_tree_r(head); return EXIT_SUCCESS; }