1. 程式人生 > >《資料結構與演算法分析》學習筆記-第四章-樹

《資料結構與演算法分析》學習筆記-第四章-樹

[toc] *** ## 4.1 預備知識 - 對於大量的輸入資料,連結串列的執行緒訪問時間太慢,不宜使用。**二叉查詢樹**大部分操作的執行時間平均為**O(logN)**。 - 樹可以用幾種方式定義,定義樹的一種自然的方式是遞迴的方法。一棵樹是一些節點的集合。這個集合可以是空集。若非空,則==一棵樹由稱作根節點r以及0個或多個費控的子樹T1, T2, ..., Tk組成==。這些子樹中的每一棵的根都被來自根r的一條==有向邊==所連線 - 每一棵子樹的根叫做根r的兒子,而r是每一棵子樹的父親。 - 一棵樹是==N個節點和N-1條邊的集合,其中的一個節點叫做根。存在N-1條邊的結論是由:每條邊都將某個節點連線到它的父親,而除去根節點的每一個節點都有一個父親。 - 每個節點可以有任意多個兒子(可以是0個)。==沒有兒子的節點叫做樹葉(leaf)==。==具有相同父親的節點叫做兄弟(sibling)==。用類似的方法可以定義祖父和孫子的關係 - 對任意節點ni,ni的==深度(depth)為從根到ni的唯一路徑的長==。因此,**根的深度是0**。ni的==高(height)是從ni到一片樹葉的最長路徑的長==。因此所有**樹葉的高是0**。==一棵樹的高等於它的根的高==。一棵樹的深度等於它最深樹葉的深度,等於這棵樹的高。 - 如果存在從n1到n2的一條路徑,那麼n1是n2的一位祖先,而n2是n1的一個後裔。如果n1!=n2,那麼n1是n2的一個真祖先,而n2是n1的一個真後裔 ### 4.1.1 樹的實現 ``` typedef struct TreeNode *PtrToNode; struct TreeNode { ElementType Element; PtrToNode FirstChild; PtrToNode NextSibling; } ``` ### 4.1.2 樹的遍歷和應用 1. 先序遍歷: 對節點的處理工作是在它的所有兒子節點被處理之前進行的 ``` static void ListDir(DirectoryOrFile D, int Depth) { if (D is a legitimate entry) { PrintName(D, Depth); if (D is a directory) { for each child, C, of D ListDir(C, Depth+1); } } } void ListDirectory(DirectoryOrFile D) { ListDir(D, 0); } ``` 2. 後序遍歷:對節點的處理工作是在它的所有兒子節點被處理之後進行的 ``` static void SizeDirectory (DirectoryOrFile D) { int TotalSize; TotalSize = 0; if (D is a legitimate entry) { TotalSize = FileSize(D); if (D is a directory) for each child, C, of D TotalSize += SizeDirectory(C) } return TotalSize; } ``` ## 4.2 二叉樹 - 二叉樹是一棵樹,其中==每個節點都不能有多餘兩個==的兒子 - 二叉樹的一個性質是平均二叉樹的深度要比N小得多,平均深度為O(N的平方根) - 二叉查詢樹深度的平均值為O(logN),但是極端情況下這個深度是可以大到N-1的 ### 4.2.1 實現 具有N個節點的每一棵二叉樹,都將需要N+1個NULL指標 ``` typedef struct TreeNode *PtrToNode; typedef PtrToNode Tree; struct TreeNode { ElementType Element; Tree Left; Tree Right; } ``` ### 4.2.2 表示式樹 表示式樹的樹葉是運算元,比如常量或變數,而其它節點為操作符。 1. 中序遍歷inorder traversal(得到中綴表示式):遞迴的打印出左子樹,中間,右子樹 2. 後序遍歷postorder traversal(得到字尾表示式):遞迴的打印出左子樹,右子樹,中間 3. 先序遍歷preorder traversal(得到字首表示式):遞迴的打印出中間,左子樹,右子樹 構造一棵表示式樹 ``` void suffixExpression(char *inputStr) { int cnt, cnt2; Stack s = CreateStack(); for (cnt = 0; inputStr[cnt] != '\0'; cnt++) { if ((inputStr[cnt] >= '0') && (inputStr[cnt] <= '9')) { PtrToTreeHead numTree = CreateTreeNode(); numTree->Element = inputStr[cnt]; printf("Push %c\n", numTree->Element); Push(numTree, s); } for (cnt2 = 0; cnt2 < OPERATOR_TYPE; cnt2++) { if (inputStr[cnt] == Operator[cnt2]) { PtrToTreeHead operatorTree = CreateTreeNode(); operatorTree->Element = inputStr[cnt]; PtrToTreeHead num2Tree = Top(s); Pop(s); PtrToTreeHead num1Tree = Top(s);; Pop(s); operatorTree->LeftChild = num1Tree; operatorTree->RightChild = num2Tree; Push(operatorTree, s); printf("operator=%c, num1=%c, num2=%c\n", operatorTree->Element, num1Tree->Element, num2Tree->Element); } } } PtrToTreeHead printTree = Top(s); PrintTree(printTree); DrstroyTree(printTree); DistroyStack(s); } ``` ## 4.3 查詢樹ADT-二叉查詢樹 - 使二叉樹成為二叉查詢樹的性質是:對於樹中的每個節點X,它的左子樹中的所有關鍵字值都小於X的關鍵字值,而它的右子樹中所有關鍵字值大於X的關鍵字值。這意味著該樹所有的元素可以用某種統一的方式排序。 - 由於樹的遞迴定義,通常是遞迴的編寫這些操作的例程。因為二叉查詢樹的平均深度是O(logN),所以一般不必擔心棧空間被耗盡。 - 二叉查詢樹節點定義 ``` struct TreeNode; typedef struct TreeNode *Position; typedef Position SearchTree; struct TreeNode { ElementType Element; Search Left; Srarch Right } ``` - MakeEmpty ``` SearchTree MakeEmpty(SearchTree treeHead) { if (treeHead == NULL) { return NULL; } if (treeHead->Left != NULL) { MakeEmpty(treeHead->Left); } if (treeHead->Right != NULL) { MakeEmpty(treeHead->Right); } if (treeHead != NULL) { free(treeHead); } } ``` - Find: 注意測試的順序。==首先判斷是否為空樹,其次最不可能的情況應該安排在最後進行==。這裡使用尾遞迴,可以用一次賦值和一個goto語句代替。尾遞迴在這裡的使用是合理的,因為演算法表示式的簡明性是以速度的降低為代價的。而這裡使用的棧空間的量也只不過是O(logN)而已。 ``` SearchTree Find(SearchTree treeHead, ElementType Element) { if (treeHead == NULL) { return NULL; } if (Element < treeHead->Element) { return Find(treeHead->Left, Element); } else if (Element > treeHead->Element) { return Find(treeHead->Right, Element); } else { return treeHead; } } ``` - FindMin遞迴實現 ``` SearchTree FindMin(SearchTree treeHead) { if (treeHead == NULL) { return NULL; } if (treeHead->Left != NULL) { return FindMin(treeHead->Left); } else { return treeHead; } } ``` - FindMin非遞迴實現 ``` SearchTree FindMin(SearchTree treeHead) { if (treeHead == NULL) { return NULL; } SearchTree tmp = treeHead; while (tmp->Left != NULL) { tmp = tmp->Left; } return tmp; } ``` - FindMax遞迴實現 ``` SearchTree FindMax(SearchTree treeHead) { if (treeHead == NULL) { return NULL; } if (treeHead->Right != NULL) { return FindMin(treeHead->Right); } else { return treeHead; } } ``` - FindMax非遞迴實現 ``` SearchTree FindMax(SearchTree treeHead) { if (treeHead == NULL) { return NULL; } SearchTree tmp = treeHead; while (tmp->Right != NULL) { tmp = tmp->Right; } return tmp; } ``` - Insert: 重複元素不重複插入,儲存在某個輔助資料結構中即可,例如表 ``` SearchTree Insert(SearchTree treeHead, ElementType element) { if (treeHead == NULL) { treeHead = (SearchTree)malloc(sizeof(struct TreeNode)); if (treeHead == NULL) { return NULL; } memset(treeHead, 0, sizeof(struct TreeNode)); treeHead->Element = element; treeHead->Left = treeHead->Right = NULL; } else if (element < treeHead->Element) { treeHead->Left = Insert(treeHead->Left, element); } else if (element > treeHead->Element) { treeHead->Right = Insert(treeHead->Right, element); } return treeHead; } ``` - Delete: 如果節點是一片樹葉,那麼他可以立即被刪除。如果節點有一個兒子,則該節點可以在其父節點調整指標繞過該節點指向它的兒子的時候(原父節點的孫子節點)該節點可以被刪除。所刪除的節點不再引用,只有在指向它的指標已被省去的情況下才能夠被去掉。 ``` SearchTree Delete(SearchTree T, ElementType element) { SearchTree tmp; if (T == NULL) { printf("Couldn't find element\n"); return NULL; } else if (element < T->element) { T->Left = Delete(T->Left, element); } else if (element > T->element) { T->Right = Delete(T->Right, element); } else if (T->Left && T->Right) { tmp = T; T->Element = tmp->Element; T->Right = Delete(T->Right, element); } else { tmp = T; if (T->Left) { T = T->Left; } else if (T->Right) { T = T->Right; } free(tmp); } } ``` - 懶惰刪除:如果==刪除的次數不多==,則通常使用的策略是懶惰刪除。==即當一個元素要被刪除時,它仍留在樹中,而是隻做了個被刪除的記號==。這種做法特別是在有重複關鍵字時很流行,因為此時記錄出現頻率數的域可以減一。==如果樹中的實際節點數和“被刪除”的節點數相同,那麼樹的深度預計只上升一個小的常數==。因此,存在一個與懶惰刪除相關的非常小的時間損耗。再有,==如果被刪除的關鍵字是重新插入的==,那麼==分配一個新單元的開銷就避免了==。 ### 4.3.6 平均情形分析 - 除MakeEmpty外,我們期望上一節所有的操作都花費O(logN)時間,所有的操作都是O(d),其中d是包含所訪問的關鍵字的結點的深度。本節證明,假設所有的樹出現的機會均等。則樹的所有結點的平均深度為O(logN)。 - 一棵樹的所有節點的深度的和為==內部路徑長==。 - 令D(N)是具有N個節點的某棵樹T的內部路徑長。D(1) = 0。一棵N節點樹是由一棵i節點左子樹,和一棵(N-i-1)節點右子樹,以及深度為零的一個根節點組成。其