線性結構中的數據元素是一對一的關係,樹形結構是一對多的非線性結構,很是相似於天然界中的樹,數據元素之間既有分支關係,又有層次關係。樹形結構在現實世界中普遍存在,如家族的家譜、一個單位的行政機構組織等均可以用樹形結構來形象地表示。樹形結構在計算機領域中也有着很是普遍的應用,如 Windows 操做系統中對磁盤文件的管理、編譯程序中對源程序的語法結構的表示等都採用樹形結構。在數據庫系統中,樹形結構也是數據的重要組織形式之一。樹形結構有樹和二叉樹兩種,樹的操做實現比較複雜,但樹能夠轉換爲二叉樹進行處理,因此,咱們主要討論二叉樹。node
樹(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)。數據結構
由樹的定義可知,樹的定義是遞歸的,用樹來定義樹。所以,樹(以及二叉樹)的許多算法都使用了遞歸。ide
樹的形式定義爲:樹(Tree)簡記爲 T,是一個二元組,
T = (D, R)
其中:D 是結點的有限集合;
R 是結點之間關係的有限集合。優化
圖 1.1 動畫
從樹的定義和上圖的示例能夠看出,樹具備下面兩個特色: this
(1)樹的根結點沒有前驅結點,除根結點以外的全部結點有且只有一個前驅結點。編碼
(2)樹中的全部結點均可以有零個或多個後繼結點。
實際上,第(1)個特色表示的就是樹形結構的「一對多關係」中的「一」,第(2)特色表示的是「多」。
由此特色可知,下圖 所示的都不是樹。
一、結點(Node):表示樹中的數據元素,由數據項和數據元素之間的關係組成。在圖 1.1中,共有 10 個結點。
二、結點的度(Degree of Node):結點所擁有的子樹的個數,在圖 1.1 中,結點 A 的度爲 3。
三、樹的度(Degree of Tree):樹中各結點度的最大值。在圖 1.1 中,樹的度爲3。
四、葉子結點(Leaf Node):度爲 0 的結點,也叫終端結點。在圖 1.1 中,結點 E、F、G、H、I、J 都是葉子結點。
五、分支結點(Branch Node):度不爲 0 的結點,也叫非終端結點或內部結點。在圖 1.1 中,結點 A、B、C、D 是分支結點。
六、孩子(Child):結點子樹的根。在圖 1.1 中,結點 B、C、D 是結點 A 的孩子。
七、雙親(Parent):結點的上層結點叫該結點的雙親。在圖 1.1 中,結點 B、C、D 的雙親是結點 A。
八、祖先(Ancestor):從根到該結點所經分支上的全部結點。在圖 1.1 中,結點 E 的祖先是 A 和 B。
九、子孫(Descendant):以某結點爲根的子樹中的任一結點。在圖 1.1 中,除A 以外的全部結點都是 A 的子孫。
十、兄弟(Brother):同一雙親的孩子。在圖 1.1 中,結點 B、C、D 互爲兄弟。
十一、結點的層次(Level of Node):從根結點到樹中某結點所經路徑上的分支數稱爲該結點的層次。根結點的層次規定爲 1,其他結點的層次等於其雙親結點的層次加 1。
十二、堂兄弟(Sibling):同一層的雙親不一樣的結點。在圖 1.1 中,G 和 H 互爲堂兄弟。
1三、樹的深度(Depth of Tree):樹中結點的最大層次數。在圖 1.1 中,樹的深度爲 3。
1四、無序樹(Unordered Tree):樹中任意一個結點的各孩子結點之間的次序構成可有可無的樹。一般樹指無序樹。
1五、有序樹(Ordered Tree):樹中任意一個結點的各孩子結點有嚴格排列次序的樹。二叉樹是有序樹,由於二叉樹中每一個孩子結點都確切定義爲是該結點的左孩子結點仍是右孩子結點。
1六、森林(Forest):m(m≥0)棵樹的集合。天然界中的樹和森林的概念差異很大,但在數據結構中樹和森林的概念差異很小。從定義可知,一棵樹有根結點和m 個子樹構成,若把樹的根結點刪除,則樹變成了包含 m 棵樹的森林。固然,根據定義,一棵樹也能夠稱爲森林。
樹的邏輯表示方法不少,下面是常見的表示方法。
一、直觀表示法
它象平常生活中的樹木同樣。整個圖就象一棵倒立的樹,從根結點出發不斷擴展,根結點在最上層,葉子結點在最下面,如圖 1.1 所示。
二、凹入表示法
每一個結點對應一個矩形,全部結點的矩形都右對齊,根結點用最長的矩形表示,同一層的結點的矩形長度相同,層次越高,矩形長度越短,圖 1.1 中的樹的凹入表示法以下
三、廣義表表示法
用廣義表的形式表示根結點排在最前面,用一對圓括號把它的子樹結點括起來,子樹結點用逗號隔開。圖 1.1 的樹的廣義表表示以下:
(A(B(E,F,G),C(H),D(I,J)))
四、嵌套表示法
相似數學中所說的文氏圖表示法,以下圖 所示。
二叉樹(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)二叉樹
性質 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,則該結點無右孩子
二叉樹的存儲結構主要有三種:順序存儲結構、二叉鏈表存儲結構和三叉鏈表存儲結構。
對於一棵徹底二叉樹,由性質 5 可計算獲得任意結點 i 的雙親結點序號、左孩子結點序號和右孩子結點序號。因此,徹底二叉樹的結點可按從上到下和從左到右的順序存儲在一維數組中,其結點間的關係可由性質 5 計算獲得,這就是二叉樹的順序存儲結構。圖 (a)所示的二叉樹的順序存儲結構爲:
可是,對於一棵非徹底二叉樹,不能簡單地按照從上到下和從左到右的順序存放在一維數組中,由於數組下標之間的關係不能反映二叉樹中結點之間的邏輯關係。因此,應該對一棵非徹底二叉樹進行改造,增長空結點(並不存在的結點)使之成爲一棵徹底二叉樹,而後順序存儲在一維數組中。圖 (b)是圖 (a)的順序存儲示意圖
顯然,順序存儲對於需增長不少空結點才能改造爲一棵徹底二叉樹的二叉樹不適合,由於會形成空間的大量浪費。實際上,採用順序存儲結構,是對非線性的數據結構線性化,用線性結構來表示二叉樹的結點之間的邏輯關係,因此,須要增長空間。通常來講,有大約一半的空間被浪費。最差的狀況是右單支樹,以下圖 所示,一棵深度爲k的右單支樹,只有k個結點,卻須要分配 2 k -1 個存儲單元。
二叉樹的二叉鏈表存儲結構是指二叉樹的結點有三個域:一個數據域和兩個引用域,數據域存儲數據,兩個引用域分別存放其左、右孩子結點的地址。當左孩子或右孩子不存在時,相應域爲空,用符號 NULL 或∧表示。結點的存儲結構以下所示:
下圖是圖2.3.1(a)所示的二叉樹的二叉鏈表示意圖。圖 (a)是不帶頭結點的二叉鏈表,圖 (b)是帶頭結點的二叉鏈表。
由上圖所示的二叉樹有 4 個結點,每一個結點中有兩個引用,共有 8 個引用,其中 3 個引用被使用,5 個引用是空的。由性質 4 可知:由 n 個結點構成的二叉鏈表中,只有 n-1 個引用域被使用,還有 n+1 個引用域是空的。
使用二叉鏈表,能夠很是方便地訪問一個結點的子孫結點,但要訪問祖先結點很是困難。能夠考慮在每一個結點中再增長一個引用域存放其雙親結點的地址信息,這樣就能夠經過該引用域很是方便地訪問其祖先結點。這就是下面要介紹的三叉鏈表。
二叉樹的三叉鏈表存儲結構是指二叉樹的結點有四個域:一個數據域和三個引用域,數據域存儲數據,三個引用域分別存放其左、右孩子結點和雙親結點的地址。當左、右孩子或雙親結點不存在時,相應域爲空,用符號 NULL 或∧表示。結點的存儲結構以下所示:
下圖 (a)是不帶頭結點的三叉鏈表,圖 (b)是帶頭結點的三叉鏈表。
二叉樹的二叉鏈表的結點類有 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 }
實際上,遍歷是將二叉樹中的結點信息由非線性排列變爲某種意義上的線性排列。也就是說,遍歷操做使非線性結構線性化。
由二叉樹的定義可知,一棵二叉樹由根結點、左子樹和右子樹三部分組成,若規定 D、L、R 分別表明遍歷根結點、遍歷左子樹、遍歷右子樹,則二叉樹的遍歷方式有 6 種:DLR、DRL、LDR、LRD、RDL、RLD。因爲先遍歷左子樹和先遍歷右子樹在算法設計上沒有本質區別,因此,只討論三種方式:DLR(先序遍歷)、LDR(中序遍歷)和 LRD(後序遍歷)。
除了這三種遍歷方式外,還有一種方式:層序遍歷(Level Order)。層序遍歷是從根結點開始,按照從上到下、從左到右的順序依次訪問每一個結點一次僅一次。
先序遍歷的基本思想是:首先訪問根結點,而後先序遍歷其左子樹,最後先序遍歷其右子樹。先序遍歷的遞歸算法實現以下,注意:這裏的訪問根結點是把根結點的值輸出到控制檯上。固然,也能夠對根結點做其它處理。
徹底二叉樹
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
中序遍歷的基本思想是:首先中序遍歷根結點的左子樹,而後訪問根結點,最後中序遍歷其右子樹。中序遍歷的遞歸算法實現以下:
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
層序遍歷的基本思想是:因爲層序遍歷結點的順序是先遇到的結點先訪問,與隊列操做的順序相同。因此,在進行層序遍歷時,設置一個隊列,將根結點引用入隊,當隊列非空時,循環執行如下三步:
(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
實際場景使用上,用的最多的是二叉平衡樹,有種特殊的二叉平衡樹就是紅黑樹,Java集合中的TreeSet和TreeMap,C++STL中的set,map以及LInux虛擬內存的管理,都是經過紅黑樹去實現的,還有哈弗曼樹編碼方面的應用,以及B-Tree,B+-Tree在文件系統中的應用。固然二叉查找樹能夠用來查找和排序。
二叉樹在搜索上的優點
數組的搜索比較方便,能夠直接使用下標,但刪除或者插入就比較麻煩了,而鏈表與之相反,刪除和插入都比較簡單,可是查找很慢,這天然也與這兩種數據結構的存儲方式有關,數組是取一段相連的空間,而鏈表是每建立一個節點便取一個節點所需的空間,只是使用指針進行鏈接,空間上並非連續的。而二叉樹就既有鏈表的好處,又有數組的優勢。
二叉查找樹具備很高的靈活性,對其優化能夠生成平衡二叉樹,紅黑樹等高效的查找和插入數據結構,後文會介紹。
二叉查找樹(Binary Search Tree),也稱有序二叉樹(ordered binary tree),排序二叉樹(sorted binary tree),是指一棵空樹或者具備下列性質的二叉樹:
1. 若任意節點的左子樹不空,則左子樹上全部結點的值均小於它的根結點的值;
2. 若任意節點的右子樹不空,則右子樹上全部結點的值均大於它的根結點的值;
3. 任意節點的左、右子樹也分別爲二叉查找樹。
4. 沒有鍵值相等的節點(no duplicate nodes)。
以下圖,在二叉樹的基礎上,加上節點之間的大小關係,就是二叉查找樹
從圖中能夠看出,二叉查找樹中,最左和最右節點即爲最小值和最大值
查找操做和二分查找相似,將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 }
插入和查找相似,首先查找有沒有和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 }
隨機插入造成樹的動畫以下,能夠看到,插入的時候樹仍是可以保持近似平衡狀態:
二叉排序樹的刪除狀況以下圖所示。
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的時間複雜度。緣由在於插入和刪除元素的時候,樹沒有保持平衡。咱們追求的是在最壞的狀況下仍然有較好的時間複雜度,這就是平衡查找樹了。
樹的存儲結構包括順序存儲結構和鏈式存儲結構但不管採用哪一種存儲結構,都要求存儲結構不但能存儲結點自己的信息,還能存儲樹中各結點之間的邏輯關係。
從樹的定義可知,除根結點外,樹中的每一個結點都有惟一的一個雙親結點。根據這一特性,可用一組連續的存儲空間(一維數組)存儲樹中的各結點。樹中的結點除保存結點自己的信息以外,還要保存其雙親結點在數組中的位置(數組的下標),樹的這種表示法稱爲雙親表示法。
因爲樹的結點只保存兩個信息,因此樹的結點用結構體 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個兄弟在數組中的序號的域。
孩子鏈表表示法也是用一維數組來存儲樹中各結點的信息。但結點的結構與雙親表示法中結點的結構不一樣,孩子鏈表表示法中的結點除保存自己的信息外,不是保存其雙親結點在數組中的序號,而是保存一個鏈表的第一個結點的地址信息。這個鏈表是由該結點的全部孩子結點組成。每一個孩子結點保存有兩個信息,一個是每一個孩子結點在一維數組中的序號,另外一個是下一個孩子結點的地址信息。
孩子結點的結構以下所示:
孩子結點類 ChildNode 的定義以下
1 public class ChildNode 2 { 3 private int index; 4 private ChildNode nextChild; 5 … 6 }
樹結構和樹孩子鏈表表示法以下圖:
樹的孩子鏈表表示法對於實現查找孩子結點等操做很是方便,但對於實現查找雙親結點、兄弟結點等操做則比較困難。
這是一種經常使用的數據結構,又稱二叉樹表示法,或二叉鏈表表示法,即以二叉鏈表做爲樹的存儲結構。每一個結點除存儲自己的信息外,還有兩個引用域分別存儲該結點第一個孩子的地址信息和下一個兄弟的地址信息。樹類 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 }
樹的孩子兄弟表示法以下
樹的孩子兄弟表示法對於實現查找孩子、兄弟等操做很是方便,但對於實現查找雙親結點等操做則很是困難。若是在樹的結點中再增長一個域來存儲孩子的雙親結點的地址信息,則就能夠較方便地實現上述操做了。
從樹的孩子兄弟表示法可知,樹能夠用二叉鏈表進行存儲,因此,二叉鏈表能夠做爲樹和二叉樹之間的媒介。也就是說,藉助二叉鏈表,樹和二叉樹能夠相互進行轉換。從物理結構來看,它們的二叉鏈表是相同的,只是解釋不一樣而已。而且,若是設定必定的規則,就可用二叉樹來表示森林,森林和二叉樹也能夠相互進行轉換。
因爲二叉樹是有序的,爲了不混淆,對於無序樹,咱們約定樹中的每一個結點的孩子結點按從左到右的順序進行編號。如上圖所示的樹,根結點 A 有三個孩子 B、C、D,規定結點 B 是結點 A 的第一個孩子,結點 C 是結點 A 的第 2個孩子,結點 D 是結點 A 的第 3 個孩子。
將樹轉換成二叉樹的步驟是:
(1)加線。就是在全部兄弟結點之間加一條連線;
(2)抹線。就是對樹中的每一個結點,只保留他與第一個孩子結點之間的連
線,刪除它與其它孩子結點之間的連線;
(3)旋轉。就是以樹的根結點爲軸心,將整棵樹順時針旋轉必定角度,使
之結構井井有條。
下圖是樹轉換爲二叉樹的轉換過程示意圖。
森林是由若干棵樹組成,能夠將森林中的每棵樹的根結點看做是兄弟,因爲每棵樹均可以轉換爲二叉樹,因此森林也能夠轉換爲二叉樹。
將森林轉換爲二叉樹的步驟是:
(1)先把每棵樹轉換爲二叉樹;
(2)第一棵二叉樹不動,從第二棵二叉樹開始,依次把後一棵二叉樹的根結點做爲前一棵二叉樹的根結點的右孩子結點,用線鏈接起來。當全部的二叉樹鏈接起來後獲得的二叉樹就是由森林轉換獲得的二叉樹。
森林轉換爲二叉樹的轉換過程示意圖以下:
二叉樹轉換爲樹是樹轉換爲二叉樹的逆過程,其步驟是:
(1)若某結點的左孩子結點存在,將左孩子結點的右孩子結點、右孩子結
點的右孩子結點……都做爲該結點的孩子結點,將該結點與這些右孩子結點用線
鏈接起來;
(2)刪除原二叉樹中全部結點與其右孩子結點的連線;
(3)整理(1)和(2)兩步獲得的樹,使之結構井井有條。
二叉樹轉換爲樹的過程示意圖以下:
二叉樹轉換爲森林比較簡單,其步驟以下:
(1)先把每一個結點與右孩子結點的連線刪除,獲得分離的二叉樹;
(2)把分離後的每棵二叉樹轉換爲樹;
(3)整理第(2)步獲得的樹,使之規範,這樣獲得森林。
一、樹的遍歷
樹的遍歷一般有兩種方式:
(1)先序遍歷,即先訪問樹的根結點,而後依次先序遍歷樹中的每棵子樹。
(2)後序遍歷,即先依次後序遍歷樹中的每棵子樹,而後訪問根結點。
對上圖中的樹所示的樹進行先序遍歷所獲得的結點序列爲:A B E F G C H D I J
對此樹進行後序遍歷獲得的結點序列爲:E F G B H C I J D A
根據樹與二叉樹的轉換關係以及二叉樹的遍歷定義能夠推知,樹的先序遍歷與其轉換的相應的二叉樹的先序遍歷的結果序列相同;樹的後序遍歷與其轉換的二叉樹的中序遍歷的結果序列相同;樹的層序遍歷與其轉換的二叉樹的後序遍歷的結果序列相同。所以,樹的遍歷算法能夠採用相應的二叉樹的遍歷算法來實現。
二、森林的遍歷
森林的遍歷有兩種方式。
(1)先序遍歷,即先訪問森林中第一棵樹的根結點,而後先序遍歷第一棵樹中的每棵子樹,最後先序遍歷除第一棵樹以後剩餘的子樹森林。
(2)中序遍歷,即先中序遍歷森林中第一棵樹的根結點的全部子樹,而後訪問第一棵樹的根結點,最後中序遍歷除第一棵樹以後剩餘的子樹森林。
上圖所示的森林的先序遍歷的結點序列爲:A B C D E F G H J I
此森林的中序遍歷的結點序列爲:B C D A F E J H I G
由森林與二叉樹的轉換關係以及森林與二叉樹的遍歷定義可知,森林的先序遍歷和中序遍歷與所轉換獲得的二叉樹的先序遍歷和中序遍歷的結果序列相同。
首先給出定義哈夫曼樹所要用到的幾個基本概念。
(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的權分別是 一、二、三、四、五、六、七、8,則這些結點的帶權路徑長度分別是 1*一、2*二、2*三、3*四、3*五、3*六、4*七、5*8,該樹的帶權路徑長度爲 3*5+3*6+5*8=73。
哈夫曼樹(Huffman Tree),又叫最優二叉樹,指的是對於一組具備肯定權值的葉子結點的具備最小帶權路徑長度的二叉樹。在下圖所示的的四棵二叉樹,都有 4 個葉子結點,其權值分別爲 一、二、三、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 個結點。
哈夫曼樹的構造過程:
由哈夫曼樹的構造算法可知,用一個數組存放原來的 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 }
在數據通訊中,一般須要把要傳送的文字轉換爲由二進制字符 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。這樣的編碼被稱之爲具備二義性的編碼,二義性編碼是不惟一的。而在哈夫曼樹中,每一個字符結點都是葉子結點,它們不可能在根結點到其它字符結點的路徑上,因此一個字符的哈夫曼編碼不多是另外一個字符的哈夫曼編碼的前綴,從而保證了譯碼的非二義性。
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)棵樹的集合。樹、森林與二叉樹的之間能夠進行相互轉換。樹的遍歷方式有先序遍歷和後序遍歷兩種,森林的遍歷方式有先序遍歷和中序遍歷兩種。
哈夫曼樹是一組具備肯定權值的葉子結點的具備最小帶權路徑長度的二叉樹。哈夫曼樹可用於解決最優化問題,在數據通訊等領域應用很廣。