zoukankan      html  css  js  c++  java
  • 二叉树的创建算法

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

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

  • 相关阅读:
    Codeforces 512E
    UOJ #36 -【清华集训2014】玛里苟斯(线性基+暴搜)
    Codeforces 1188E
    洛谷 P7163
    C++ Boost库 操作日期与时间
    C/C++ 搜索缝隙并插入ShellCode
    线性代数学习之正交性,标准正交矩阵和投影
    洛谷 P5851 [USACO19DEC]Greedy Pie Eaters P(区间dp)
    洛谷 [NOIP2009 普及组] 道路游戏(dp)
    洛谷 P2890 [USACO07OPEN]Cheapest Palindrome G(区间dp)
  • 原文地址:https://www.cnblogs.com/hapjin/p/4812850.html
Copyright © 2011-2022 走看看