zoukankan      html  css  js  c++  java
  • [数据结构]二叉树创建与遍历

    实验报告:二叉树创建与遍历

    一、问题描述

    二叉树是一种实用范围很广的非线性结构,一棵非空二叉树有也只有一个根结点,每个结点最多有两个子树,我们称为左子树与右子树,当一个结点的左、右子树都是空的时,沃恩称此结点为叶子结点。

    二叉树有一些很好的性质,这里不再赘述。考虑如何存储一棵树,本实验选择使用链式存储结构——二叉链表;如果事先知道需要存储的二叉树是满二叉树或者完全二叉树,则可以考虑使用顺序存储,否则将浪费大量的存储空间。

    对于一棵既成的二叉树,有三种遍历方式——先序、中序与后序。可以证明,一棵形态固定的二叉树与先序遍历、后序遍历生成的序列是1-1对应的,基于这一点,我们可以从先序序列或后序序列出发得到唯一与之对应的二叉树。这是本实验创建二叉树的理论基础。

    值得注意的是,一个中序遍历序列与二叉树并不是1-1对应的关系,也就是说一个中序序列对应的二叉树的形态并不固定,即使像本实验中这样添加了虚空结点也不是1-1对应的,所以中序列并不能够生成一棵二叉树。

    本实验主要给出先序创建二叉树,以及中序遍历该树。

    二、数据结构——二叉链表

    二叉链表是二叉树的链式存储结构,其结构与二叉树的性质有天然的契合。

    我们知道,二叉树的结点需要保存的信息有:当前结点的信息、左子树、右子树。基于如此的结构,我们在一个结点处分为数据域和指针域:数据域保存该结点自身的信息;指针域分为左、右指针域,分别指向该结点的左、右子树;有必要时可以加上一个链域指向该结点的父亲结点(双亲结点),当然这样就成了三叉链表。三叉链表与二叉链表没有太大的差别,只是三叉链表在具体操作涉及到寻找结点的祖先时会有优势,比如寻找最近公共祖先(lca),这与本实验无关,不再赘述。

    值得注意的是,当结点的某个子树不存在时,指向该结点的指针域应该是NULL的。有个小技巧是,可以人为定义一个“空指针域”,应该指向NULL的时候都指向这个所谓的“空结点”,可以一定程度上避免指针访问越界的错误。

    三、算法的设计和实现

    这个算法的思想非常简单。本实验可以分为两部分——创建二叉树与遍历二叉树。

    1、创建二叉树(以先序序列为例)

    树的序列生成是一个递归的过程,所以以遍历序列创建树的时候也需要递归生成这棵树。

    以先序序列为例,如果当前标号非空,那么我们得到的就是当前结点的数据,保存该数据后,下一个数据——无论是虚空结点还是真实的数据——是左子树的数据,所以我们将递归到左子树中;如果当前标号是空的,即虚空结点,表示此处不存在结点,返回NULL即可;当前结点的左子树递归返回后进入右子树递归;返回该结点的地址,结束。

    可以看出这就是一个模拟生成先序序列的过程,着眼于一个结点来说,就是先访问当前子树的根节点,然后左子树,再右子树,最后返回。

    后序序列也可以类似地生成,具体细节将在下面提到。

    2、遍历二叉树(以中序遍历为例)

    与树的创建相似,二叉树的遍历同样是一个递归的过程。

    以中序序遍历为例,如果左子树非空,则递归进入左子树;输出当前结点的标号;如果右子树非空,则递归进入右子树;结束。如果不是需要输出序列,而是想得到这个序列,那么很自然地可以想到栈,将输出标号的操作改为将标号压栈即可,别的操作不改动。

    遍历二叉树的意义重大,比如中序遍历一棵排序二叉树,得到的就是保存的有序的序列。

    四、预期结果和实验中的问题

    1、预期结果:

    程序能够根据输入的先序序列(带有虚空结点),正确地生成对应的二叉树,并正确地输出中序遍历序列。下图是一个生成三序的例子。

    2、实验中的问题及一些说明:

    (1)如果没有虚空结点,能否通过三序得到唯一对应的二叉树?

    答案是肯定的。下面将简单地说明如何利用先序序列和中序序列得到对应的二叉树。

    A)分析:

    对于一个先序序列来说,第一个结点一定是根结点,它的右边是左子树的先序序列,然后是右子树的先序序列。这里的问题在于,无法找到左、右子树序列的分界点,换句话说,仅靠一个先序序列我们无法得到对应的二叉树。

    这时我们再对中序序列的构成分析,我们可以在中序序列中找到根结点(根结点的标号已经在先序序列中得到了),它的左边是左子树的中序序列,右边是右子树的中序序列。

    这时候我们发现,这里出现了一个子结构。因为一棵树(子树)的三序序列长度是相等的,所以我们可以得到左、右子树的先序序列,从而对于任一子树来说,我们得到了它的先序序列和中序序列,问题转换成了相似的子问题。

    B)实现:

    a)先序序列第一个结点找到根结点;

    b)在中序序列中找到该结点(此处可以顺序查找,找到结束即可;当数据量比较大时推荐使用二分法查找),得到左、右子树的结点数,从而分别得到左、右子树对应的先序序列和中序序列;

    c)分别递归进入左、右子树,建树。递归的边界是叶子结点,它的先序和中序都是长度为1的自己的标号。

    (2)包含虚空结点的后序序列如何生成对应的二叉树?

    有一个简单的办法,从后往前扫描后序序列,相当于是顺序为“根结点-右子树-左子树”生成的序列。这个与先序序列生成对应二叉树是一个镜面的过程,不再赘述。

    (3)中序序列能够生成唯一对应的二叉树吗?

    答案是否定的。中序序列与二叉树并不是1-1对应的,下图是一个例子:

    这两个都是合法的二叉树,两者的中序序列都是CBA,即使是加上了虚空结点,得到的中序序列也都是$C$B$A$$表示空格,也就是虚空结点),所以中序序列与二叉树不是1-1对应的,当然不能通过中序序列生成一棵唯一的二叉树。

    (2)能否不使用递归操作?

    这个问题可以归结为研究递归操作的本质。

    递归操作实际上是一个对栈的操作,以先序遍历二叉树为例:将当前结点压栈;如果左子树非空,将左子树压栈;如果右子树非空,将右子树压栈;如果左、右子树都是空的,即当前结点为叶子结点,弹栈;当结点的左右子树都完成了操作,弹栈。

    于是我们得到了一个有意思的结果:所有的递归算法是可以有非递归调用的实现方式的(这里的非递归调用实现方式指的是递归调用函数,其本质应该还是递归)。当然,也不排除一些递归解决的问题同样可以用递推解决,比如汉诺塔问题,这里不赘述。

    附:c++源代码:

     1 /*
     2 项目:创建二叉树,三序遍历
     3 作者:张译尹 
     4 */ 
     5 #include <iostream>
     6 #include <cstdio>
     7 
     8 using namespace std;
     9 
    10 template <class T> class BiTree
    11 {
    12 private:
    13     T data; //根结点的标志
    14     BiTree *lch, *rch; //左右儿子
    15 public:
    16     void InitBiTree() //初始化二叉链表
    17     {
    18         data = 0;
    19         lch = rch = NULL;
    20     }
    21     BiTree* CreatBiTree() //先序(带虚空结点)递归建立二叉链表 
    22     {
    23         BiTree <char> *rt = new BiTree;
    24         rt -> InitBiTree();
    25         char ch;
    26         scanf("%c", &ch);
    27         if(ch != ' ') //非空指针,递归建树
    28         {
    29             rt -> data = ch;
    30             rt -> lch = CreatBiTree();
    31             rt -> rch = CreatBiTree();
    32         } 
    33         else
    34         {
    35             delete rt;
    36             rt = NULL;
    37         }
    38         return rt;
    39     } 
    40     void PreOrderTraverse() //递归先序遍历 
    41     {
    42         printf("%c", data);
    43         if(lch)
    44             lch -> PreOrderTraverse();
    45         if(rch)
    46             rch -> PreOrderTraverse();
    47     }
    48     void InOrderTraverse() //递归中序遍历
    49     {
    50         if(lch)
    51             lch -> InOrderTraverse();
    52         printf("%c", data);
    53         if(rch)
    54             rch -> InOrderTraverse();
    55     } 
    56     void PostOrderTraverse() //递归后序遍历
    57     {
    58         if(lch)
    59             lch -> PostOrderTraverse();
    60         if(rch)
    61             rch -> PostOrderTraverse();
    62         printf("%c", data);
    63     } 
    64 };
    65 
    66 int main()
    67 {
    68     BiTree <char> *Tree;
    69     //Tree -> InitBiTree();
    70     
    71     printf("请输入二叉树的先序列,结点用字母表示,空域用空格表示。
    ");
    72     Tree = Tree -> CreatBiTree();
    73     
    74     printf("先序序列:
    ");
    75     Tree -> PreOrderTraverse();
    76     printf("
    ");
    77     
    78     printf("中序序列:
    ");
    79     Tree -> InOrderTraverse();
    80     printf("
    ");
    81     
    82     printf("后序序列:
    ");
    83     Tree -> PostOrderTraverse();
    84     printf("
    ");
    85     
    86     return 0;
    87 }
    View Code
  • 相关阅读:
    Altium Designer如何从已有的PCB图中导出封装库
    获得内核函数地址的四种方法
    poj2976 Dropping tests
    poj3045 Cow Acrobats
    CF916C Jamie and Interesting Graph
    poj3104 Drying
    poj2455 Secret Milking Machine
    poj2289 Jamie's Contact Groups
    网络流最小路径覆盖
    CF897C Nephren gives a riddle
  • 原文地址:https://www.cnblogs.com/CQBZOIer-zyy/p/5024037.html
Copyright © 2011-2022 走看看