線性結構中的數據元素是一對一的關系,樹形結構是一對多的非線性結構,非常類似於自然界中的樹,數據元素之間既有分支關系,又有層次關系。樹形結構在現實世界中廣泛存在,如家族的家譜、一個單位的行政機構組織等都可以用樹形結構來形象地表示。樹形結構在計算機領域中也有着非常廣泛的應用,如 Windows 操作系統中對磁盤文件的管理、編譯程序中對源程序的語法結構的表示等都采用樹形結構。在數據庫系統中,樹形結構也是數據的重要組織形式之一。樹形結構有樹和二叉樹兩種,樹的操作實現比較復雜,但樹可以轉換為二叉樹進行處理,所以,我們主要討論二叉樹。
一:樹
1.1 樹的定義
樹(Tree)是 n(n≥0)個相同類型的數據元素的有限集合。樹中的數據元素叫結點(Node)。n=0 的樹稱為空樹(Empty Tree);對於 n>0 的任意非空樹 T 有:
(1)有且僅有一個特殊的結點稱為樹的根(Root)結點,根沒有前驅結點;
(2)若n>1,則除根結點外,其余結點被分成了m(m>0)個互不相交的集合T 1 ,T 2 ,…,T m ,其中每一個集合T i (1≤i≤m)本身又是一棵樹。樹T 1 ,T 2 ,…,T m
稱為這棵樹的子樹(Subtree)。
由樹的定義可知,樹的定義是遞歸的,用樹來定義樹。因此,樹(以及二叉樹)的許多算法都使用了遞歸。
樹的形式定義為:樹(Tree)簡記為 T,是一個二元組,
T = (D, R)
其中:D 是結點的有限集合;
R 是結點之間關系的有限集合。
圖 1.1
從樹的定義和上圖的示例可以看出,樹具有下面兩個特點:
(1)樹的根結點沒有前驅結點,除根結點之外的所有結點有且只有一個前驅結點。
(2)樹中的所有結點都可以有零個或多個后繼結點。
實際上,第(1)個特點表示的就是樹形結構的“一對多關系”中的“一”,第(2)特點表示的是“多”。
由此特點可知,下圖 所示的都不是樹。
1.2 樹的相關術語
1、結點(Node):表示樹中的數據元素,由數據項和數據元素之間的關系組成。在圖 1.1中,共有 10 個結點。
2、結點的度(Degree of Node):結點所擁有的子樹的個數,在圖 1.1 中,結點 A 的度為 3。
3、樹的度(Degree of Tree):樹中各結點度的最大值。在圖 1.1 中,樹的度為3。
4、葉子結點(Leaf Node):度為 0 的結點,也叫終端結點。在圖 1.1 中,結點 E、F、G、H、I、J 都是葉子結點。
5、分支結點(Branch Node):度不為 0 的結點,也叫非終端結點或內部結點。在圖 1.1 中,結點 A、B、C、D 是分支結點。
6、孩子(Child):結點子樹的根。在圖 1.1 中,結點 B、C、D 是結點 A 的孩子。
7、雙親(Parent):結點的上層結點叫該結點的雙親。在圖 1.1 中,結點 B、C、D 的雙親是結點 A。
8、祖先(Ancestor):從根到該結點所經分支上的所有結點。在圖 1.1 中,結點 E 的祖先是 A 和 B。
9、子孫(Descendant):以某結點為根的子樹中的任一結點。在圖 1.1 中,除A 之外的所有結點都是 A 的子孫。
10、兄弟(Brother):同一雙親的孩子。在圖 1.1 中,結點 B、C、D 互為兄弟。
11、結點的層次(Level of Node):從根結點到樹中某結點所經路徑上的分支數稱為該結點的層次。根結點的層次規定為 1,其余結點的層次等於其雙親結點的層次加 1。
12、堂兄弟(Sibling):同一層的雙親不同的結點。在圖 1.1 中,G 和 H 互為堂兄弟。
13、樹的深度(Depth of Tree):樹中結點的最大層次數。在圖 1.1 中,樹的深度為 3。
14、無序樹(Unordered Tree):樹中任意一個結點的各孩子結點之間的次序構成無關緊要的樹。通常樹指無序樹。
15、有序樹(Ordered Tree):樹中任意一個結點的各孩子結點有嚴格排列次序的樹。二叉樹是有序樹,因為二叉樹中每個孩子結點都確切定義為是該結點的左孩子結點還是右孩子結點。
16、森林(Forest):m(m≥0)棵樹的集合。自然界中的樹和森林的概念差別很大,但在數據結構中樹和森林的概念差別很小。從定義可知,一棵樹有根結點和m 個子樹構成,若把樹的根結點刪除,則樹變成了包含 m 棵樹的森林。當然,根據定義,一棵樹也可以稱為森林。
1.3 樹的邏輯表示
樹的邏輯表示方法很多,下面是常見的表示方法。
1、直觀表示法
它象日常生活中的樹木一樣。整個圖就象一棵倒立的樹,從根結點出發不斷擴展,根結點在最上層,葉子結點在最下面,如圖 1.1 所示。
2、凹入表示法
每個結點對應一個矩形,所有結點的矩形都右對齊,根結點用最長的矩形表示,同一層的結點的矩形長度相同,層次越高,矩形長度越短,圖 1.1 中的樹的凹入表示法如下
3、廣義表表示法
用廣義表的形式表示根結點排在最前面,用一對圓括號把它的子樹結點括起來,子樹結點用逗號隔開。圖 1.1 的樹的廣義表表示如下:
(A(B(E,F,G),C(H),D(I,J)))
4、嵌套表示法
類似數學中所說的文氏圖表示法,如下圖 所示。
二:二叉樹
2.1 二叉樹的定義
二叉樹(Binary Tree)是 n(n≥0)個相同類型的結點的有限集合。n=0 的二叉樹稱為空二叉樹(Empty Binary Tree);對於 n>0 的任意非空二叉樹有:
(1)有且僅有一個特殊的結點稱為二叉樹的根(Root)結點,根沒有前驅結點;
(2)若n>1,則除根結點外,其余結點被分成了 2 個互不相交的集合T L ,T R ,而T L 、T R 本身又是一棵二叉樹,分別稱為這棵二叉樹的左子樹(Left Subtree)和右子樹(Right Subtree)。
二叉樹的形式定義為:二叉樹(Binary Tree)簡記為 BT,是一個二元組,
BT = (D, R)
其中:D 是結點的有限集合;
R 是結點之間關系的有限集合。
由樹的定義可知,二叉樹是另外一種樹形結構,並且是有序樹,它的左子樹和右子樹有嚴格的次序,若將其左、右子樹顛倒,就成為另外一棵不同的二叉樹。因此,圖 (a)和圖 (b)所示是不同的二叉樹。
二叉樹的形態共有 5 種:空二叉樹、只有根結點的二叉樹、右子樹為空的二叉樹、左子樹為空的二叉樹和左、右子樹非空的二叉樹。二叉樹的 5 種形態如下圖所示。
三種特殊的二叉樹:
(1)完美二叉樹(Perfect Binary Tree):Every node except the leaf nodes have two children and every level (last level too) is completely filled. 除了葉子結點之外的每一個結點都有兩個孩子,每一層(當然包含最后一層)都被完全填充。
(2)完全二叉樹(Complete Binary Tree):Every level except the last level is completely filled and all the nodes are left justified. 除了最后一層之外的其他每一層都被完全填充,並且所有結點都保持向左對齊。
(若設二叉樹的深度為h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層所有的結點都連續集中在最左邊,這就是完全二叉樹。【來源百度百科】)
這是一種有些難以理解的特殊二叉樹,首先從字面上要區分,“完全”和“滿”的差異,滿二叉樹一定是一棵完全二叉樹,但完全二叉樹不一定是滿的。
(3)完滿二叉樹(Full Binary Tree):Every node except the leaf nodes have two children. 除了葉子結點之外的每一個結點都有兩個孩子結點。
完滿(Full)二叉樹 v.s. 完全(Complete)二叉樹 v.s. 完美(Perfect)二叉樹
2.2 二叉樹的特性
性質 1 :版本一:若二叉樹的層次從0開始,則在二叉樹的第i層至多有2^i個結點(i>=0)。【Thomas和Charles等人寫的《算法導論》和 Robert Sedgewick所著的《算法》從 level 0 開始定義】
版本二:若二叉樹的層次從1開始,則在二叉樹的第i層至多有2^(i-1)個結點(i>=1)。【嚴蔚敏老師的《數據結構》則是從level 1開始定義的】
性質 2: 若規定空樹的深度為 0,則深度為k的二叉樹最多有 2^k -1 個結點(滿二叉樹)(k≥0)。
性質 3 :具有n個結點的完全二叉樹的深度k為log 2 n+1。
性質 4: 對於一棵非空二叉樹,如果葉子結點(度為0)數目為m ,度為 2 的結點數目為n,則有m= n +1。
性質 5: 對於具有 n 個結點的完全二叉樹,如果按照從上到下和從左到右的順序對所有結點從 1 開始編號,則對於序號為 i 的結點,有:
(1)如果 i>1,則序號為 i 的結點的雙親結點的序號為 i/2(“/”表示整除);如果 i=1,則該結點是根結點,無雙親結點。
(2)如果 2i≤n,則該結點的左孩子結點的序號為 2i;若 2i>n,則該結點無左孩子。
(3)如果 2i+1≤n,則該結點的右孩子結點的序號為 2i+1;若 2i+1>n,則該結點無右孩子
2.3 二叉樹的存儲結構
二叉樹的存儲結構主要有三種:順序存儲結構、二叉鏈表存儲結構和三叉鏈表存儲結構。
2.3.1:二叉樹的順序存儲結構
對於一棵完全二叉樹,由性質 5 可計算得到任意結點 i 的雙親結點序號、左孩子結點序號和右孩子結點序號。所以,完全二叉樹的結點可按從上到下和從左到右的順序存儲在一維數組中,其結點間的關系可由性質 5 計算得到,這就是二叉樹的順序存儲結構。圖 (a)所示的二叉樹的順序存儲結構為:
但是,對於一棵非完全二叉樹,不能簡單地按照從上到下和從左到右的順序存放在一維數組中,因為數組下標之間的關系不能反映二叉樹中結點之間的邏輯關系。所以,應該對一棵非完全二叉樹進行改造,增加空結點(並不存在的結點)使之成為一棵完全二叉樹,然后順序存儲在一維數組中。圖 (b)是圖 (a)的順序存儲示意圖
顯然,順序存儲對於需增加很多空結點才能改造為一棵完全二叉樹的二叉樹不適合,因為會造成空間的大量浪費。實際上,采用順序存儲結構,是對非線性的數據結構線性化,用線性結構來表示二叉樹的結點之間的邏輯關系,所以,需要增加空間。一般來說,有大約一半的空間被浪費。最差的情況是右單支樹,如下圖 所示,一棵深度為k的右單支樹,只有k個結點,卻需要分配 2 k -1 個存儲單元。
2.3.2:二叉樹的二叉鏈表存儲結構
二叉樹的二叉鏈表存儲結構是指二叉樹的結點有三個域:一個數據域和兩個引用域,數據域存儲數據,兩個引用域分別存放其左、右孩子結點的地址。當左孩子或右孩子不存在時,相應域為空,用符號 NULL 或∧表示。結點的存儲結構如下所示:
下圖是圖2.3.1(a)所示的二叉樹的二叉鏈表示意圖。圖 (a)是不帶頭結點的二叉鏈表,圖 (b)是帶頭結點的二叉鏈表。
由上圖所示的二叉樹有 4 個結點,每個結點中有兩個引用,共有 8 個引用,其中 3 個引用被使用,5 個引用是空的。由性質 4 可知:由 n 個結點構成的二叉鏈表中,只有 n-1 個引用域被使用,還有 n+1 個引用域是空的。
2.3.3:二叉樹的三叉鏈表存儲結構
使用二叉鏈表,可以非常方便地訪問一個結點的子孫結點,但要訪問祖先結點非常困難。可以考慮在每個結點中再增加一個引用域存放其雙親結點的地址信息,這樣就可以通過該引用域非常方便地訪問其祖先結點。這就是下面要介紹的三叉鏈表。
二叉樹的三叉鏈表存儲結構是指二叉樹的結點有四個域:一個數據域和三個引用域,數據域存儲數據,三個引用域分別存放其左、右孩子結點和雙親結點的地址。當左、右孩子或雙親結點不存在時,相應域為空,用符號 NULL 或∧表示。結點的存儲結構如下所示:
下圖 (a)是不帶頭結點的三叉鏈表,圖 (b)是帶頭結點的三叉鏈表。
2.4 二叉鏈表存儲結構的類實現
二叉樹的二叉鏈表的結點類有 3 個成員字段:數據域字段 data、左孩子引用域字段 lChild 和右孩子引用域字段 rChild。二叉樹的二叉鏈表的結點類的實現如下所示。

