作者:Vamei 出處:http://www.cnblogs.com/vamei 歡迎轉載,也請保留這段聲明。謝謝!
樹的特征和定義
樹(Tree)是元素的集合。我們先以比較直觀的方式介紹樹。下面的數據結構是一個樹:
樹有多個節點(node),用以儲存元素。某些節點之間存在一定的關系,用連線表示,連線稱為邊(edge)。邊的上端節點稱為父節點,下端稱為子節點。樹像是一個不斷分叉的樹根。
每個節點可以有多個子節點(children),而該節點是相應子節點的父節點(parent)。比如說,3,5是6的子節點,6是3,5的父節點;1,8,7是3的子節點, 3是1,8,7的父節點。樹有一個沒有父節點的節點,稱為根節點(root),如圖中的6。沒有子節點的節點稱為葉節點(leaf),比如圖中的1,8,9,5節點。從圖中還可以看到,上面的樹總共有4個層次,6位於第一層,9位於第四層。樹中節點的最大層次被稱為深度。也就是說,該樹的深度(depth)為4。
如果我們從節點3開始向下看,而忽略其它部分。那么我們看到的是一個以節點3為根節點的樹:
三角形代表一棵樹
再進一步,如果我們定義孤立的一個節點也是一棵樹的話,原來的樹就可以表示為根節點和子樹(subtree)的關系:
上述觀察實際上給了我們一種嚴格的定義樹的方法:
1. 樹是元素的集合。
2. 該集合可以為空。這時樹中沒有元素,我們稱樹為空樹 (empty tree)。
3. 如果該集合不為空,那么該集合有一個根節點,以及0個或者多個子樹。根節點與它的子樹的根節點用一個邊(edge)相連。
上面的第三點是以遞歸的方式來定義樹,也就是在定義樹的過程中使用了樹自身(子樹)。由於樹的遞歸特征,許多樹相關的操作也可以方便的使用遞歸實現。我們將在后面看到。
(上述定義來自"Data Structures and Algorithm Analysis in C, by Mark Allen Weiss"。 我覺得有一點不太嚴格的地方。如果說空樹屬於樹,第三點應該是 “...以及0個和多個非空子樹...” )
樹的實現
樹的示意圖已經給出了樹的一種內存實現方式: 每個節點儲存元素和多個指向子節點的指針。然而,子節點數目是不確定的。一個父節點可能有大量的子節點,而另一個父節點可能只有一個子節點,而樹的增刪節點操作會讓子節點的數目發生進一步的變化。這種不確定性就可能帶來大量的內存相關操作,並且容易造成內存的浪費。
一種經典的實現方式如下:
樹的內存實現
擁有同一父節點的兩個節點互為兄弟節點(sibling)。上圖的實現方式中,每個節點包含有一個指針指向第一個子節點,並有另一個指針指向它的下一個兄弟節點。這樣,我們就可以用統一的、確定的結構來表示每個節點。
計算機的文件系統是樹的結構,比如Linux文件管理背景知識中所介紹的。在UNIX的文件系統中,每個文件(文件夾同樣是一種文件),都可以看做是一個節點。非文件夾的文件被儲存在葉節點。文件夾中有指向父節點和子節點的指針(在UNIX中,文件夾還包含一個指向自身的指針,這與我們上面見到的樹有所區別)。在git中,也有類似的樹狀結構,用以表達整個文件系統的版本變化 (參考版本管理三國志)。
文件樹
二叉搜索樹的C實現
二叉樹(binary)是一種特殊的樹。二叉樹的每個節點最多只能有2個子節點:
二叉樹
由於二叉樹的子節點數目確定,所以可以直接采用上圖方式在內存中實現。每個節點有一個左子節點(left children)和右子節點(right children)。左子節點是左子樹的根節點,右子節點是右子樹的根節點。
如果我們給二叉樹加一個額外的條件,就可以得到一種被稱作二叉搜索樹(binary search tree)的特殊二叉樹。二叉搜索樹要求:每個節點都不比它左子樹的任意元素小,而且不比它的右子樹的任意元素大。
(如果我們假設樹中沒有重復的元素,那么上述要求可以寫成:每個節點比它左子樹的任意節點大,而且比它右子樹的任意節點小)
二叉搜索樹,注意樹中元素的大小
二叉搜索樹可以方便的實現搜索算法。在搜索元素x的時候,我們可以將x和根節點比較:
1. 如果x等於根節點,那么找到x,停止搜索 (終止條件)
2. 如果x小於根節點,那么搜索左子樹
3. 如果x大於根節點,那么搜索右子樹
二叉搜索樹所需要進行的操作次數最多與樹的深度相等。n個節點的二叉搜索樹的深度最多為n,最少為log(n)。
下面是用C語言實現的二叉搜索樹,並有搜索,插入,刪除,尋找最大最小節點的操作。每個節點中存有三個指針,一個指向父節點,一個指向左子節點,一個指向右子節點。
(這樣的實現是為了方便。節點可以只保存有指向左右子節點的兩個指針,並實現上述操作。)
刪除節點相對比較復雜。刪除節點后,有時需要進行一定的調整,以恢復二叉搜索樹的性質(每個節點都不比它左子樹的任意元素小,而且不比它的右子樹的任意元素大)。
- 葉節點可以直接刪除。
- 刪除非葉節點時,比如下圖中的節點8,我們可以刪除左子樹中最大的元素(或者右樹中最大的元素),用刪除的節點來補充元素8產生的空缺。但該元素可能也不是葉節點,所以它所產生的空缺需要其他元素補充…… 直到最后刪除一個葉節點。上述過程可以遞歸實現。
刪除節點
刪除節點后的二叉搜索樹
/* By Vamei */
/* binary search tree */ #include <stdio.h> #include <stdlib.h> typedef struct node *position; typedef int ElementTP; struct node { position parent; ElementTP element; position lchild; position rchild; }; /* pointer => root node of the tree */ typedef struct node *TREE; void print_sorted_tree(TREE); position find_min(TREE); position find_max(TREE); position find_value(TREE, ElementTP); position insert_value(TREE, ElementTP); ElementTP delete_node(position); static int is_root(position); static int is_leaf(position); static ElementTP delete_leaf(position); static void insert_node_to_nonempty_tree(TREE, position); void main(void) { TREE tr; position np; ElementTP element; tr = NULL; tr = insert_value(tr, 18); tr = insert_value(tr, 5); tr = insert_value(tr, 2); tr = insert_value(tr, 8); tr = insert_value(tr, 81); tr = insert_value(tr, 101); printf("Original:\n"); print_sorted_tree(tr); np = find_value(tr, 8); if(np != NULL) { delete_node(np); printf("After deletion:\n"); print_sorted_tree(tr); } } /* * print values of the tree in sorted order */
void print_sorted_tree(TREE tr) { if (tr == NULL) return; print_sorted_tree(tr->lchild); printf("%d \n", tr->element); print_sorted_tree(tr->rchild); } /* * search for minimum value * traverse lchild */ position find_min(TREE tr) { position np; np = tr; if (np == NULL) return NULL; while(np->lchild != NULL) { np = np->lchild; } return np; } /* * search for maximum value * traverse rchild */ position find_max(TREE tr) { position np; np = tr; if (np == NULL) return NULL; while(np->rchild != NULL) { np = np->rchild; } return np; } /* * search for value * */ position find_value(TREE tr, ElementTP value) { if (tr == NULL) return NULL; if (tr->element == value) { return tr; } else if (value < tr->element) { return find_value(tr->lchild, value); } else { return find_value(tr->rchild, value); } } /* * delete node np */ ElementTP delete_node(position np) { position replace; ElementTP element; if (is_leaf(np)) { return delete_leaf(np); } else { /* if a node is not a leaf, then we need to find a replacement */ replace = (np->lchild != NULL) ? find_max(np->lchild) : find_min(np->rchild); element = np->element; np->element = delete_node(replace); return element; } } /* * insert a value into the tree * return root address of the tree */ position insert_value(TREE tr, ElementTP value) { position np; /* prepare the node */ np = (position) malloc(sizeof(struct node)); np->element = value; np->parent = NULL; np->lchild = NULL; np->rchild = NULL; if (tr == NULL) tr = np; else { insert_node_to_nonempty_tree(tr, np); } return tr; } //=============================================
/* * np is root? */
static int is_root(position np) { return (np->parent == NULL); } /* * np is leaf? */
static int is_leaf(position np) { return (np->lchild == NULL && np->rchild == NULL); } /* * if an element is a leaf, * then it could be removed with no side effect. */
static ElementTP delete_leaf(position np) { ElementTP element; position parent; element = np->element; parent = np->parent; if(!is_root(np)) { if (parent->lchild == np) { parent->lchild = NULL; } else { parent->rchild = NULL; } } free(np); return element; } /* * insert a node to a non-empty tree * called by insert_value() */
static void insert_node_to_nonempty_tree(TREE tr, position np) { /* insert the node */
if(np->element <= tr->element) { if (tr->lchild == NULL) { /* then tr->lchild is the proper place */ tr->lchild = np; np->parent = tr; return; } else { insert_node_to_nonempty_tree(tr->lchild, np); } } else if(np->element > tr->element) { if (tr->rchild == NULL) { tr->rchild = np; np->parent = tr; return; } else { insert_node_to_nonempty_tree(tr->rchild, np); } } }
運行結果:
Original:
2
5
8
18
81
101
After deletion:
2
5
18
81
101
上述實現中的刪除比較復雜。有一種簡單的替代操作,稱為懶惰刪除(lazy deletion)。在懶惰刪除時,我們並不真正從二叉搜索樹中刪除該節點,而是將該節點標記為“已刪除”。這樣,我們只用找到元素並標記,就可以完成刪除元素了。如果有相同的元素重新插入,我們可以將該節點找到,並取消刪除標記。
懶惰刪除的實現比較簡單,可以嘗試一下。樹所占據的內存空間不會因為刪除節點而減小。懶惰節點實際上是用內存空間換取操作的簡便性。
總結
樹, 二叉樹, 二叉搜索樹
二叉搜索樹的刪除
懶惰刪除
歡迎繼續閱讀“紙上談兵: 算法與數據結構”系列。