树的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中的每棵树。