二叉树顺序存储之 前序,中序 ,后序遍历

二叉树遍历的概念:

  二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。

 

二叉树的深度优先遍历可细分为前序遍历、中序遍历、后序遍历,这三种遍历可以用递归实现

  前序遍历:根节点->左子树->右子树(根->左->右)

  中序遍历:左子树->根节点->右子树(左->根->右)

  后序遍历:左子树->右子树->根节点(左->右->根)

前序遍历

1)依据上文提到的遍历思路:根结点 ---> 左子树 ---> 右子树,非常easy写出递归版本号:

/* 以递归方式 前序遍历二叉树 */
void PreOrderTraverse(BiTree t,int level)
{
    if (t)
    {
     printf("data = %c level = %d\n ",t->data,level);
      PreOrderTraverse(t->lchild,level + 1);
      PreOrderTraverse(t->rchild,level + 1);
   } 

2)如今讨论非递归的版本号:

  依据前序遍历的顺序,优先訪问根结点。然后在訪问左子树和右子树。所以。对于随意结点node。第一部分即直接訪问之,之后在推断左子树是否为空,不为空时即反复上面的步骤,直到其为空。若为空。则须要訪问右子树。注意。在訪问过左孩子之后。须要反过来訪问其右孩子。所以,须要栈这样的数据结构的支持。对于随意一个结点node,详细过程例如以下:

a)訪问之,并把结点node入栈。当前结点置为左孩子;

b)推断结点node是否为空,若为空。则取出栈顶结点并出栈,将右孩子置为当前结点;否则反复a)步直到当前结点为空或者栈为空(能够发现栈中的结点就是为了訪问右孩子才存储的)

代码例如以下:

public void preOrderTraverse2(TreeNode root) {  
        Stack<TreeNode> stack = new Stack<>();  
        TreeNode pNode = root;  
        while (pNode != null || !stack.isEmpty()) {  
            if (pNode != null) {  
                System.out.print(pNode.val+"  ");  
                stack.push(pNode);  
                pNode = pNode.left;  
            } else { //pNode == null && !stack.isEmpty()  
                TreeNode node = stack.top();  
          stack.pop(); pNode
= node.right; } } }

 

 

 

 

 

 

 

中序遍历

1)依据上文提到的遍历思路:左子树 ---> 根结点 ---> 右子树,非常easy写出递归版本号:

 以递归方式 中序遍历二叉树 if (t == NULL)
    {
     PreOrderTraverse(t->lchild,level + 1);
      printf("data = %c level = %d\n ",level);
      PreOrderTraverse(t->rchild,level + 1);
   } 
}

2)非递归实现,有了上面前序的解释,中序也就比較简单了。同样的道理。仅仅只是訪问的顺序移到出栈时。代码例如以下:

 inOrderTraverse2(TreeNode root) {  
        Stack<TreeNode> stack = ) {  
                stack.push(pNode);  
                pNode = stack.top();
         stack.pop(); System.
out.print(node.val+); pNode = node.right; } } }

 

后序遍历

1)依据上文提到的遍历思路:左子树 ---> 右子树 ---> 根结点。非常easy写出递归版本号:

 以递归方式 后序遍历二叉树 )
    {
      PreOrderTraverse(t->lchild,level + 1);
      PreOrderTraverse(t->rchild,1)">);
      printf("data = %c level = %d\n ",t->data,level);
   } }

2)后序遍历的非递归实现是三种遍历方式中最难的一种。由于在后序遍历中,要保证左孩子和右孩子都已被訪问而且左孩子在右孩子前訪问才干訪问根结点,这就为流程的控制带来了难题。以下介绍两种思路。

      第一种思路:对于任一结点P,将其入栈,然后沿其左子树一直往下搜索。直到搜索到没有左孩子的结点,此时该结点出在栈顶,可是此时不能将其出栈并訪问,因此其右孩子还未被訪问。

所以接下来依照同样的规则对其右子树进行同样的处理,当訪问完其右孩子时。该结点又出在栈顶,此时能够将其出栈并訪问。这样就保证了正确的訪问顺序。能够看出,在这个过程中,每一个结点都两次出在栈顶,仅仅有在第二次出如今栈顶时,才干訪问它。因此须要多设置一个变量标识该结点是否是第一次出如今栈顶。

