1.04k likes | 1.22k Views
第五章 樹. 資料結構與演算法. 徐熊健. 目錄. 5.1 樹的概念 5.2 樹的表示法 5.3 二元樹 5.4 二元樹表示法 5.5 二元樹的走訪 5.6 二元樹的複製與相等測試 5.7 引線二元樹 5.8 堆積 5.9 二元搜尋樹 5.10 樹林. 5.1 樹的概念. 樹 (tree) 是一種重要的離散結構 (discrete structure) ,它提供一種「具有層次關係」的概念來結構資料。. 生物演化樹 (evolutionary tree). More Examples. 5.1.2 專有名詞.
E N D
第五章 樹 資料結構與演算法 徐熊健
目錄 5.1樹的概念 5.2樹的表示法 5.3二元樹 5.4二元樹表示法 5.5二元樹的走訪 5.6二元樹的複製與相等測試 5.7引線二元樹 5.8 堆積 5.9 二元搜尋樹 5.10 樹林
5.1 樹的概念 • 樹 (tree) 是一種重要的離散結構 (discrete structure),它提供一種「具有層次關係」的概念來結構資料。
5.1.2 專有名詞 定義: 樹為節點所組成的有限集合,其中 (1) 存在一特殊節點 R 稱為樹根 (root) ; (2) 其它節點可分割成n個無交集的集合: T1, T2, …, Tn,n 0,而T1、T2、…Tn本身皆為樹,稱其為 R 的子樹 (subtree) 。 A recursive definition !! 本章所提到的樹皆為「有根樹」 (rooted tree)。
5.1.2 專有名詞(續) • 節點 (node): 圖中圓圈和向下的分支(branch) • 樹根 (root): 節點A • 分支度 (degree): 一個節點其所有子樹的數目 • 樹葉 (leaf):分支度為0的節點 • 內部節點(internal node)、非終端節點(non-terminal node) • 深度 (depth): 樹的最高階層(level)
5.1.2 專有名詞(續) • 兒子 (son): 任何節點 X,其子樹的樹根 Y,為 X 的兒子 • 父親 (father, parent): Y 是 X 的「父親」 • 兄弟 (sibling, brother) : 共有一個父親的數個節點 • 祖先 (ancestor): 該節點走向樹根所經過的所有節點 • 後裔 (descendant): 任一節點的所有子樹節點
5.2 樹的表示法 • 用陣列表示樹 ? • 用鍵結串列表示樹 ! 令分支度=k 若T 共有n個節點,則需要n個鍵結節點,計有nk 個指標;然而除了樹根外,每個節點只須由唯一的指標所指向,遂只需要有n-1個指標空間。用不著的指標空間多達nk-(n-1)=n(k-1)+1個,比用得著的還多,著實浪費。
5.2.1一般化的串列表示 • 串列是一個有限且有順序的元素集合 A = (a1, a2, … , an) 每個元素ai,1in,皆有相同的資料型態 • 倘若不限定各個元素都具有相同的資料型態,則稱此串列為「一般化串列」 (generalized list) (ai 可以是一個節點或是一棵樹) • 可用一般化串列來表示一棵樹T: T = (R, T1, T2, … , Tn) 其中R 為T 的樹根,而T1, T2, … , Tn 為R 的子樹,Ti可能是一個節點也可能是一棵樹(一般化串列)
範例5-1 以一般化串列表示樹 • (A, (B, (E, K), (F, L, M), G), (C, H), (D, (I, N), (J, O, P))) (A, T1, T2, T3) T1= (B, (E, K), (F, L, M), G) T2= (C, H) T3 = (D, (I, N), (J, O, P)) T1 T3 T2
程式5-1 一般化串列鍵結節點的宣告 1 struct TreeNode 2 { int tag; 3 union 4 { int data; 5 struct TreeNode *Tlink; 6 } node; 7 struct TreeNode *link; 8 };
(A, T1, T2, T3) T1= (B, (E, K), (F, L, M), G) T2= (C, H) T3 = (D, (I, N), (J, O, P))
5.2.2 左子右兄弟表示法 • 每個節點都有唯一的最左 兒子 (leftmost child) ; • 每個節點都有最靠近它的 右兄弟。
5.2.3 分支度為 2 的二元樹表示法 • 分支度為 2 的樹又稱為 二元樹 (binary tree) • 二元樹中任一節點皆有 2個指標分別指向該節點的左子樹和右子樹。 • 二元樹可以表示任何樹!事實上二元樹有許多很好的性質,我們在一下節中介紹。
5.3 二元樹 • 二元樹是一個由節點組成的有限集合,可以是空集合,或是包含了 (1) 一個樹根節點; (2) 其它節點分割成兩個沒有交集的二元樹, 其一為左子樹 (left subtree) , 另一為右子樹 (right subtree) • 樹和二元樹之間的差異: • 樹不允許空節點,而空二元樹是允許的; • (2) 樹的子樹沒有順序,而二元樹的子樹有左右之分。
5.3.1 樹和二元樹的基本性質 樹或二元樹皆擁有下面的性質: 定理5-1: 若一棵樹 T的總節點數為 V,總邊數為E,則 V = E + 1。
證明:利用數學歸納法: 歸納法的基礎:當V=1時,即T 只有一個樹根節點,E為0。所以V = E + 1成立。 歸納法的假設:假設當1 V K 時,該式成立。 歸納法的推論:當V = K + 1時,令此樹的樹根為R,而R有m個子樹T1, T2, …, Tm(如圖5-12所示),分別有個V1, V2, …, Vm個節點。 其中V1 +V2 + …+ Vm =K。若E1, E2, …, Em 分別為此m個子樹的邊數,則 E = E1 +E2+ …+ Em+ m (1) 根據歸納法的假設可知: V1 = E1+1 V2 = E2+1 ‧ ‧ ‧ Vm= Em+1 所以 V1 +V2 + …+Vm = E1 + E2 + …+ Em +m (2) 然而 V = V1 +V2 + …+Vm+1(3) 將式 (2) 代入式 (3) 可得: V = E1+ E2+ …+ Em+ m + 1 引用式 (1) ,則可得 V = E + 1。 由數學歸納法的原理可得證本定理。
5.3.2 二元樹的性質 (a) 稱為歪斜樹 (skew tree) ; (b) 稱為完備二元樹 (complete binary tree) (其樹葉節點皆在相鄰階層,完整定義在後) 同樣數目的樹節點存放在歪斜樹上,會花較多的階層;放在完備二元樹上,則只須最少的階層。
= = 2k-1 5.3.2 二元樹的性質(續) 定理5-2: (1) 二元樹上階層 i 的節點數目最多為 2i-1,i1; (2) 深度為 k 的二元樹,其節點數目最多為 2k-1,k1。 • 定理5-2說明了:(1) 每一階層上可放的最多節點數; (2) 階層數已知時,二元樹可放的最多節點數。 證明:(1) 利用數學歸納法: 歸納法的基礎:當i=1時只有樹根一個節點:2i-1=21-1=1;原式成立 歸納法的假設:假設i = k 時該式成立,即階層 k 的最大節點數為 2k-1,k 1。 歸納法的推論:當i = k+1時,因在階層 k 的每一個節點最多會有 2 個子節點,遂在k+1階層最多會有 22k-1 = 2k = 2i-1 節點。 由數學歸納法的原理可知本定理得證。 (2) 由 (1) 可知:二元樹在階層 i 的最多節點數為2i-1;那麼深度k 的二元樹最多可有節點如下:
對二元樹而言,任一節點的分支度可能為 0、1、或2;分支度為 0 者是為樹葉。 • 樹葉的數目與分支度為 2 的節點數目,存在著有趣的關係,請見定理5-3。 定理5-3: 若 T為一非空的二元樹,n0為樹葉節點數目,n2為分支度為 2 的節點數目,則n0 = n2+1。 證明:令 n 為所有節點數目,而 n1 為分支度為 1 的節點數目。 可得 n = n0 + n1 +n2 (1) 令 B 為二元樹的分支數,除了樹根外,每一節點皆有一 分支指向之。所以 n = B+1 (亦可參考定理5-1)。 又 B = n1+2n2 且 n = n1+ 2n2+1 (2) 由式 (1) 和式 (2) ,我們得到 n0= n2+ 1。
完滿二元樹 (full binary tree) 定義:一個深度為 k 的完滿二元樹即為一深度為 k 且有 2k-1 個節點的二元樹,k 0。 • 由定理5-2(2)可知:深度為k的二元樹,其最多節點數為2k-1。這樣的樹會將樹上所有可能存放節點的位置都放滿,完滿 (full) 之名因而生成。
完備二元樹 (complete binary tree) 我們可對二元樹上節點予以編號,階層小者先編,同一階層則自左向右編號,這樣的編號方式,使我們定義出完備(complete) 二元樹。 定義:深度為k,節點數為 n 的二元樹稱為完備二元樹,若且唯若 (1) k = 1時,樹有一個節點; (2) 當k 2 且 1i<k 時, 深度 i 有 2i-1個節點, 且第 k 層的節點皆由 第k-1層的分支由左至右 逐一連接而成。 完滿或完備二元樹之所以受人注意,乃因它們可以將節點以較少的階層數存放。
定理5-4:n個節點的完備或完滿二元樹,其深度為 log2(n+1)。 證明: 完滿或完備二元樹的節點皆自階層 1 起排放,任一階層 i 可排放 2i-1 個節點,遂由定理5-2得知,此 n 個節點所佔的深度為 log2(n+1)。
正規二元樹 (formal binary tree) • 在二元樹中非樹葉的節點,又稱為內部節點 (internal node) 。若所有內部節點恰有 2 個子節點,即成為正規二元樹。 定義:若二元樹中所有內部節點恰有2個子 節點,則稱之為正規二元樹 。 • 正規二元樹常被引用在單淘汰賽制的各項賽程安排。樹葉為參賽隊伍,內部節點為優勝隊伍,樹根即為冠軍隊伍。 • 在單淘汰賽制中自n 個隊伍中產生冠軍,須舉辦 n-1場比賽。
5.4 二元樹的表示法 5.4.1以陣列表示二元樹 定理5-5:若一個具有n 個節點的完備二元樹以循序的方式編號,並依序存放在陣列 A 中,即第i 個節點放在 A[i] 中,1 i n,則 (1) 節點 i 的父節點位在 A[i/2]處,i ≠ 1 (i = 1時,其節點正為樹根,無父節點) (2) 若2in,則節點 i 的左兒子節點位在 A[2i]處;若2i >n,節點 i 沒有左子節點 (3) 若2i+1 n,節點 i 的右兒子節點位 A[2i+1]處;若2i+1>n,節點i 沒有左子節點
證明:先用數學歸納法證明 (2) : • 歸納法的基礎:當 i = 1時,其左子節點確實在 A[2] 處(除非 n<2,此時節點1沒有左子節點)。 • 歸納法的假設:假設當I = k 時,其左子節點確在 A[2i] = A[2k]處(除非2i 已大於n,此時節點 i 無左子節點)。 • 歸納法的推論:當i = k+1時,因第k節點的左子節點在A[2k] 處,其右子節點則位在 A[2k+1] 處;那麼第k+1節點的左子節點應緊臨 A[2k+1],亦即位於 A[2k+2] = A[2(k+1)] 處(除非 2i 已大於n,此時節點 i 無左子節點)。 • 由數學歸納法的原理,得證定理5-5 (2) 。 (3) 由於 (2) 成立,而且節點 i 的右子節點會緊臨 A[2i],亦即其位於 A[2i+1]處,除非 i 根本沒有右子節點;得證定理5-5 (3) 。 由 (2) 和 (3) ,我們可知定理 5-5 (1) 成立。
‧ ‧ ‧ ‧ ‧ ‧ ‧ ‧ ‧ ‧ ‧ ‧ ‧ ‧ ‧ ‧
5.4.2 以鍵結串列表示二元樹 • 以陣列來表示完備二元樹算是相當方便,但 • 一般的二元樹將導致陣列空間的浪費; • 如果二元樹中節點的新增、刪除動作頻繁,則會造成陣列對應資料的搬動隨而頻繁。 • 以鍵結串列表示二元樹 為方便辦識樹根,我們常用root 指標指向樹根,做為樹鍵結串列的起點。
陣列表示和鍵結表示的空間需求 • 完備二元樹的陣列表示和鍵結表示皆沒浪費儲存資料的空間; • 但歪斜樹的陣列表示,比起鍵結表示則浪費許多。 • 以n 個節點的歪斜樹而言,陣列表示法可能需要2n-1 個陣列元素;其中 2n-1-n 個陣列元素空間是浪費的,而鍵結表示法只需要n 個節點空間。
二元樹節點的宣告 程式5-2 1 struct BTreeNode 2 { struct BtreeNode *leftchiled; 3 int data; 4 struct BTreeNode *rightchild; 5 }; 6 struct BTreeNode *root;
這樣的二元樹節點,要取得兒子節點的指標十分方便。但若有取得父節點的需求則嫌不足。我們可以加上 parent 欄位指向父節點:
5.5 二元樹的走訪 • 假如一組資料已用二元樹的組織在一起,總有需求對其全數資料做動作,例如計算所有數目、印出所有資料、在所有資料中搜尋某項資料、…等。此時即須對此二元樹做走訪 (traversal) 的運算,利用走訪二元樹的同時,將計算、列印或搜尋的動作完成。 • 事實上走訪即在決定二元樹上資料被處理(計算、列印或搜尋)的順序。我們也希望走訪的演算法對任何節點皆一致,是容易撰寫程式實作的。 • 若對一個二元樹上的節點而言,V表示處理節點上的資料,L 表示走訪其左子樹,R 表示走訪其右子樹。
5.5 二元樹的走訪(續) • 因為對稱的緣故,我們只考慮先走訪左邊再走訪右邊的情形,那麼右圖只剩下三種走訪方式。 • 以V所在的相對位置,分別對此三種走訪方式取名如下: (1) LVR 中序走訪 (inorder traversal) 或中序表示法 (infix notation); (2) LRV 後序走訪 (postorder traversal) 或後序表示法 (postfix notation); (3) VLR 前序走訪 (preorder traversal) 或前序表示法 (prefix notation)。
5.5.1 中序走訪 • 利用遞迴的方式撰寫中序走訪的程序;希望把二元樹中序走訪的順序印出來。 程式5-3二元樹的中序走訪 1 struct BTreeNode 2 { struct BTreeNode *leftchild; 3 char data; 4 struct BTreeNode *rightchild; 5 }; 6 struct BTreeNode *root; 7 void inorder(struct BTreeNode *node) 8 { if (node != NULL) 9 { inorder(node->leftchild); 10 cout << node->data; 11 inorder(node->rightchild); 12 } 13}
範例 5-12 6 struct BTreeNode *root; 7 void inorder(struct BTreeNode *node) 8 { if (node != NULL) 9 { inorder(node->leftchild); 10 cout << node->data; 11 inorder(node->rightchild); 12 } 13 } 此為運算式二元樹 其對應中序表示運算式為 : (A+B)*C+D/(E-F)。
6 struct BTreeNode *root; 7 void inorder(struct BTreeNode *node) 8 { if (node != NULL) 9 { inorder(node->leftchild); 10 cout << node->data; 11 inorder(node->rightchild); 12 } 13 } (A+B)*C+D/(E-F) 如何加上 ( )?
5.5.2 後序走訪 程式5-4二元樹的後序走訪 14 void postorder(struct BTreeNode *node) 15 { if (node !=NULL) 16 { postorder(node->leftchild); 17 postorder(node->rightchild); 18 cout << node->data; 19 } 20 } 以後序走訪,可得到: AB+C*DEF-/+ 其正為對應的後序運算式。
5.5.3 前序走訪 程式5-5二元樹的前序走訪 21 void preorder(struct BTreeNode *node) 22 { if (node != NULL) 23 { cout << node->data; 24 preorder(node->leftchild); 25 preorder(node->rightchild); 26 } 27 } 以前序走訪,可得到: +*+ABC/D-EF 其正為對應的前序運算式。
5.5.4 利用堆疊和迴圈取代遞迴,進行二元樹的走訪 1 struct BTreeNode 2 { struct BTreeNode *leftchild; 3 char data; 4 struct BtreeNode *rightchild; 5 }; 6 struct BTreeNode *root; 7 struct StackNode 8 { struct BTreeNode *treenode; 9 struct StackNode *next; 10}; 11 struct StackNode *top;
12 void push_node(struct BtreeNode *node ) 13 { struct StackNode *old_top; 14 old_top = top; 15 top = (struct StackNode *) malloc (sizeof(struct StackNode)); 16 top->treenode = node; 17 top->next = old_top; 18 } 19 struct BTreeNode *pop_node() 20 { struct BTreeNode *Tnode; 21 struct StackNode *old_top; 22 if (top == NULL) 23 StackEmpty(); 24 else 25 { Tnode = top->treenode; 26 old_top = top; 27 top = top->next 28 free(old_top); 29 return Tnode; 30 } 31 }
6 struct BTreeNode *root; 7 void inorder(struct BTreeNode *node) 8 { if (node != NULL) 9 { inorder(node->leftchild); 10 cout << node->data; 11 inorder(node->rightchild); 12 } 13 } 32 void inorder_Stack(struct BtreeNode* node) 33 { do 34 { while (node != NULL) 35 { push_node(node); 36 node = node->leftchild; 37 } // push all left children 38 if (top != NULL) 39 { node = pop_node(); 40 cout << node->data; 41 node = node->rightchild; 42 } // inorder printing and check right child 43 } while ((top!=NULL)||(node!=NULL)); 44 } // top==NULL 堆疊已空;node==NULL沒有右子樹
中序表示為: DBGEHAFIC 32 void inorder_Stack() 33 { struct BTreeNode * node; 34 node = root; 35 do 36 { while (node != NULL) 37 { push_node(node); 38 node = node->leftchild; 39 } 40 if (top != NULL) 41 { node = pop_node(); 42 cout << node->data; 43 node = node->rightchild; 44 } 45 }while(top!=NULL)||(node!=NULL)); 46 }
程式5-5二元樹的前序走訪 21 void preorder(struct BTreeNode *node) 22 { if (node != NULL) 23 { cout << node->data; 24 preorder(node->leftchild); 25 preorder(node->rightchild); 26 } 27 } 45 void Preorder_Stack(struct BSTreeNode * node) 46 { do 47 { while (node != NULL) 48 { cout << node->data); 49 push_node(node); 50 node = node->leftchild; 51 } // push all left children 52 if (top != NULL) 53 { node = pop_node(); 54 node = node->rightchild; 55 } 56 } while ((top!=NULL)||(node!=NULL)); 57 }
程式5-4二元樹的後序走訪 14 void postorder(struct BTreeNode *node) 15 { if (node !=NULL) 16 { postorder(node->leftchild); 17 postorder(node->rightchild); 18 cout << node->data; 19 } 20 } 58 void Postorder_Stack(struct BTreeNode * node) 59 { do 60 { while (node != NULL) 61 { cout << node->data; 62 push_node(node); 63 node = node->rightchild; 64 } // push all left children 65 if (top != NULL) 66 { node = pop_node(); 67 node = node->leftchild; 68 } 69 } while ((top!=NULL)||(node!=NULL)); 70 }
5.5.5 階層走訪 • 「階層走訪」 (level-order traversal) 是依階層的順序,進行二元樹的走訪,先走訪階層小的節點,後走訪階層大的節點,同一階層者則依自左向右的順序走訪。 • 對下圖的二元樹,進行階層走訪的結果為: ABCDEFGHI。 • 對同一階層而言,先走訪的節點,其子節點亦在下一階層中先被走訪。 • 這種「先進先出」的特性,恰可用佇列予以儲存與處理。
程式5-7利用佇列進行二元樹的階層走訪 1 struct BTreeNode 2 { struct BTreeNode *leftchild; 3 char data; 4 struct BTreeNode *rightchild; 5 }; 6 struct BTreeNode *root; 7 struct QNode 8 { struct BTreeNode *treenode; 9 struct QNode *next; 10 }; • struct QNode *front, *rear; 12 void AddQueue(struct BTreeNode *Tnode) 13 { struct QNode *node; 14 node=(struct QNode *)malloc(sizeof(struct QNode)); 15 node->treenode = Tnode; 16 node->next = NULL; 17 if (front == NULL) front = node; • else rear->next = node; • rear = node; 21 }
22 struct BTreeNode *DeleteQueue() 23 { struct BTreeNode *Tnode; 24 struct QNode *old_front; 25 if (rear == NULL) 26 QueueEmpty(); 27 else 28 { Tnode = front->treenode; 29 old_front = front; 30 front = front->next; 31 free(old_front); 32 return Tnode; 33 } 34 } 35 void LevelOrder (struct BTreeNode *node) 36 { AddQueue(node); 37 while (front !=NULL) 38 { node = DeleteQueue(); 39 cout << node->data; 40 if (node->leftchild != NULL) 41 AddQueue(node->leftchild); 42 if (node->rightchild != NULL) 43 AddQueue(node->rightchild); 44 } 45 }