前言
使用遞歸(Recursion)建立二叉樹(Binary Tree)的非順序存儲結構(即二叉鏈表),可以簡化算法編寫的復雜程度,但是遞歸效率低,而且容易導致堆棧溢出,因而很有必要使用非遞歸算法。
引入
無論是單鏈表還是二叉樹,創建時要解決問題就是關系的建立,即單鏈表中前驅節點與當前節點的關系和二叉樹中父節點與子節點的關系。
首先,思考一下建立單鏈表的過程,為了使鏈表各個節點連接起來,在創建當前節點(q)的時候,需借助一個指針(p)指向前一個節點,然后p->next = q。
由此推廣至二叉樹,把二叉樹每一層比作是鏈表的節點,接着借助一個指針列表(parent_list)存放父層的所有節點,然后每創建當前層的一個節點(current_node)時就與父層次的節點建立關系。
分析
引入中提出了創建二叉樹的整體思想,同時也拋出一個問題,如何建立父節點與子節點的關系?
下圖為一棵普通的二叉樹,下面對其進行一些處理。
首先,將其補全,用#代表空節點,補全規則為:將只有一個或沒有子節點的節點(空節點除外),用空節點補全為兩個子節點。
為便於后續分析,將其節點左結構化並去掉關系線。
現在,回顧一下引入中提到的“把二叉樹每一層比作鏈表的節點”,而建立單鏈表每次都只涉及兩個節點,因而下面每次分析都只涉及兩層。
其中,規定第二層為當前層,第一層為當前層的父層,且當前層為下次分析的父層。
在規定,父層為一個數組p[i](i為父層節點數,后同),當前層為數組q[j]。
下圖為第一次,選中層的節點標記為深灰底色。
可以容易看出,當前層的兩個節點與父層節點的關系:
p[0]->next = q[0]
p[0]->next = q[1]
為其添加關系線,然后再看下一次。
同樣,其關系如下:
p[0]->next = q[0]
p[0]->next = q[1] = #
p[1]->next = q[2]
p[2]->next = q[3]
由前兩次不難得出,j/2 = i (注意這里 / 運算結果只取整數),這個結果和完全二叉樹的性質相同,但是注意這里不是一棵完全二叉樹。
接着看下一次。
如圖所示,為了建立正確的二叉樹關系,父層的節點一定不能為空節點。
總觀整個結構,可以得出一個規律(該規律可用於動態改變父層列表,以減少內存占用):父層節點數(不包括空節點)的兩倍恰好為當前層的節點數(包括空節點)。
此時,還有個小問題,便是判斷當前節點是左子樹(Left Subtree)還是右子樹(Right Subtree)?
這里解決方法很簡單,便是計算 j % 2 ,若為0則為左子樹,否則為右子樹。
實現
現在,理清一下思路:
1.以從上到下、從左到右的順序創建二叉樹,因此有兩層循環。
2.有個父層列表(parent_list)用於存放父層所有節點的地址,且外層循環一次就更新一次(即讓父層列表等於當前層列表,代碼中為tmp_list),同時釋放舊父層列表。
3.內層循環創建每一層的節點,若輸入數據不為“#”則不為空節點,然后申請內存空間並賦值,再根據上述論述進行父節點和當前節點(current_node)建立關系。
下面給出代碼:
#include <stdio.h> #include <malloc.h> // 布爾類型 typedef enum {FALSE=0,TRUE=1} bool; // 用於標識當前建立左子樹還是右子樹 typedef enum {LEFT=0,RIGHT=1} flag; // 節點存放數據的類型 typedef char data_type; // 二叉樹節點類型 typedef struct node { data_type data; struct node *left_subtree, *right_subtree; } node , *bin_tree; bool create_bin_tree(bin_tree *root) { /* 創建根節點 */ data_type data = '\0'; scanf("%c",&data); if(data == '#'){ return FALSE; // 根節點為空,創建失敗 } else { *root = (node*)malloc(sizeof(node)); (*root)->data = data; (*root)->left_subtree = NULL; (*root)->right_subtree = NULL; } /* 創建非根節點 */ // 存放父層的節點列表 node **parent_list = (node**)malloc(sizeof(node*)); parent_list[0] = *root; // 父節點個數 int parent_amount = 1; while(1) { // 當前節點個數,設置為父節點個數的兩倍 int current_amount = parent_amount * 2; // 創建臨時列表存放當前深度的節點 node **tmp_list = (node**)malloc(sizeof(node*) * current_amount); // 用於記錄當前深度節點非空節點個數 int count = 0; // 創建當前層次的所有節點 int j = 0; for(;j < current_amount;++j) { data = '\0'; scanf("%c",&data); if(data != '#') // 不為空節點 { // 新建節點並賦值 node *current_node = (node*)malloc(sizeof(node)); current_node->data = data; current_node->left_subtree = NULL; current_node->right_subtree = NULL; // 加入到臨時列表中 tmp_list[count] = current_node; // 非空節點數加1 count++; // 與父節點建立關系 if(j%2 == LEFT) { (parent_list[j/2])->left_subtree = current_node; } else { (parent_list[j/2])->right_subtree = current_node; } } } // for循環結束 // 釋放父層列表 free(parent_list); // 更新父層列表 parent_list = tmp_list; // 更新父節點數 parent_amount = count; // 若非空節點數為0,則停止創建 if(count == 0) break; } return TRUE; }
上述代碼中,由於根節點較特殊且需要傳出地址,為了降低代碼編寫復雜程度,因而獨立於內層循環。
后話
寫文章除了用於記錄外,其實也無形中能理清想問題的思路。如寫這篇文章前,代碼雖實現了,但總感覺思路很亂。而文章寫完了,便也豁然開朗了。