《資料結構與演算法分析》學習筆記-第四章-樹
阿新 • • 發佈:2021-02-19
[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)節點右子樹,以及深度為零的一個根節點組成。其