void postOrder2(BinTree *root)    非递归后序遍历
{
    stack<BTNode*> s;
    BinTree *p=root;
    BTNode *temp;
    while(p!=NULL||!s.empty())
    {
        while(p!=NULL)              沿左子树一直往下搜索。直至出现没有左子树的结点 
        {
            BTNode *btn=(BTNode *)malloc(sizeof(BTNode));
            btn->btnode=p;
            btn->isFirst=true;
            s.push(btn);
            p=p->lchild;
        }
        if(!s.empty())
        {
            temp=s.top();
            s.pop();
            if(temp->isFirst==true)     表示是第一次出如今栈顶 
             {
                temp->isFirst=false;
                s.push(temp);
                p=temp->btnode->rchild;    
            }
            else                        第二次出如今栈顶 
             {
                cout<<temp->btnode->data<<" ;
                p=NULL;
            }
        }
    }    
}

另外一种思路:要保证根结点在左孩子和右孩子訪问之后才干訪问,因此对于任一结点P。先将其入栈。假设P不存在左孩子和右孩子。则能够直接訪问它;或者P存在左孩子或者右孩子。可是其左孩子和右孩子都已被訪问过了。则相同能够直接訪问该结点。若非上述两种情况。则将P的右孩子和左孩子依次入栈。这样就保证了每次取栈顶元素的时候,左孩子在右孩子前面被訪问。左孩子和右孩子都在根结点前面被訪问。

void postOrder3(BinTree *root)     {
    stack<BinTree*> s;
    BinTree *cur;                      当前结点 
    BinTree *pre=NULL;                 前一次訪问的结点 
    s.push(root);
    while(!s.empty())
    {
        cur=s.top();
        if((cur->lchild==NULL&&cur->rchild==NULL)||
           (pre!=NULL&&(pre==cur->lchild||pre==cur->rchild)))
        {
            cout<<cur->data<<";  假设当前结点没有孩子结点或者孩子节点都已被訪问过 
              s.pop();
            pre=cur; 
        }
        else
        {
            if(cur->rchild!=NULL)
                s.push(cur->rchild);
            if(cur->lchild!=NULL)    
                s.push(cur->lchild);
        }
    }    
}

 

三种递归遍历方式对应的代码几乎相同,只是一条语句的位置发生了变化

