1,导论
什么是数据结构?
A data structure is an aggregation of data components that together constitute a meaningful whole。在计算机领域中,技术千变万化,但是基本的数据结构始终只有那几种。而抽象数据类型(ADT)就是用来描述数据结构具有的功能。比如,二叉树就有前序、中序遍历功能;栈,有先进后出功能。对于某一数据结构,放在不同的层次,它有不同的抽象,比如,对于存入整形数组的栈而言,站在使用Stack的角度,它具有提供先入后出功能;而栈可以用List实现,而List又可以用数组实现,而数组元素又可以是由各个Integer组成,而Integer对于编译器而言,又由bit组成。因此,谈论一种数据结构(类型),需要考虑站在的角度。
2,二叉树的构建算法--以二叉树的结点存储String类型的数据为例讨论
通常的想法是,创建一个根结点,再创建一颗左子树和右子树,然后把根结点的左孩子指向左子树,右孩子指向右子树。这种说法并没有考虑,结点如何构造?子树根据什么原则来构造?具体的实现代码怎么写?下面就是记录二叉树创建的具体实现算法。先给一个定义:
singature of a data structure: The signature of a data structure is an encoding of the structure and its contents in plain-text format that can be stored off-line(on disk) and used to recreate the data structure in memory when needed.
那二叉树的Signature是什么?由二叉树的性质知道:中序遍历和先序遍历顺序可以唯一确定一颗二叉树;中序遍历和后序遍历也可以唯一确定一颗二叉树。因此,若把二叉树遍历的顺序(如:中序遍历和先序遍历)保存在一个磁盘中的文本文件中,那么当需要构造一颗二叉树时(当然是在内存中),直接读该文本文件,然后调用构建算法即可。
①从Signature文本文件中读取二叉树的各个结点:
1 public BinaryTree<String> buildFromSignature() throws IOException{ 2 BufferedReader stdinbr = new BufferedReader(new InputStreamReader(System.in)); 3 System.out.println("File name? -->"); 4 System.out.flush(); 5 String file = stdinbr.readLine(); 6 Scanner sc = new Scanner(new File(file)); 7 int numNodes = sc.nextInt(); 8 String[] preorder = new String[numNodes]; 9 String[] inorder = new String[numNodes]; 10 11 for(int i = 0; i < numNodes; i++) 12 preorder[i] = sc.next(); 13 for(int i = 0; i < numNodes; i++) 14 inorder[i] = sc.next(); 15 return buildTree(preorder, 0, inorder, 0, numNodes - 1);//key point 16 }
②buildTree算法的实现,先看完整代码,然后再解释:
1 private BinaryTree<String> buildTree(String[]pre, int i, String[]in, int lo, int hi){ 2 if(i >= pre.length) 3 return null; 4 BinaryTree<String> myTree = new BinaryTree<>(); 5 myTree.makeRoot(pre[i]); 6 //search for pre[i] in in[lo..hi] 7 int j; 8 for(j = lo; j <= hi; j++) 9 if(pre[i].equals(in[j])) 10 break; 11 //build left and right subtrees recursively 12 BinaryTree<String>leftSub = buildTree(pre, i + 1, in, lo, j - 1); 13 BinaryTree<String>rightSub = buildTree(pre, i + j - lo + 1, in, j + 1, hi); 14 //attach them to the root and return 15 myTree.attachLeft(leftSub); 16 myTree.attachRight(rightSub); 17 return myTree; 18 }
@param String[] pre: 二叉树的先序遍历顺序
@param i:标记当前正在构造的根结点,从第5行的makeRoot(pre[i])看出,它用来标记当前正在构造哪个根结点,以及进一步构造该根结点的子树
@param String[] in:二叉树的中序遍历顺序
@param lo:查找下一个根结点时,lo 用来指示中序数组中的下限(low)
@param hi:查找下一个根结点时,hi 用来指示中序数组中的上限(high)
解释:根据先序遍历和中序遍历推断二叉树时,先在先序中找一个结点,然后再中序数组中去查找该结点,在中序数组中该结左边的元素都是它的左子树中的结点,在该结点右边的元素都是它的右子树中的结点。
return buildTree(preorder, 0, inorder, 0, numNodes - 1);
初始调用buildTree时,i = 0,因为先序遍历中的第一个结点即为根结点。lo = 0, hi = numNodes-1,表示此时在中序数组String[] in 中起始下标为0,终止下标为numNodes-1 的范围内查找当前的根结点pre[i]。(之所以称 当前“根”结点,这里的根结点不是指整棵树的根结点,而是在每一步的构建步骤中,考虑的当前结点,以该结点为根,构建它的左右子树。初始时,当前根结点即为整棵树的根结点(先序遍历的原因)。)
myTree.makeRoot(pre[i]);
构建当前的根结点
int j; for(j = lo; j <= hi; j++) if(pre[i].equals(in[j])) break;
在中序遍历的数组中查找当前的根结点。j 从 lo(low)下标开始,到hi(high)结束。第一次执行时,lo为0,hi为整棵树结点个数减1。j 用来标记找到的”当前"根结点在中序数组中的位置。那么,在String[] in 数组中, j 左边的某些结点则为当前根结点的左孩子,在 j 右边的某些结点则为当前根结点的右孩子了。
BinaryTree<String>leftSub = buildTree(pre, i + 1, in, lo, j - 1);
构造当前根结点的左子树。由于是先序遍历,因此,左子树的根结点位置为i+1,在中序数组中查找 左子树的根结点 的范围就是[lo, j-1]。这个比较好理解。
BinaryTree<String>rightSub = buildTree(pre, i + j - lo + 1, in, j + 1, hi);
构造当前根结点的右子树。j-lo+1表示,在中序数组中查找当前结点的右孩子时移动的元素个数(见上面for循环)。因此,i 加上 j-lo+1 就表示 当前根结点的右孩子的位置了。
再解释一下:在先序数组中,i 表示当前根结点的位置。经过在中序数组中的一番查找之后,找到了 j-lo+1 个元素,这些元素都是 i 的左子树中的结点(因为这些结点的在中序数组中的位置都是在 j 的左边),在先序数组中,i 向前移动 j-lo+1 个元素,得到 i+j-lo+1, i+j-lo+1先序数组中就是 i 的右孩子!!!
最后两个参数 j+1 和 hi 就很好理解了,就是:先序数组位置i 处结点的 右孩子 的子树结点的范围了。
3,树的层次结构的应用----文件系统
以Linux文件系统为例来说,它就是一种树形结构,当然,它比二叉树要复杂得多,但是基本原理和二叉树的操作相同。那么如何将文件系统的操作(如,删除文件、创建新文件……)转化为对树的操作呢?
如:Linux 命令: touch /home/xxx/newfile
经过某种解释器,将上面命令解析成对树的操作即可。首先找到树的根结点 "/",然后依次查找到结点 "xxx",最后在 "xxx"下创建一个新结点来代表 newfile
到这里,让我对文件系统的底层有了一些了解。也知道了为什么用B树来作为文件系统的数据结构。B树很大的一个特点就是矮!这样,对文件系统的一次操作访问磁盘的次数就少。
有一个小问题:在Linux文件系统的某个目录中,可以创建很多很多文件(不考虑权限),而在我们讨论的二叉树中每个结点的数据域是已经定义好的,参考JAVA实现二叉树
public class BinaryNode<T> implements BinaryNodeInterface<T>, java.io.Serializable{ private T data;//结点的数据域 private BinaryNode<T> left;//左孩子 private BinaryNode<T> right;//右孩子
而对应到文件系统中的某个目录,该目录下存放多少个文件是未知的。若统一将结点的child数据域设置为某个最大值,就会造成很大的浪费(如:大部分结点只有1,2个孩子,只有小部分结点有非常多的孩子,而结点的child数据域则必须是最多孩子那个结点的数据域个数)
针对上述情况:可以用另一种树的表示方法----左孩子右兄弟表示法。即,结点还是固定只有两个孩子域,左孩子域表示该结点的孩子。右孩子域表示该结点的兄弟。这样,就可以将一颗普通的树(每个结点有多个孩子域)转化成一颗二叉树的形式(每个结点只有两个孩子域),就可以解决上述所说的存储空间的浪费问题。