1 public class Node<T> 2 { 3 public T Data { get; set; } 4 public Node<T> LChild { get; set; } 5 public Node<T> RChild { get; set; } 6 7 public Node(T data, Node<T> lp, Node<T> rp) 8 { 9 Data = data; 10 LChild = lp; 11 RChild = rp; 12 } 13 14 public Node(Node<T> lp, Node<T> rp) 15 { 16 Data = default(T); 17 LChild = lp; 18 RChild = rp; 19 } 20 21 public Node(T data) 22 { 23 Data = data; 24 LChild = null; 25 RChild = null; 26 } 27 28 public Node() 29 { 30 Data = default(T); 31 LChild = null; 32 RChild = null; 33 } 34 }
不帶頭結點的二叉樹的二叉鏈表比帶頭結點的二叉樹的二叉鏈表的區別與不帶頭結點的單鏈表與帶頭結點的單鏈表的區別一樣。下面只介紹不帶頭結點的二叉樹的二叉鏈表的類 BiTree<T>。BiTree<T>類只有一個成員字段 head 表示頭引用。以下是 BiTree<T>類的實現。

1 public class BiTree<T> 2 { 3 //頭引用屬性 4 public Node<T> Head { get; set; } 5 6 //構造器 7 public BiTree() 8 { 9 Head = null; 10 } 11 12 //構造器 13 public BiTree(T val) 14 { 15 Node<T> p = new Node<T>(val); 16 Head = p; 17 } 18 19 //構造器 20 public BiTree(Node<T> lp, Node<T> rp) 21 { 22 Node<T> p = new Node<T>(lp, rp); 23 Head = p; 24 } 25 26 //構造器 27 public BiTree(T val, Node<T> lp, Node<T> rp) 28 { 29 Node<T> p = new Node<T>(val, lp, rp); 30 Head = p; 31 } 32 33 //判斷是否是空二叉樹 34 public bool IsEmpty() 35 { 36 if (Head == null) 37 { 38 return true; 39 } 40 else 41 { 42 return false; 43 } 44 } 45 46 //獲取根結點 47 public Node<T> Root() 48 { 49 return Head; 50 } 51 52 //獲取結點的左孩子結點 53 public Node<T> GetLChild(Node<T> p) 54 { 55 return p.LChild; 56 } 57 58 //獲取結點的右孩子結點 59 public Node<T> GetRChild(Node<T> p) 60 { 61 return p.RChild; 62 } 63 64 //將結點p的左子樹插入值為val的新結點, 65 //原來的左子樹成為新結點的左子樹 66 public void InsertL(T val, Node<T> p) 67 { 68 Node<T> tmp = new Node<T>(val); 69 tmp.LChild = p.LChild; 70 p.LChild = tmp; 71 } 72 73 //將結點p的右子樹插入值為val的新結點, 74 //原來的右子樹成為新結點的右子樹 75 public void InsertR(T val, Node<T> p) 76 { 77 Node<T> tmp = new Node<T>(val); 78 tmp.RChild = p.RChild; 79 p.RChild = tmp; 80 } 81 82 //若p非空,刪除p的左子樹 83 public Node<T> DeleteL(Node<T> p) 84 { 85 if ((p == null) || (p.LChild == null)) 86 { 87 return null; 88 } 89 Node<T> tmp = p.LChild; 90 p.LChild = null; 91 return tmp; 92 } 93 94 //若p非空,刪除p的右子樹 95 public Node<T> DeleteR(Node<T> p) 96 { 97 if ((p == null) || (p.RChild == null)) 98 { 99 return null; 100 } 101 Node<T> tmp = p.RChild; 102 p.RChild = null; 103 return tmp; 104 } 105 106 //判斷是否是葉子結點 107 public bool IsLeaf(Node<T> p) 108 { 109 if ((p != null) && (p.LChild == null) && (p.RChild == null)) 110 { 111 return true; 112 } 113 else 114 { 115 return false; 116 } 117 } 118 }
2.5 二叉樹的遍歷
實際上,遍歷是將二叉樹中的結點信息由非線性排列變為某種意義上的線性排列。也就是說,遍歷操作使非線性結構線性化。
由二叉樹的定義可知,一棵二叉樹由根結點、左子樹和右子樹三部分組成,若規定 D、L、R 分別代表遍歷根結點、遍歷左子樹、遍歷右子樹,則二叉樹的遍歷方式有 6 種:DLR、DRL、LDR、LRD、RDL、RLD。由於先遍歷左子樹和先遍歷右子樹在算法設計上沒有本質區別,所以,只討論三種方式:DLR(先序遍歷)、LDR(中序遍歷)和 LRD(后序遍歷)。
除了這三種遍歷方式外,還有一種方式:層序遍歷(Level Order)。層序遍歷是從根結點開始,按照從上到下、從左到右的順序依次訪問每個結點一次僅一次。
1、先序遍歷(DLR)
先序遍歷的基本思想是:首先訪問根結點,然后先序遍歷其左子樹,最后先序遍歷其右子樹。先序遍歷的遞歸算法實現如下,注意:這里的訪問根結點是把根結點的值輸出到控制台上。當然,也可以對根結點作其它處理。
完全二叉樹

1 public static void PreOrder<T>(Node<T> root) 2 { 3 //根結點為空 4 if (root == null) 5 { 6 return; 7 } 8 9 //處理根結點 10 Console.WriteLine("{0}", root.Data); 11 12 //先序遍歷左子樹 13 PreOrder(root.LChild); 14 15 //先序遍歷右子樹 16 PreOrder(root.RChild); 17 }
對於上圖所示的完全二叉樹,按先序遍歷所得到的結點序列為:A B D H I E J C F G
2、中序遍歷(LDR)
中序遍歷的基本思想是:首先中序遍歷根結點的左子樹,然后訪問根結點,最后中序遍歷其右子樹。中序遍歷的遞歸算法實現如下:

1 public static void InOrder<T>(Node<T> root) 2 { 3 //根結點為空 4 if (root == null) 5 { 6 return; 7 } 8 //中序遍歷左子樹 9 InOrder(root.LChild); 10 //處理根結點 11 Console.WriteLine("{0}", root.Data); 12 //中序遍歷右子樹 13 InOrder(root.RChild); 14 }
對於上圖所示的完全二叉樹,按中序遍歷所得到的結點序列為:H D I B J E A F C G
3、后序遍歷(LRD)
后序遍歷的基本思想是:首先后序遍歷根結點的左子樹,然后后序遍歷根結點的右子樹,最后訪問根結點。后序遍歷的遞歸算法實現如下:

1 public void PostOrder<T>(Node<T> root) 2 { 3 //根結點為空 4 if (root == null) 5 { 6 return; 7 } 8 9 //先序遍歷左子樹 10 PostOrder(root.LChild); 11 12 //先序遍歷右子樹 13 PostOrder(root.RChild); 14 15 //處理根結點 16 Console.Write("{0} ", root.Data); 17 }
對於上圖所示的二叉樹,按后序遍歷所得到的結點序列為:H I D J E B F G C A
4、層序遍歷(Level Order)
層序遍歷的基本思想是:由於層序遍歷結點的順序是先遇到的結點先訪問,與隊列操作的順序相同。所以,在進行層序遍歷時,設置一個隊列,將根結點引用入隊,當隊列非空時,循環執行以下三步:
(1) 從隊列中取出一個結點引用,並訪問該結點;
(2) 若該結點的左子樹非空,將該結點的左子樹引用入隊;
(3) 若該結點的右子樹非空,將該結點的右子樹引用入隊;
層序遍歷的算法實現如下:

1 public static void LevelOrder<T>(Node<T> root) 2 { 3 //根結點為空 4 if (root == null) 5 { 6 return; 7 } 8 9 //設置一個隊列保存層序遍歷的結點 10 CSeqQueue<Node<T>> sq = new CSeqQueue<Node<T>>(50); 11 12 //根結點入隊 13 sq.In(root); 14 15 //隊列非空,結點沒有處理完 16 while (!sq.IsEmpty()) 17 { 18 //結點出隊 19 Node<T> tmp = sq.Out(); 20 //處理當前結點 21 Console.WriteLine("{0}", tmp.Data); 22 //將當前結點的左孩子結點入隊 23 if (tmp.LChild != null) 24 { 25 sq.In(tmp.LChild); 26 } 27 //將當前結點的右孩子結點入隊 28 if (tmp.RChild != null) 29 { 30 sq.In(tmp.RChild); 31 } 32 } 33 }
對於上圖所示的二叉樹,按層次遍歷所得到的結點序列為:A B C D E F G H I J
2.6 二叉樹的應用
實際場景使用上,用的最多的是二叉平衡樹,有種特殊的二叉平衡樹就是紅黑樹,Java集合中的TreeSet和TreeMap,C++STL中的set,map以及LInux虛擬內存的管理,都是通過紅黑樹去實現的,還有哈弗曼樹編碼方面的應用,以及B-Tree,B+-Tree在文件系統中的應用。當然二叉查找樹可以用來查找和排序。
二叉樹在搜索上的優勢
數組的搜索比較方便,可以直接使用下標,但刪除或者插入就比較麻煩了,而鏈表與之相反,刪除和插入都比較簡單,但是查找很慢,這自然也與這兩種數據結構的存儲方式有關,數組是取一段相連的空間,而鏈表是每創建一個節點便取一個節點所需的空間,只是使用指針進行連接,空間上並不是連續的。而二叉樹就既有鏈表的好處,又有數組的優點。
2.6.1 二叉查找樹
二叉查找樹具有很高的靈活性,對其優化可以生成平衡二叉樹,紅黑樹等高效的查找和插入數據結構,后文會介紹。
1: 定義
二叉查找樹(Binary Search Tree),也稱有序二叉樹(ordered binary tree),排序二叉樹(sorted binary tree),是指一棵空樹或者具有下列性質的二叉樹:
1. 若任意節點的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
2. 若任意節點的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;
3. 任意節點的左、右子樹也分別為二叉查找樹。
4. 沒有鍵值相等的節點(no duplicate nodes)。
如下圖,在二叉樹的基礎上,加上節點之間的大小關系,就是二叉查找樹
從圖中可以看出,二叉查找樹中,最左和最右節點即為最小值和最大值
2: 查找
查找操作和二分查找類似,將key和節點的key比較,如果小於,那么就在左節點查找,如果大於,則在右節點查找,如果相等,直接返回Value。
C# 迭代實現

1 /// <summary> 2 /// 二叉查找樹查找 3 /// </summary> 4 /// <param name="bt">二叉樹</param> 5 /// <param name="key">目標值</param> 6 /// <returns>0:查找成功,1:查找失敗</returns> 7 public int Search(BiTree<int> bt, int key) 8 { 9 Node<int> p; 10 //二叉排序樹為空 11 if (bt.IsEmpty() == true) 12 { 13 Console.WriteLine("The Binary Sorting Tree is empty!"); 14 return 1; 15 } 16 p = bt.Head; 17 //二叉排序樹非空 18 while (p != null) 19 { 20 //存在要查找的記錄 21 if (p.Data == key) 22 { 23 Console.WriteLine("Search is Successful!"); 24 return 0; 25 } 26 //待查找記錄的關鍵碼大於結點的關鍵碼 27 else if (p.Data < key) 28 { 29 p = p.RChild; 30 } 31 //待查找記錄的關鍵碼小於結點的關鍵碼 32 else 33 { 34 p = p.LChild; 35 } 36 } 37 38 return 1; 39 }
3: 插入
插入和查找類似,首先查找有沒有和key相同的,如果有,更新;如果沒有找到,那么創建新的節點。並更新每個節點的Number值,代碼實現如下:
C#實現

1 /// <summary> 2 /// 二叉查找樹插入 3 /// </summary> 4 /// <param name="bt">二叉樹</param> 5 /// <param name="key">目標值</param> 6 /// <returns>0:查找成功,1:查找失敗</returns> 7 public int Insert(BiTree<int> bt, int key) 8 { 9 Node<int> p; 10 Node<int> parent = new Node<int>();//插入節點的父級 11 p = bt.Head; 12 while (p != null) 13 { 14 //存在關鍵碼等於key的結點 15 if (p.Data == key) 16 { 17 Console.WriteLine("Record is exist!"); 18 return 1; 19 } 20 parent = p; 21 //記錄的關鍵碼大於結點的關鍵碼 22 if (p.Data < key) 23 { 24 p = p.RChild; 25 } 26 //記錄的關鍵碼小於結點的關鍵碼 27 else 28 { 29 p = p.LChild; 30 } 31 } 32 33 p = new Node<int>(key); 34 //二叉查找樹為空 35 if (parent == null) 36 { 37 bt.Head = parent; 38 } 39 //待插入記錄的關鍵碼小於結點的關鍵碼 40 else if (p.Data < parent.Data) 41 { 42 parent.LChild = p; 43 } 44 //待插入記錄的關鍵碼大於結點的關鍵碼 45 else 46 { 47 parent.RChild = p; 48 } 49 return 0; 50 }
隨機插入形成樹的動畫如下,可以看到,插入的時候樹還是能夠保持近似平衡狀態:
4: 刪除
二叉排序樹的刪除情況如下圖所示。
C# 實現

1 /// <summary> 2 /// 二叉查找樹刪除 3 /// </summary> 4 /// <param name="bt"></param> 5 /// <param name="key"></param> 6 /// <returns></returns> 7 public int Delete(BiTree<int> bt, int key) 8 { 9 Node<int> p; 10 Node<int> parent = new Node<int>(); 11 Node<int> s = new Node<int>(); 12 Node<int> q = new Node<int>(); 13 //二叉排序樹為空 14 if (bt.IsEmpty() == true) 15 { 16 Console.WriteLine("The Binary Sorting is empty!"); 17 return 1; 18 } 19 p = bt.Head; 20 parent = p; 21 //二叉排序樹非空 22 while (p != null) 23 { 24 //存在關鍵碼等於key的結點 25 if (p.Data == key) 26 { 27 //結點為葉子結點 28 if (bt.IsLeaf(p)) 29 { 30 if (p == bt.Head) 31 { 32 bt.Head = null; 33 } 34 else if (p == parent.LChild) 35 { 36 parent.LChild = null; 37 } 38 else 39 { 40 parent.RChild = null; 41 } 42 } 43 //結點的右子結點為空而左子結點非空 44 else if ((p.RChild == null) && (p.LChild != null)) 45 { 46 if (p == parent.LChild) 47 { 48 parent.LChild = p.LChild; 49 } 50 else 51 { 52 parent.RChild = p.LChild; 53 } 54 } 55 //結點的左子結點為空而右子結點非空 56 else if ((p.LChild == null) && (p.RChild != null)) 57 { 58 if (p == parent.LChild) 59 { 60 parent.LChild = p.RChild; 61 } 62 else 63 { 64 parent.RChild = p.RChild; 65 } 66 } 67 //結點的左右子結點均非空 68 else 69 { 70 q = p; 71 s = p.RChild; 72 while (s.LChild != null) 73 { 74 q = s; 75 s = s.LChild; 76 } 77 p.Data = s.Data; 78 if (q != p) 79 { 80 q.LChild = s.RChild; 81 } 82 else 83 { 84 q.RChild = s.RChild; 85 } 86 } 87 return 0; 88 } 89 //待刪除記錄的關鍵碼大於結點的關鍵碼 90 else if (p.Data < key) 91 { 92 parent = p; 93 p = p.RChild; 94 } 95 else 96 { 97 parent = p; 98 p = p.LChild; 99 } 100 } 101 return -1; 102 }
以上二叉查找樹的刪除節點的算法不是完美的,因為隨着刪除的進行,二叉樹會變得不太平衡,下面是動畫演示。
二叉查找樹和二分查找一樣,插入和查找的時間復雜度均為lgN,但是在最壞的情況下仍然會有N的時間復雜度。原因在於插入和刪除元素的時候,樹沒有保持平衡。我們追求的是在最壞的情況下仍然有較好的時間復雜度,這就是平衡查找樹了。
2.7 樹與森林
2.7.1樹的存儲
樹的存儲結構包括順序存儲結構和鏈式存儲結構但無論采用哪種存儲結構,都要求存儲結構不但能存儲結點本身的信息,還能存儲樹中各結點之間的邏輯關系。
1、雙親表示法
從樹的定義可知,除根結點外,樹中的每個結點都有唯一的一個雙親結點。根據這一特性,可用一組連續的存儲空間(一維數組)存儲樹中的各結點。樹中的結點除保存結點本身的信息之外,還要保存其雙親結點在數組中的位置(數組的下標),樹的這種表示法稱為雙親表示法。
由於樹的結點只保存兩個信息,所以樹的結點用結構體 PNode<T>來表示。結構中有兩個字段:數據字段 data 和雙親位置字段 pPos。而樹類 PTree<T>只有一個成員數組字段 nodes,用於保存結點。
樹的雙親表示法的結點的結構如下所示:
樹的雙親表示法的結點的結構體 PNode<T>和樹類 PTree<T>的定義如下:

1 public struct PNode<T> 2 { 3 public T data; 4 public int pPos; 5 … 6 } 7 public class PTree<T> 8 { 9 private PNode<T>[] nodes; 10 … 11 }
下圖分別為樹結構和樹雙親表示法
樹的雙親表示法對於實現 Parent(t)操作和 Root()操作非常方便。Parent(t)操作可以在常量時間內實現,反復調用 Parent(t)操作,直到遇到無雙親的結點(其 pPos值為-1)時,便找到了樹的根,這就是 Root()操作的執行過程。但要實現查找孩子結點和兄弟結點等操作非常困難,因為這需要查詢整個數組。要實現這些操作,需要在結點結構中增設存放第1個孩子在數組中的序號的域和存放第1個兄弟在數組中的序號的域。
2、孩子鏈表表示法
孩子鏈表表示法也是用一維數組來存儲樹中各結點的信息。但結點的結構與雙親表示法中結點的結構不同,孩子鏈表表示法中的結點除保存本身的信息外,不是保存其雙親結點在數組中的序號,而是保存一個鏈表的第一個結點的地址信息。這個鏈表是由該結點的所有孩子結點組成。每個孩子結點保存有兩個信息,一個是每個孩子結點在一維數組中的序號,另一個是下一個孩子結點的地址信息。
孩子結點的結構如下所示:
孩子結點類 ChildNode 的定義如下

1 public class ChildNode 2 { 3 private int index; 4 private ChildNode nextChild; 5 … 6 }
樹結構和樹孩子鏈表表示法如下圖:
樹的孩子鏈表表示法對於實現查找孩子結點等操作非常方便,但對於實現查找雙親結點、兄弟結點等操作則比較困難。
3、孩子兄弟表示法
這是一種常用的數據結構,又稱二叉樹表示法,或二叉鏈表表示法,即以二叉鏈表作為樹的存儲結構。每個結點除存儲本身的信息外,還有兩個引用域分別存儲該結點第一個孩子的地址信息和下一個兄弟的地址信息。樹類 CSTree<T>只有一個成員字段 head,表示頭引用。
樹的孩子兄弟表示法的結點的結構如下所示:
樹的孩子兄弟表示法的結點類 CSNode<T>的定義如下:

1 public class CSNode<T> 2 { 3 private T data; 4 private CSNode<T> firstChild; 5 private CSNode<T> nextSibling; 6 … 7 }
樹類 CSTree<T>的定義如下:

1 public class CSTree<T> 2 { 3 private CSNode<T> head;; 4 … 5 }
樹的孩子兄弟表示法如下
樹的孩子兄弟表示法對於實現查找孩子、兄弟等操作非常方便,但對於實現查找雙親結點等操作則非常困難。如果在樹的結點中再增加一個域來存儲孩子的雙親結點的地址信息,則就可以較方便地實現上述操作了。
2.7.2 樹、森林與二叉樹的轉換
從樹的孩子兄弟表示法可知,樹可以用二叉鏈表進行存儲,所以,二叉鏈表可以作為樹和二叉樹之間的媒介。也就是說,借助二叉鏈表,樹和二叉樹可以相互進行轉換。從物理結構來看,它們的二叉鏈表是相同的,只是解釋不同而已。並且,如果設定一定的規則,就可用二叉樹來表示森林,森林和二叉樹也可以相互進行轉換。
1、樹轉換為二叉樹
由於二叉樹是有序的,為了避免混淆,對於無序樹,我們約定樹中的每個結點的孩子結點按從左到右的順序進行編號。如上圖所示的樹,根結點 A 有三個孩子 B、C、D,規定結點 B 是結點 A 的第一個孩子,結點 C 是結點 A 的第 2個孩子,結點 D 是結點 A 的第 3 個孩子。
將樹轉換成二叉樹的步驟是:
(1)加線。就是在所有兄弟結點之間加一條連線;
(2)抹線。就是對樹中的每個結點,只保留他與第一個孩子結點之間的連
線,刪除它與其它孩子結點之間的連線;
(3)旋轉。就是以樹的根結點為軸心,將整棵樹順時針旋轉一定角度,使
之結構層次分明。
下圖是樹轉換為二叉樹的轉換過程示意圖。
2、森林轉換為二叉樹
森林是由若干棵樹組成,可以將森林中的每棵樹的根結點看作是兄弟,由於每棵樹都可以轉換為二叉樹,所以森林也可以轉換為二叉樹。
將森林轉換為二叉樹的步驟是:
(1)先把每棵樹轉換為二叉樹;
(2)第一棵二叉樹不動,從第二棵二叉樹開始,依次把后一棵二叉樹的根結點作為前一棵二叉樹的根結點的右孩子結點,用線連接起來。當所有的二叉樹連接起來后得到的二叉樹就是由森林轉換得到的二叉樹。
森林轉換為二叉樹的轉換過程示意圖如下:
3、二叉樹轉換為樹
二叉樹轉換為樹是樹轉換為二叉樹的逆過程,其步驟是:
(1)若某結點的左孩子結點存在,將左孩子結點的右孩子結點、右孩子結
點的右孩子結點……都作為該結點的孩子結點,將該結點與這些右孩子結點用線
連接起來;
(2)刪除原二叉樹中所有結點與其右孩子結點的連線;
(3)整理(1)和(2)兩步得到的樹,使之結構層次分明。
二叉樹轉換為樹的過程示意圖如下:
4、二叉樹轉換為森林
二叉樹轉換為森林比較簡單,其步驟如下:
(1)先把每個結點與右孩子結點的連線刪除,得到分離的二叉樹;
(2)把分離后的每棵二叉樹轉換為樹;
(3)整理第(2)步得到的樹,使之規范,這樣得到森林。
2.7.3 樹和森林的遍歷
1、樹的遍歷
樹的遍歷通常有兩種方式:
(1)先序遍歷,即先訪問樹的根結點,然后依次先序遍歷樹中的每棵子樹。
(2)后序遍歷,即先依次后序遍歷樹中的每棵子樹,然后訪問根結點。
對上圖中的樹所示的樹進行先序遍歷所得到的結點序列為:A B E F G C H D I J
對此樹進行后序遍歷得到的結點序列為:E F G B H C I J D A
根據樹與二叉樹的轉換關系以及二叉樹的遍歷定義可以推知,樹的先序遍歷與其轉換的相應的二叉樹的先序遍歷的結果序列相同;樹的后序遍歷與其轉換的二叉樹的中序遍歷的結果序列相同;樹的層序遍歷與其轉換的二叉樹的后序遍歷的結果序列相同。因此,樹的遍歷算法可以采用相應的二叉樹的遍歷算法來實現。
2、森林的遍歷
森林的遍歷有兩種方式。
(1)先序遍歷,即先訪問森林中第一棵樹的根結點,然后先序遍歷第一棵樹中的每棵子樹,最后先序遍歷除第一棵樹之后剩余的子樹森林。
(2)中序遍歷,即先中序遍歷森林中第一棵樹的根結點的所有子樹,然后訪問第一棵樹的根結點,最后中序遍歷除第一棵樹之后剩余的子樹森林。
上圖所示的森林的先序遍歷的結點序列為:A B C D E F G H J I
此森林的中序遍歷的結點序列為:B C D A F E J H I G
由森林與二叉樹的轉換關系以及森林與二叉樹的遍歷定義可知,森林的先序遍歷和中序遍歷與所轉換得到的二叉樹的先序遍歷和中序遍歷的結果序列相同。
2.8 哈夫曼樹
2.8.1 哈夫曼樹的基本概念
首先給出定義哈夫曼樹所要用到的幾個基本概念。
(1)路徑(Path):從樹中的一個結點到另一個結點之間的分支構成這兩個結點間的路徑。
(2)路徑長度(Path Length):路徑上的分支數。
(3)樹的路徑長度(Path Length of Tree):從樹的根結點到每個結點的路徑長度之和。在結點數目相同的二叉樹中,完全二叉樹的路徑長度最短。
(4)結點的權(Weight of Node):在一些應用中,賦予樹中結點的一個有實際意義的數。
(5)結點的帶權路徑長度(Weight Path Length of Node):從該結點到樹的根結點的路徑長度與該結點的權的乘積。
(6)樹的帶權路徑長度(WPL):樹中所有葉子結點的帶權路徑長度之和記為
其中,W k 為第k個葉子結點的權值,L k 為第k個葉子結點的路徑長度。在下圖所示的二叉樹中,結點B的路徑長度為 1,結點C和D的路徑長度為 2,結點E、F和G的路徑長度為 3,結點H的路徑長度為 4,結點I的路徑長度為 5。該樹的路徑長度為:1+2*2+3*3+4+5=23。如果結點B、C、D、E、F、G、H、I的權分別是 1、2、3、4、5、6、7、8,則這些結點的帶權路徑長度分別是 1*1、2*2、2*3、3*4、3*5、3*6、4*7、5*8,該樹的帶權路徑長度為 3*5+3*6+5*8=73。
2.8.2 :哈夫曼樹(Huffman Tree)
哈夫曼樹(Huffman Tree),又叫最優二叉樹,指的是對於一組具有確定權值的葉子結點的具有最小帶權路徑長度的二叉樹。在下圖所示的的四棵二叉樹,都有 4 個葉子結點,其權值分別為 1、2、3、4,它們的帶權路徑長度分別為:
(a)WPL=1×2+2×2+3×2+4×2=20
(b)WPL=1×1+2×2+3×3+4×3=28
(c)WPL=1×3+2×3+3×2+4×1=19
(d)WPL=2×1+1×2+3×3+4×3=29
其中,圖 (c)所示的二叉樹的帶權路徑長度最小,這棵樹就是哈夫曼樹。可以驗證,哈夫曼樹的帶權路徑長度最小。
那么,如何構造一棵哈夫曼樹呢?哈夫曼最早給出了一個帶有一般規律的算法,俗稱哈夫曼算法。現敘述如下:
(1)根據給定的n個權值{w 1 ,w 2 ,…,w n },構造n棵只有根結點的二叉樹集合F={T 1 ,T 2 ,…,T n };
(2)從集合 F 中選取兩棵根結點的權最小的二叉樹作為左右子樹,構造一棵新的二叉樹,且置新的二叉樹的根結點的權值為其左、右子樹根結點權值之和。
(3)在集合 F 中刪除這兩棵樹,並把新得到的二叉樹加入到集合 F 中;
(4)重復上述步驟,直到集合中只有一棵二叉樹為止,這棵二叉樹就是哈夫曼樹。
由二叉樹的性質 4 和哈夫曼樹的特點可知,一棵有 n 個葉子結點構造的哈夫曼樹共有 2n-1 個結點。
哈夫曼樹的構造過程:
2.8.3 哈夫曼樹類的實現
由哈夫曼樹的構造算法可知,用一個數組存放原來的 n 個葉子結點和構造過程中臨時生成的結點,數組的大小為 2n-1。所以,哈夫曼樹類 HuffmanTree 中有兩個成員字段:data 數組用於存放結點,leafNum 表示哈夫曼樹葉子結點的數目。結點有四個域,一個域 weight,用於存放該結點的權值;一個域 lChild,用於存放該結點的左孩子結點在數組中的序號;一個域 rChild,用於存放該結點的右孩子結點在數組中的序號;一個域 parent,用於判定該結點是否已加入哈夫曼樹中。哈夫曼樹結點的結構為。
所以,結點類 Node 有 4 個成員字段,weight 表示該結點的權值,lChild 和rChild 分別表示左、右孩子結點在數組中的序號,parent 表示該結點是否已加入哈夫曼樹中,如果 parent 的值為-1,表示該結點未加入到哈夫曼樹中。當該結點已加入到哈夫曼樹中時,parent 的值為其雙親結點在數組中的序號。
結點類 Node 的定義如下:

1 public class HuffmanNode 2 { 3 private int weight;//結點權值 4 private int lChild;///左孩子結點 5 private int rChild; //右孩子結點 6 private int parent; //父結點 7 8 public int Weight { get; set; } 9 public int LChild { get; set; } 10 public int RChild { get; set; } 11 public int Parent { get; set; } 12 13 //構造器 14 public HuffmanNode() 15 { 16 weight = 0; 17 lChild = -1; 18 rChild = -1; 19 parent = -1; 20 } 21 22 //構造器 23 public HuffmanNode(int w, int lc, int rc, int p) 24 { 25 weight = w; 26 lChild = lc; 27 rChild = rc; 28 parent = p; 29 } 30 }
哈夫曼樹類 HuffmanTree 中只有一個成員方法 Create,它的功能是輸入 n 個葉子結點的權值,創建一棵哈夫曼樹。哈夫曼樹類 HuffmanTree 的實現如下。

1 public class HuffmanTree 2 { 3 private HuffmanNode[] data;//結點數組 4 private int leafNum;//葉子結點數目 5 6 //索引器 7 public HuffmanNode this[int index] 8 { 9 get 10 { 11 return data[index]; 12 } 13 set 14 { 15 data[index] = value; 16 } 17 } 18 19 //葉子結點數目屬性 20 public int LeafNum { get; set; } 21 22 public HuffmanTree(int n) 23 { 24 data = new HuffmanNode[2 * n - 1]; 25 for (int i = 0; i < 2 * n - 1; i++) 26 { 27 data[i] = new HuffmanNode(); 28 } 29 leafNum = n; 30 } 31 32 //創建哈夫曼樹 33 public HuffmanNode[] Create(List<int> list) 34 { 35 int max1; 36 int max2; 37 int tmp1; 38 int tmp2; 39 // 輸入 n 個葉子結點的權值 40 for (int i = 0; i < this.leafNum; ++i) 41 { 42 data[i].Weight = list[i]; 43 } 44 45 //處理 n 個葉子結點,建立哈夫曼樹 46 for (int i = 0; i < this.leafNum - 1; ++i) 47 { 48 max1 = max2 = Int32.MaxValue; 49 tmp1 = tmp2 = 0; 50 //在全部結點中找權值最小的兩個結點 51 for (int j = 0; j < this.leafNum + i; ++j) 52 { 53 if ((data[j].Weight < max1) && (data[j].Parent == -1)) 54 { 55 max2 = max1; 56 tmp2 = tmp1; 57 tmp1 = j; 58 max1 = data[j].Weight; 59 } 60 else if ((data[j].Weight < max2) && (data[j].Parent == -1)) 61 { 62 max2 = data[j].Weight; 63 tmp2 = j; 64 } 65 } 66 data[tmp1].Parent = this.leafNum + i; 67 data[this.leafNum + i].Weight = data[tmp1].Weight + data[tmp2].Weight; 68 data[this.leafNum + i].LChild = tmp1; 69 data[this.leafNum + i].RChild = tmp2; 70 } 71 return data; 72 73 } 74 }
2.8.4 哈夫曼編碼
在數據通信中,通常需要把要傳送的文字轉換為由二進制字符 0 和 1 組成的二進制串,這個過程被稱之為編碼(Encoding)。例如,假設要傳送的電文為DCBBADD,電文中只有 A、B、C、D 四種字符,若這四個字符采用表 下圖(a)所示的編碼方案,則電文的代碼為 11100101001111,代碼總長度為 14。若采用表 5-1(b) 所示的編碼方案,則電文的代碼為 0110101011100,代碼總長度為 13。
哈夫曼樹可用於構造總長度最短的編碼方案。具體構造方法如下:設需要編碼的字符集為{d 1 ,d 2 ,…,d n },各個字符在電文中出現的次數或頻率集合為{w 1 ,w 2 ,…,w n }。以d 1 ,d 2 ,…,d n 作為葉子結點,以w 1 ,w 2 ,…,w n 作為相應葉子結點的權值來構造一棵哈夫曼樹。規定哈夫曼樹中的左分支代表 0,右分支代表 1,則從根結點到葉子結點所經過的路徑分支組成的0和1的序列便為該結點對應字符的編碼就是哈夫曼編碼(Huffman Encoding)。
下圖 就是電文 DCBBADD 的哈夫曼樹,其編碼就是表 (b)。在建立不等長編碼中,必須使任何一個字符的編碼都不是另一個編碼的前綴,這樣才能保證譯碼的唯一性。例如,若字符 A 的編碼是 00,字符 B 的編碼是 001,那么字符 A 的編碼就成了字符 B 的編碼的后綴。這樣,對於代碼串001001,在譯碼時就無法判定是將前兩位碼 00 譯成字符 A 還是將前三位碼 001譯成 B。這樣的編碼被稱之為具有二義性的編碼,二義性編碼是不唯一的。而在哈夫曼樹中,每個字符結點都是葉子結點,它們不可能在根結點到其它字符結點的路徑上,所以一個字符的哈夫曼編碼不可能是另一個字符的哈夫曼編碼的前綴,從而保證了譯碼的非二義性。
2.9 C#中的樹
C#中的樹很多。比如,Windows Form 程序設計和 Web 程序設計中都有一種被稱為 TreeView 的控件。TreeView 控件是一個顯示樹形結構的控件,此樹形結構與 Windows 資源管理器中的樹形結構非常類似。不同的是,TreeView 可以由任意多個節點對象組成。每個節點對象都可以關聯文本和圖像。另外,Web 程序設計中的 TreeView 的節點還可以顯示為超鏈接並與某個 URL 相關聯。每個節點還可以包括任意多個子節點對象。包含節點及其子節點的層次結構構成了TreeView 控件所呈現的樹形結構。
DOM(Document Object Model)是 C#中樹形結構的另一個例子。文檔對象模型 DOM 不是 C#中獨有的,它是 W3C 提供的能夠讓程序和腳本動態訪問和更新文檔內容、結構和樣式的語言平台。DOM 被分為不同的部分(Core DOM,XML DOM和 HTML DOM)和不同的版本(DOM 1/2/3),Core DOM 定義了任意結構文檔的標准對象集合,XML DOM 定義了針對 XML 文檔的標准對象集合,而 HTML DOM 定義了針對 HTML 文檔的標准對象集合。C#提供了一個標准的接口來訪問並操作 HTML和 XML 對象集。后面將以 XML 對象集為例進行說明,對 HTML 對象集的操作類似。DOM 允許將 XML 文檔的結構加載到內存中,由此可以獲得在 XML 文檔中執行更新、插入和刪除操作的能力。DOM 是一個樹形結構,文件中的每一項都是樹中的一個結點。每個結點下面還有子結點。還可以用結點表示數據,並且數據和元素是不同的。在 C#中使用很多類來訪問 DOM,主要的類見下表所示。
本章小結
樹形結構是一種非常重要的非線性結構,樹形結構中的數據元素稱為結點,它們之間是一對多的關系,既有層次關系,又有分支關系。樹形結構有樹和二叉樹兩種。
樹是遞歸定義的,樹由一個根結點和若干棵互不相交的子樹構成,每棵子樹的結構與樹相同,通常樹指無序樹。樹的邏輯表示通常有四種方法,即直觀表示法、凹入表示法、廣義表表示法和嵌套表示法。樹的存儲方式有 3 種,即雙親表示法、孩子鏈表表示法和孩子兄弟表示法。
二叉樹的定義也是遞歸的,二叉樹由一個根結點和兩棵互不相交的子樹構成,每棵子樹的結構與二叉樹相同,通常二叉樹指有序樹。重要的二叉樹有滿二叉樹和完全二叉樹。二叉樹的性質主要有 5 條。二叉樹的的存儲結構主要有三種:順序存儲結構、二叉鏈表存儲結構和三叉鏈表存儲結構,本書給出了二叉鏈表存儲結構的 C#實現。二叉樹的遍歷方式通常有四種:先序遍歷(DLR)、中序遍歷(LDR)、后序遍歷(LRD)和層序遍歷(Level Order)。
森林是 m(m≥0)棵樹的集合。樹、森林與二叉樹的之間可以進行相互轉換。樹的遍歷方式有先序遍歷和后序遍歷兩種,森林的遍歷方式有先序遍歷和中序遍歷兩種。
哈夫曼樹是一組具有確定權值的葉子結點的具有最小帶權路徑長度的二叉樹。哈夫曼樹可用於解決最優化問題,在數據通信等領域應用很廣。