樹的3種常用鏈表結構
1 雙親表示法(順序存儲結構)
優點:parent(tree, x)操作可以在常量時間內實現
缺點:求結點的孩子時需要遍歷整個結構
用一組連續的存儲空間來存儲樹的結點,同時在每個結點中附加一個指示器(整數域) ,用以指示雙親結點的位置(下標值) 。
圖所示是一棵樹及其雙親表示的存儲結構。這種存儲結構利用了任一結點的父結點唯一的性質。可以方便地直接找到任一結點的父結點,但求結點的子結點時需要掃描整個數組。
代碼實現:
1 // 1.雙親表示法 2 // 優點:parent(tree, x)操作可以在常量時間內實現 3 // 缺點:求結點的孩子時需要遍歷整個結構 4 function ParentTree() { 5 this.nodes = []; 6 } 7 ParentTree.prototype = { 8 constructor: ParentTree, 9 getDepth: function () { 10 var maxDepth = 0; 11 12 for (var i = 0; i < this.nodes.length; i++) { 13 var dep = 0; 14 for (var j = i; j >= 0; j = this.nodes[i].parent) dep++; 15 if (dep > maxDepth) maxDepth = dep; 16 } 17 18 return maxDepth; 19 } 20 }; 21 function ParentTreeNode(data, parent) { 22 // type: ParentTree 23 this.data = data || null; 24 // 雙親位置域 {Number} 25 this.parent = parent || 0; 26 } 27 var pt = new ParentTree(); 28 pt.nodes.push(new ParentTreeNode('R', -1)); 29 pt.nodes.push(new ParentTreeNode('A', 0)); 30 pt.nodes.push(new ParentTreeNode('B', 0)); 31 pt.nodes.push(new ParentTreeNode('C', 0)); 32 pt.nodes.push(new ParentTreeNode('D', 1)); 33 pt.nodes.push(new ParentTreeNode('E', 1)); 34 pt.nodes.push(new ParentTreeNode('F', 3)); 35 pt.nodes.push(new ParentTreeNode('G', 6)); 36 pt.nodes.push(new ParentTreeNode('H', 6)); 37 pt.nodes.push(new ParentTreeNode('I', 6));
2 孩子鏈表表示法
樹中每個結點有多個指針域,每個指針指向其一棵子樹的根結點。有兩種結點結構。
⑴ 定長結點結構
指針域的數目就是樹的度。
其特點是:鏈表結構簡單,但指針域的浪費明顯。結點結構如下圖(a)所示。在一棵有n個結點,度為k的樹中必有n(k-1)+1空指針域。
⑵ 不定長結點結構
樹中每個結點的指針域數量不同,是該結點的度,如圖(b) 所示。沒有多余的指針域,但操作不便。
⑶ 復合鏈表結構
對於樹中的每個結點,其孩子結點用帶頭結點的單鏈表表示,表結點和頭結點的結構如下圖所示。
n個結點的樹有n個(孩子)單鏈表(葉子結點的孩子鏈表為空),而n個頭結點又組成一個線性表且以順序存儲結構表示。
復合鏈表結構代碼:
1 // 孩子表示法 2 3 function ChildTree() { 4 this.nodes = []; 5 } 6 ChildTree.prototype = { 7 constructor: ChildTree, 8 getDepth: function () { 9 var self = this; 10 return function subDepth(rootIndex) { 11 if (!self.nodes[rootIndex]) return 1; 12 13 for (var sd = 1, p = self.nodes[rootIndex]; p; p = p.next) { 14 var d = subDepth(p.child); 15 if (d > sd) sd = d; 16 } 17 18 return sd + 1; 19 }(this.data[0]); 20 } 21 }; 22 /** 23 * 24 * @param {*} data 25 * @param {ChildTreeNode} firstChild 孩子鏈表頭指針 26 * @constructor 27 */ 28 function ChildTreeBox(data, firstChild) { 29 this.data = data; 30 this.firstChild = firstChild; 31 } 32 /** 33 * 孩子結點 34 * 35 * @param {Number} child 36 * @param {ChildTreeNode} next 37 * @constructor 38 */ 39 function ChildTreeNode(child, next) { 40 this.child = child; 41 this.next = next; 42 }
孩子表示法便於涉及孩子的操作的實現,但不適用於parent操作。
我們可以把雙親表示法和孩子表示法結合起來。
3 孩子兄弟表示法(二叉樹表示法)
以二叉鏈表作為樹的存儲結構。
兩個指針域:分別指向結點的第一個子結點和下一個兄弟結點。結點類型定義如下:
1 // 孩子兄弟表示法(二叉樹表示法) 2 // 可增設一個parent域實現parent操作 3 function ChildSiblingTree(data) { 4 this.data = data || null; 5 this.firstChild = null; 6 this.nextSibling = null; 7 } 8 ChildSiblingTree.prototype = { 9 // 輸出孩子兄弟鏈表表示的樹的各邊 10 print: function print() { 11 for (var child = this.firstChild; child; child = child.nextSibling) { 12 console.log('%c %c', this.data, child.data); 13 print.call(child); 14 } 15 }, 16 // 求孩子兄弟鏈表表示的樹的葉子數目 17 leafCount: function leafCount() { 18 if (!this.firstChild) return 1; 19 else { 20 var count = 0; 21 for (var child = this.firstChild; child; child = child.nextSibling) { 22 count += leafCount.call(child); 23 } 24 return count; 25 } 26 }, 27 // 求樹的度 28 getDegree: function getDegree() { 29 if (!this.firstChild) return 0; 30 else { 31 var degree = 0; 32 for (var p = this.firstChild; p; p = p.nextSibling) degree++; 33 34 for (p = this.firstChild; p; p = p.nextSibling) { 35 var d = getDegree.call(p); 36 if (d > degree) degree = d; 37 } 38 39 return degree; 40 } 41 }, 42 getDepth: function getDepth() { 43 if (this === global) return 0; 44 else { 45 for (var maxd = 0, p = this.firstChild; p; p = p.nextSibling) { 46 var d = getDepth.call(p); 47 if (d > maxd) maxd = d; 48 } 49 50 return maxd + 1; 51 } 52 } 53 };
森林與二叉樹的轉換
由於二叉樹和樹都可用二叉鏈表作為存儲結構,對比各自的結點結構可以看出,以二叉鏈表作為媒介可以導出樹和二叉樹之間的一個對應關系。
◆ 從物理結構來看,樹和二叉樹的二叉鏈表是相同的,只是對指針的邏輯解釋不同而已。
◆ 從樹的二叉鏈表表示的定義可知,任何一棵和樹對應的二叉樹,其右子樹一定為空。
1 樹轉換成二叉樹
對於一般的樹,可以方便地轉換成一棵唯一的二叉樹與之對應。將樹轉換成二叉樹在“孩子兄弟表示法”中已給出,其詳細步驟是:
⑴ 加虛線。在樹的每層按從“左至右”的順序在兄弟結點之間加虛線相連。
⑵ 去連線。除最左的第一個子結點外,父結點與所有其它子結點的連線都去掉。
⑶ 旋轉。將樹順時針旋轉450,原有的實線左斜。
⑷ 整型。將旋轉后樹中的所有虛線改為實線,並向右斜。
2 二叉樹轉換成樹
對於一棵轉換后的二叉樹,如何還原成原來的樹? 其步驟是:
⑴ 加虛線。若某結點i是其父結點的左子樹的根結點,則將該結點i的右子結點以及沿右子鏈不斷地搜索所有的右子結點,將所有這些右子結點與i結點的父結點之間加虛線相連,如下圖a所示。
⑵ 去連線。去掉二叉樹中所有父結點與其右子結點之間的連線,如下圖b所示。
⑶ 規整化。將圖中各結點按層次排列且將所有的虛線變成實線,如下圖c所示。
3 森林轉換成二叉樹
當一般的樹轉換成二叉樹后,二叉樹的右子樹必為空。若把森林中的第二棵樹(轉換成二叉樹后)的根結點作為第一棵樹(二叉樹)的根結點的兄弟結點,則可導出森林轉換成二叉樹的轉換算法如下:
設F={T1, T2,⋯,Tn}是森林,則按以下規則可轉換成一棵二叉樹B=(root,LB,RB)
① 若n=0,則B是空樹。
② 若n>0,則二叉樹B的根是森林T1的根root(T1),B的左子樹LB是B(T11,T12, ⋯,T1m) ,其中T11,T12, ⋯,T1m是T1的子樹(轉換后),而其右子樹RB是從森林F’={T2, T3,⋯,Tn}轉換而成的二叉樹。
轉換步驟:
① 將F={T1, T2,⋯,Tn} 中的每棵樹轉換成二叉樹。
② 按給出的森林中樹的次序,從最后一棵二叉樹開始,每棵二叉樹作為前一棵二叉樹的根結點的右子樹,依次類推,則第一棵樹的根結點就是轉換后生成的二叉樹的根結點。
4 二叉樹轉換成森林
若B=(root,LB,RB)是一棵二叉樹,則可以將其轉換成由若干棵樹構成的森林:F={T1, T2,⋯,Tn} 。
轉換算法:
① 若B是空樹,則F為空。
② 若B非空,則F中第一棵樹T1的根root(T1)就是二叉樹的根root, T1中根結點的子森林F1是由樹B的左子樹LB轉換而成的森林;F中除T1外其余樹組成的的森林F’={T2, T3,⋯,Tn} 是由B右子樹RB轉換得到的森林。
上述轉換規則是遞歸的,可以寫出其遞歸算法。以下給出具體的還原步驟。
① 去連線。將二叉樹B的根結點與其右子結點以及沿右子結點鏈方向的所有右子結點的連線全部去掉,得到若干棵孤立的二叉樹,每一棵就是原來森林F中的樹依次對應的二叉樹,如圖(b)所示。
② 二叉樹的還原。將各棵孤立的二叉樹按二叉樹還原為樹的方法還原成一般的樹,如圖(c)所示。
樹和森林的遍歷
1 樹的遍歷
由樹結構的定義可知,樹的遍歷有二種方法。
⑴ 先序遍歷:先訪問根結點,然后依次先序遍歷完每棵子樹。如圖的樹,先序遍歷的次序是: ABCDEFGIJHK
⑵ 后序遍歷:先依次后序遍歷完每棵子樹,然后訪問根結點。如圖的樹,后序遍歷的次序是: CDBFGIJHEKA
說明:
◆ 樹的先序遍歷實質上與將樹轉換成二叉樹后對二叉樹的先序遍歷相同。
◆ 樹的后序遍歷實質上與將樹轉換成二叉樹后對二叉樹的中序遍歷相同。
2 森林的遍歷
設F={T1, T2,⋯,Tn}是森林,對F的遍歷有二種方法。
⑴ 先序遍歷:按先序遍歷樹的方式依次遍歷F中的每棵樹。
⑵ 中序遍歷:按后序遍歷樹的方式依次遍歷F中的每棵樹。