二叉树的创建算法

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数据域则必须是最多孩子那个结点的数据域个数)

针对上述情况:可以用另一种树的表示方法----左孩子右兄弟表示法。即,结点还是固定只有两个孩子域,左孩子域表示该结点的孩子。右孩子域表示该结点的兄弟。这样,就可以将一颗普通的树(每个结点有多个孩子域)转化成一颗二叉树的形式(每个结点只有两个孩子域),就可以解决上述所说的存储空间的浪费问题。

posted @ 2015-09-16 20:36  大熊猫同学  阅读(3694)  评论(0编辑  收藏  举报