printf(只看文字和代码来理解遍历的过程是比较困难的,建议读者亲自去遍历,为了理清遍历的过程下面上题

 

 

 

前序遍历

前序的遍历的特点,根节点->左子树->右子树,注意看前序的遍历的代码printf语句是放在两条递归语句之前的,所以先访问根节点G,打印G,然后访问左子树D,此时左子树D又作为根节点,打印D,再访问D的左子树A

A又作为根节点,打印A,A没有左子树或者右子树,函数调用结束返回到D节点(此时已经打印出来的有:GDA)D节点的左子树已经递归完成,现在递归访问右子树F,F作为根节点,打印F,F有左子树访问左子树E,E作为

根节点,打印E,(此时已经打印出来的有:GDAFE),E没有左子树和右子树,函数递归结束返回F节点,F的左子树已经递归完成了,但没有右子树,所以函数递归结束,返回D节点,D节点的左子树和右子树递归全部完成,

函数递归结束返回G节点,访问G节点的右子树M,M作为根节点,打印M,访问M的左子树H,H作为根节点,打印H,(此时已经打印出来的有:GDAFEMH)H没有左子树和右子树,函数递归结束,返回M节点,M节点的左子树已经

递归完成,访问右子树Z,Z作为根节点,打印Z,Z没有左子树和右子树,函数递归结束,返回M节点,M节点的左子树右子树递归全部完成,函数递归结束,返回G节点,G节点的左右子树递归全部完成,整个二叉树的遍历就结束了

(MGJ,终于打完了··)

前序遍历结果:GDAFEMHZ

 

总结一下前序遍历步骤

第一步:打印该节点(再三考虑还是把访问根节点这句话去掉了)

第二步:访问左子树,返回到第一步(注意:返回到第一步的意思是将根节点的左子树作为新的根节点,就好比图中D是G的左子树但是D也是A节点和F节点的根节点)

第三步:访问右子树,返回到第一步

第四步:结束递归,返回到上一个节点

 前序遍历的另一种表述:

(1)访问根节点

(2)前序遍历左子树

(3)前序遍历右子树

(在完成第2,3步的时候,也是要按照前序遍历二叉树的规则完成)

前序遍历结果:GDAFEMHZ

 

中序遍历

中序遍历步骤

第一步:访问该节点左子树

第二步:若该节点有左子树,则返回第一步,否则打印该节点

第三步:若该节点有右子树,则返回第一步,否则结束递归并返回上一节点

(按我自己理解的中序就是:先左到底,左到不能在左了就停下来并打印该节点,然后返回到该节点的上一节点,并打印该节点,然后再访问该节点的右子树,再左到不能再左了就停下来)

中序遍历的另一种表述:

(1)中序遍历左子树

(2)访问根节点

(3)中序遍历右子树

(在完成第1,3步的时候,要按照中序遍历的规则来完成)

所以该图的中序遍历为:ADEFGHMZ

 

后序遍历步骤

第一步:访问左子树

第二步:若该节点有左子树,返回第一步

第三步:若该节点有右子树,返回第一步,否则打印该节点并返回上一节点

 后序遍历的另一种表述:

(1)后序遍历左子树

(2)后序遍历右子树

(3)访问根节点

(在完成1,2步的时候,依然要按照后序遍历的规则来完成)

该图的后序遍历为:AEFDHZMG

 (读者如果在纸上遍历二叉树的时候,仍然容易将顺序搞错建议再回去看一下三种不同遍历对应的代码)

进入正题,已知两种遍历结果求另一种遍历结果(其实就是重构二叉树)

第一种:已知前序遍历、中序遍历求后序遍历

前序遍历:ABCDEF

中序遍历:CBDAEF

在进行分析前读者需要知道不同遍历结果的特点

1、前序遍历的第一元素是整个二叉树的根节点

2、中序遍历中根节点的左边的元素是左子树,根节点右边的元素是右子树

3、后序遍历的最后一个元素是整个二叉树的根节点

(如果读者不明白上述三个特点,建议再回去看一下三种不同遍历对应的代码,并在纸上写出一个简单的二叉树的三种不同的遍历结果,以加深对三种不同遍历的理解)

用上面这些特点来分析遍历结果,

 

 

 第一步先看前序遍历A肯定是根节点

第二步:确认了根节点,再来看中序遍历,中序遍历中根节点A的左边是CBD,右边是EF,所有可以确定二叉树既有左子树又有右子树

 

 

 第三步:先来分析左子树CBD,那么CBD谁来做A的左子树呢?这个时候不能直接用中序遍历的特点(左->根->右)得出左子树应该是这个样子

 

 

 因为有两种情况都满足中序遍历为CBD

 

 

直接根据中序遍历来直接得出左子树的结构,这个时候就要返回到前序遍历中去

观察前序遍历ABCDEF,左子树CBD在前序遍历中的顺序是BCD,意味着B是左子树的根节点(这么说可能不太好理解,换个说法就是B是A的左子树),得出这个结果是因为如果一个二叉树的根节点有左子树,那么

这个左子树一定在前序遍历中一定紧跟着根节点(这个是用前序遍历的特点(根->左->右)得出的),到这里就可以确认B是左子树的根节点

  

 

 第四步:再观察中序遍历CBDAEF,B元素左边是C右边是D,说明B节点既有左子树又有右子树,左右子树只有一个元素就可以直接确定了,不用再返回去观察前序遍历

 

 

第五步:到这里左子树的重建就已经完成了,现在重建右子树,因为重建右子树的过程和左子树的过程一模一样,步骤就不像上面写这么细了((┬_┬)),观察中序遍历右子树为EF,再观察前序遍历ABCDEF中右子树

的顺序为EF,所以E为A的右子树,再观察中序便利中E只有右边有F,所有F为E的右子树,最后得到的二叉树是这个样子的

 

 

所有求得的后序遍历为:CDBFEA

 

总结一下上述步骤: 先观察前序遍历找到根节点->观察中序遍历将根节点左边归为左子树元素,右边归为右子树元素(可能会出现只有左子树或者右子树的情况)->观察前序遍历中左\右子树几个元素的顺序,最靠前的为左\右子树的根节点->重复前面的步骤

第二种:已知中序遍历、后序遍历求前序遍历(题还是上面这道)

中序遍历:CBDAEF

后序遍历为:CDBFEA

仍然是根据不同遍历方式结果的特点来重构二叉树,过程很相似这里就不详细说了,后序遍历的最后一个元素A是根节点,在中序遍历中以根节点A作为分界将元素分为左子树(CBD)和右子树(EF),再观察后序遍历中左子树的顺序是CDB

,可以判断出B是左子树的根节点(因为后序遍历是:左->右->根),再观察中序遍历,B元素左边是C右边是D,说明B节点既有左子树又有右子树,左右子树只有一个元素就可以直接确定了,不用再返回去观察后序遍历,左子树重建完成,

现在来看右子树,右子树有两个元素EF,观察后序遍历E在F的后面,所以E是右子树的根节点,然后看中序遍历中E只有右边一个F元素了,即F是E的右子树,此时整个二叉树重构完成

 

总结一下上述步骤:先观察后序遍历找到根节点->观察中序遍历将根节点左边归为左子树元素,右边归为右子树元素(可能会出现只有左子树或者右子树的情况)->观察后序遍历中左\右子树几个元素的顺序,最靠后的为左\右子树的根节点->重复前面的步骤

 

注意:已知前序遍历、后序遍历无法求出中序遍历(因为由前序后序重构出来的二叉树不止一种)

举个栗子

 

 

左图这两种二叉树前序(BEFA)和后序(AFEB)一样,但对应的中序遍历结果不一样(左边的是AFEB右边的是BEFA),所以仅靠前序后序是

重构出唯一的二叉树

层序遍历

 

 

层序遍历。听名字也知道是按层遍历。我们知道一个节点有左右节点。而每一层一层的遍历都和左右节点有着很大的关系。也就是我们选用的数据结构不能一股脑的往一个方向钻,而左右应该均衡考虑。这样我们就选用队列来实现。

  • 对于队列,现进先出。从根节点的节点push到队列,那么队列中先出来的顺序是第二层的左右(假设有)。第二层每个执行的时候添加到队列,那么添加的所有节点都在第二层后面
  • 同理,假设开始pop遍历第n层的节点,每个节点会push左右两个节点进去。但是队列先进先出。它会放到队尾(下一层)。直到第n层的最后一个pop出来,第n+1层的还在队列中整齐排着。这就达到一个层序的效果。

实现的代码也很容易理解:

void LevelOrder(node t) {层序遍历
    Queue<node> q1 = new ArrayDeque<node>();
    if (t == )
        return;
    if (t != ) {
        q1.add(t);
    }
    while (!q1.isEmpty()) {
        node t1 = q1.poll();
        if (t1.left != )
            q1.add(t1.left);
        if (t1.right != )
            q1.add(t1.right);
        System.out.print(t1.value + );
    }
    System.out.println();
}

 

 

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


文章浏览阅读315次。之前用C语言编过链表,这几天突然想用C++编一下链表,搞了大半天才搞出来,所以就赶紧整理一下记录下来,省的万一时间长了找不到代码哈哈。一、链表代码1、Node.h文件代码#pragma onceclass Node{public: int ID; char alph; Node* next; Node(int ID,char alph); ~Node();private:..._if(current->id==id)
文章浏览阅读219次。碰到问题就要记录下来,防止遗忘吧。文章目录一、VS中的命令行参数二、内联函数和宏三、初始化和赋值一、VS中的命令行参数今天在运行代码的时候,碰都了下面的情况: // 解析命令行参数 if (pcl::console::find_argument (argc, argv, "-h") >= 0) { printUsage (argv[0]); return 0; }..._"if (pcl::console::find_argument(argc, argv, "-f") >= 0)怎么输入参数"
文章浏览阅读1.8k次,点赞11次,收藏37次。因为自己对决策树的机制非常的好奇,所以就研究了一下决策树的ID3算法,在这也做一篇笔记记录一下过程。文章目录一、什么是决策树?二、信息增益2.1信息熵2.1.1定义2.1.2演变2.2信息增益三、ID3算法实现四、小结一、什么是决策树?这个问题是我从一开始就有的疑问,什么是决策树?在看了一些资料之后,因为没有看到书上给出具体定义,所以按照我自己的理解决策树就是通过一个个“决策”而构建的一种树状结构,而且决策树的整个处理机制非常类似于我们人类在面临决策问题时的处理机制,这也可能就是其名字的由来。决_c++id3
文章浏览阅读492次。C++ 设计模式之策略模式
文章浏览阅读683次。我也算是个C++的小白,对于C++中的谓语我第一时间就想到了C#中的委托,但两者又不尽相同,所以想写一篇笔记记录一下。文章目录一、什么是谓语?二、使用谓语一、什么是谓语?谓语是一个可调用的表达式,其返回的结果可以作为条件的值,在C++中其实就是向算法传递函数。这和C#中的委托的概念其实是一样的,都是将函数作为参数进行传递。C++标准库中的谓语主要有两类:一元谓语和二元谓语,也就是有的算法只能..._谓语句 c++
文章浏览阅读225次。又看了一遍操作符的东西,感觉之前对操作符的理解还停留在很浅的认知上(仅仅会用哈哈),所以做一下笔记来加深一下印象。文章目录一、为什么会有操作符重载?二、操作符重载作用的对象一、为什么会有操作符重载?如果要回答这个问题,我们其实应该仔细想一下如果没有操作符重载会怎样呢?这其实很容易就联想到了C语言,因为他就没有操作符重载这一说。虽然C语言中没有类class这一概念,但是他有着和类及其相似的结构..._6-6 我的朋友 - c/c++ 操作符重载分数 15作者 海洋饼干叔叔单位 重庆大学实现frie
文章浏览阅读216次。因为之前碰到了很多关于C++上的问题,现在整理并记录一下。文章目录一、引用一、引用在C++中,引用就是给对象起了另一个名字,也就是“对象别名”。感觉和什么东西很相似,仔细一想不就是类型别名“typedef”吗哈哈。它其实是和原对象形成了一种绑定的一种关系,..._vc++6.0报错:returning address of local
文章浏览阅读565次。因为一直好奇预处理器的工作机制,所以就查了查书,做一下自己看完书之后的笔记。文章目录一、预处理器的作用一、预处理器的作用_c语言预处理器作用
文章浏览阅读1.8k次,点赞3次,收藏10次。最近特别查阅了一下关于C++文件的输入/输出的资料,整理了一下就写一下笔记。文章目录一、什么是流二、什么是缓冲区三、代码实现文件IO3.1 使用文件流对象读取数据3.2重定向一、什么是流当前的计算机具有很多种设备,但是无论是哪种设备都要与数据和信息进行打交道,所以这就牵扯到设备与数据之间的I/O操作。而每种设备又有着不同的特性和操作协议,由于过于复杂,所以我们一般是不会和这些通信细节打交道的..._c++ inpath
文章浏览阅读4.8k次,点赞6次,收藏29次。因为要使用到C++的动态链接库,所以就特意网上找了一下资料实现了一下。文章目录一、lib与dll文件二、创建dll文件三、dll隐式链接四、显式链接五、小结一、lib与dll文件之前我一直以为动态链接库就是指dll文件,这也是C#给我造成的一种印象,因为在C#中建立的类库文件都是dll文件,而且只要简单引用就可以了,但是C++却并不是这样的,这可能是因为C#隐藏了一些细节的缘故吧。在C++中共有两种库模式,一种是包含lib和dll两种文件,这种情况下其中的lib文件包含了函数所在的dll文件和dl_c++调用动态链接库
文章浏览阅读973次。因为遇到了一这个操作符的问题,所以记录一下出现的问题*~*。一、问题描述二、产生原因因为也是第一次出现这个问题,所以就到网上查了一些资料和书籍,现在倒也大概理解这个错误出现的原因了。有时候举个例子可能更容易理解为啥会出现这个错误,就拿一本书中的例子来说一下,如下所示:template<class T> class NamedObject { public: NamedObject(std::string& nameVal, const T objectVal) __copy_assign报错
C语言中的单向链表可以解决数组和结构体在使用时的内存连续性问题,同时还能动态地调整长度。本文介绍了单向链表的结构和基本操作,并给出了一个简单的示例代码。
文章浏览阅读2.3k次。区分'0'、"0"、0、''_0和
文章浏览阅读5.8k次,点赞4次,收藏8次。C语言函数指针详解,微剖本质_c语言指针函数
数组指针和指针数组是代码中常见的定义形式。虽然它们的语法类似,但含义完全不同。对于一维数组而言,数组名即为首元素的地址,不需要取址即可赋值给指针。而对于二维数组,数组名代表首行元素的地址,可以看作是一个指针数组,需要使用取址操作。
文章浏览阅读297次。总结刚入门的新同学C语言编程常见的低级错误
文章浏览阅读1.5w次,点赞12次,收藏70次。C语言 数组指针详解_c语言数组指针
文章浏览阅读306次。cJson常用接口总结并测试_用于测试的json接口
本篇文章和大家了解一下C语言中pthread_exit()函数实现终止线程的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。多线程编程中,线程...
本教程操作系统:windows10系统、c99版本、DELL G3电脑。 C语言是一门强大的编程语言,它允许我们对不同的数据类型进行各种运算和操作。但是有时候,我们需要将一个数据类型转换为另一个数据类型。这就是强制类型转