二叉树构建,先序,中序,后序遍历(以及非递归实现),广度优先遍历

二叉树是一类简单而又重要的树形结构,在数据的排序、查找和遍历方面有着广泛的应用。由于其清晰的结构,简单的逻辑,广泛的应用和大量的指针操作,在面试过程屡见不鲜,快被面试官玩坏了。相关的问题在百行代码内就可解决,特别适合手写代码,因此我们要充分做好准备,迎接面试时关于二叉树的相关问题,尤其是手写代码。

1. 二叉树简介

二叉树的相关概念,如,树高度,节点层数,节点度数,路径,叶节点,分支节点,根节点,父节点,左节点,右节点,兄弟节点,祖先节点,子孙节点,左子树,右子树等基本概念,不再赘述,可参见百度百科:二叉树

1.1 二叉树的五种基本形态

二叉树可以定义为节点的有限集合,或者为空集,或者为根节点及其左右子树的节点集合。因此二叉树具有五种基本形态。 (1)空二叉树

(2)只有一个根节点

(3)有根节点和非空左子树

(4)有根节点和非空右子树

(5)有根节点并且左右子树均为非空

1.2二叉树的分类

二叉树有两大类,一是普通二叉树,二是特殊二叉树。

普通二叉树不用多说,除了特殊二叉树,都是普通二叉树。除了富人,都是屌丝。那就重点说一下特殊二叉树。

(1)完全二叉树 若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。

一维数组可以作为完全二叉树的存储结构,堆排序使用的数据结构就是完全二叉树。下图所示就是完全二叉树

(2)满二叉树 国际标准定义是除了叶结点外每一个结点都有左右子结点的二叉树。如下图所示:

注意:这里的定义与国内某些教材的定义不同,国内的定义是:除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。很显然,按照这个定义,上面的图示二叉树就不是满二叉树。

(3)扩充二叉树 扩充二叉树是对已有二叉树的扩充,扩充后的二叉树的节点都变为度数为2的分支节点。也就是说,如果原节点的度数为2,则不变,度数为1,则增加一个分支,度数为0的叶子节点则增加两个分支。

如下如所示,一棵普通二叉树及其扩充二叉树:

(4)平衡二叉树 是一棵空树或它的任意节点的左右两个子树的高度差的绝对值不超过1。

2.二叉树的构建

二叉树的前序、中序和后序序列中的任何一个都不能唯一确定一棵二叉树,我所知道的二叉树的构建主要有两大种方法。

第一种是根据前序+中序或者后序+中序来唯一确定二叉树的结构,第二种是根据二叉树对应的扩充二叉树的先序或者后序序列来确定。

网上有很多blog和资料都没有将上面的方法列举出来,有个文档资料里说根据扩充二叉树的任意一个遍历序列就能唯一确定这棵二叉树。这个说法是错误的,这份文档见here

举一个反例即可证明根据扩充二叉树的中序遍历序列是不能唯一确定二叉树的结构,以文档中的描述为例:

上图中扩展二叉树的中序遍历序列为:#B#D#A#C#,那么也可以对应为下面的扩展二叉树:

2.1前序+中序序列构建二叉树(递归实现)

构建过程: (1)前序遍历序列中的第一个数字为根节点,构造根节点; (2)找到根节点在中序遍历序列中的位置,中序中根节点左右两边分别为左子树和有子树,前序序列根节点后面为左子树+右子树; (3)递归处理处理左右子树,返回根节点,完成构造。

构建过程示例: 以如下二叉树为例:

其前序遍历序列为:{1,2,4,7,3,5,6,8},中序遍历序列为:{4,7,2,1,5,3,8,6}。

由于在中序遍历中,有三个左子树节点的值,因此在前序遍历的序列中,根节点后面的3个数字就是3个左子树节点的值,再后面的所有数字都是右子树节点的值。这样子我们就在前序序列和中序序列中找到了左右子书对应的子序列,然后再递归处理即可。如下图所示:

前序序列:

中序序列:

理解上面的过程,即可根据前序序列和中序序列写出如下构建二叉树代码。二叉树的存储可以先用线性存储,如数组,或者链式存储,本文采用的就是链式存储。实现废话不多说,上代码:

//二叉树节点结构体
struct BinaryTreeNode{
    int m_key;
    BinaryTreeNode* m_pLeft;
    BinaryTreeNode* m_pRight;
};

/****************************************
func:根据前序序列和中序序列构建二叉树
para:preOrder:前序序列;midOrder:中序序列;len:节点数
****************************************/
BinaryTreeNode* construct(int* preOrder,int* midOrder,int len){
    if(preOrder==NULL||midOrder==NULL||len<=0)
        return NULL;

    //先根遍历(前序遍历)的第一个值就是根节点的键值
    int rootKey=preOrder[0];
    BinaryTreeNode* root=new BinaryTreeNode;
    root->m_key=rootKey;
    root->m_pLeft=root->m_pRight=NULL;
    if(len==1 && *preOrder==*midOrder)//只有一个节点
        return root;

    //在中根遍历(中序遍历)中找到根节点的值
    int* rootMidOrder=midOrder;
    int leftLen=0; //左子树节点数
    while(*rootMidOrder!=rootKey&&rootMidOrder<=(midOrder+len-1)){ 
        ++rootMidOrder;
        ++leftLen;
    }
    if(*rootMidOrder!=rootKey)//在中根序列未找到根节点,输入错误
        return NULL;

    if(leftLen>0){ //构建左子树
        root->m_pLeft=construct(preOrder+1,midOrder,leftLen);
    }
    if(len-leftLen-1>0){ //构建右子树
        root->m_pRight=construct(preOrder+leftLen+1,rootMidOrder+1,len-leftLen-1);
    }
    return root;
}

2.2后序+中序序列构建二叉树

这种方法和前面先序+中序序列构建的过程很像,唯一的区别就是对后序序列处理时,所找出的根节点和左右子树的位置不同而已。其具体实现,我就不给出了,网友可自行实现。

2.3扩充二叉树前序序列构建二叉树

这种方法可以参考:here

假设扩展二叉树的前序遍历序列由键盘输入,root为指向根结点的指针,二叉链表的建立过程是:首先输入根结点,若输入的是一个“#”字符,则表明该二叉树为空树,即root=NULL;否则输入的字符应该赋给root->data,之后依次递归建立它的左子树和右子树。建立二叉链表的递归算法如下:

//先序创建二叉树  
void CreatBTree(BTNode *&root)  
{     
    char nValue = 0;  
    cin >> nValue;  
    if ('#' == nValue)  
        return;  
    else{  
        root = new BTNode();  
        root->m_value = nValue;  
        CreatBTree(root->m_left);  
        CreatBTree(root->m_right);  
    }     
} 

下面是本人根据扩展二叉树的先根序列完成二叉树的构建。与上面不同的是先根序列由参数给出,而非标准输入读取。扩充节点使用-1表示。

/**************************************************
//func:根据扩展二叉树先根序列构建二叉树。
//para:extendedBTPreorder:扩展二叉树先根序列。
//ret: 二叉树根节点。
**************************************************/
BinaryTreeNode* createBTree(int*& extendedBTPreorder){     
    if (-1==extendedBTPreorder[0]){
        ++extendedBTPreorder;
        return NULL; 
    }

    BinaryTreeNode* root = new BinaryTreeNode;  
    root->m_key =extendedBTPreorder[0];  
    ++extendedBTPreorder;

    root->m_pLeft=createBTree(extendedBTPreorder);  
    root->m_pRight=createBTree(extendedBTPreorder);    
    return root;
}

注意:扩展二叉树先根序列使用引用的方式,保证每次每次生成新节点时指针后移一位。

2.4扩充二叉树后序序列构建二叉树

本人尚未研究,请知道的网友留言指教。

3.二叉树的遍历

二叉树的遍历分为两类,一类是深度优先周游,另一类是广度优先周游。

3.1深度优先周游

二叉树的深度优先周游有三种方式,就是我们常说的先根次序遍历(简称先序遍历)、中根次序遍历(中序遍历)和后跟次序遍历(后序遍历)。

因为树的定义本身就是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁。而对于树的遍历若采用非递归的方法,就要采用栈去模拟实现。在三种遍历中,前序和中序遍历的非递归算法都很容易实现,非递归后序遍历实现起来相对来说要难一点。下面一一讲解具体的递归和非递归实现。

3.1.1先根次序遍历

先根次序遍历按照“根结点-左孩子-右孩子”的顺序进行访问。

(1)递归实现

//先根递归遍历
void preOrderRecursion(BinaryTreeNode* root){
    if(root==NULL)
        return;
    cout<<" "<<root->m_key;   //visit
    preOrderRecursion(root->m_pLeft);
    preOrderRecursion(root->m_pRight);
}

(2)非递归实现 根据前序遍历访问的顺序,优先访问根结点,然后再分别访问左孩子和右孩子。即对于任一结点,其可看做是根结点,因此可以直接访问,访问完之后,若其左孩子不为空,按相同规则访问它的左子树;当访问完左子树时,再访问它的右子树。因此其处理过程如下:

给定二叉树的根节点R: (a)并将根节点R入栈;

(b)判断栈是否为空,若不为空,取栈顶元素cur访问并出栈。然后先将cur的右子节点入栈,再将cur的左子节点入栈;

(c)重复(b)直到栈为空,则遍历结束。

//先根非递归遍历,需要使用栈
void preOrderStack(BinaryTreeNode* root){
    if(root==NULL)
        return; 

    stack<BinaryTreeNode*> stack;
    stack.push(root);
    BinaryTreeNode* cur=NULL;
    while(!stack.empty()){
        cur=stack.top();
        cout<<" "<<cur->m_key; //visit
        stack.pop();
        if(cur->m_pRight!=NULL)
            stack.push(cur->m_pRight);
        if(cur->m_pLeft!=NULL)
            stack.push(cur->m_pLeft);
    }
}

3.1.2中根次序遍历

中序遍历按照“左孩子-根结点-右孩子”的顺序进行访问。 (1)递归实现

//中根递归遍历
void midOrderRecursion(BinaryTreeNode* root){
    if(root==NULL)
        return;
    midOrderRecursion(root->m_pLeft);
    cout<<" "<<root->m_key;   //visit
    midOrderRecursion(root->m_pRight);
}

(2)非递归实现 根据中序遍历的顺序,对于任一结点,先访问其左孩子,而左孩子结点又可以看做一根结点,然后继续访问其左孩子结点,直到遇到左孩子结点为空的结点才进行访问,然后按相同的规则访问其右子树。因此其处理过程如下:

对于给定的二叉树根节点R, (a)若其左孩子不为空,循环将R以及R左子树中的所有节点的左孩子入栈; (b)取栈顶元素cur,访问cur并将cur出栈。然后对cur的右子节点进行步骤(a)那样的处理; (c)重复(a)和(b)的操作,直到cur为空切栈为空。

//中根非递归遍历,需要使用栈
void midOrderStack(BinaryTreeNode* root){
    if(root==NULL)
        return; 

    stack<BinaryTreeNode*> stack;
    BinaryTreeNode* cur=root;
    while(!stack.empty()||cur!=NULL){  
        while(cur){  
            stack.push(cur);  
            cur=cur->m_pLeft;  
        }  
        cur=stack.top();  
        cout<<" "<<cur->m_key;   //visit
        stack.pop();  
        cur=cur->m_pRight;  
    }              
}

3.1.3后根次序遍历

后序遍历按照“左孩子-右孩子-根结点”的顺序进行访问。 (1)递归实现

//后根递归遍历
void postOrderRecursion(BinaryTreeNode* root){
    if(root==NULL)
        return;
    postOrderRecursion(root->m_pLeft);
    postOrderRecursion(root->m_pRight);
    cout<<" "<<root->m_key;   //visit
}

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

第一种思路:对于任一结点P,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,此时该结点出现在栈顶,但是此时不能将其出栈并访问,因此其右孩子还为被访问。所以接下来按照相同的规则对其右子树进行相同的处理,当访问完其右孩子时,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。

//非递归后序遍历,版本1
void postOrderStack1(BinaryTreeNode* root){
    if(root==NULL)
        return; 

    stack<pair<BinaryTreeNode*,bool> > s;
    pair<BinaryTreeNode*,bool> cur=make_pair(root,true);
    while(cur.first!=NULL||!s.empty())
    {
        while(cur.first!=NULL) {             //沿左子树一直往下搜索,直至出现没有左子树的结点 
            s.push(cur);
            cur=make_pair(cur.first->m_pLeft,true);
        }
        if(!s.empty()){
            if(s.top().second==true){     //表示是第一次出现在栈顶 
                s.top().second=false;
                cur=make_pair(s.top().first->m_pRight,true); //将当前节点的右节点入栈
            }
            else{                        //第二次出现在栈顶 
                cout<<s.top().first->m_key<<" ";
                s.pop();
            }
        }
    }    
}

第二种思路:要保证根结点在左孩子和右孩子访问之后才能访问,因此对于任一结点R, (a)先将R入栈。如果P不存在左孩子和右孩子,则可以直接访问它并出栈; (b)如果R存在左孩子或者右孩子,但是其左孩子和右孩子都已被访问过了,则同样可以直接访问该结点并出栈; (c)若非上述两种情况,则将R的右孩子和左孩子依次入栈,这样就保证了每次取栈顶元素的时候,左孩子在右孩子前面被访问,左孩子和右孩子都在父结点前面被访问

//非递归后根遍历,版本2
void postOrderStack2(BinaryTreeNode* root)    
{   
    if(root==NULL)
        return;

    stack<BinaryTreeNode*> s;
    BinaryTreeNode *cur;                      //当前结点 
    BinaryTreeNode *pre=NULL;                 //前一次访问的结点 
    s.push(root);
    while(!s.empty())
    {
        cur=s.top();
        if((cur->m_pLeft==NULL&&cur->m_pRight==NULL)||(pre!=NULL&&(pre==cur->m_pLeft||pre==cur->m_pRight))) //在判断当前节点时,左孩子和右孩子都在根结点前已经被访问
        {
            cout<<cur->m_key<<" ";  //如果当前结点没有孩子结点或者孩子节点都已被访问过 
            s.pop();
            pre=cur; 
        }
        else{
            if(cur->m_pRight!=NULL)
                s.push(cur->m_pRight);
            if(cur->m_pLeft!=NULL)    
                s.push(cur->m_pLeft);
        }
    }    
}

3.2广度优先周游

广度优先周游的方式是按层次从上到下,从左到右的逐层访问,不难想到,可以利用一个队列来实现。基本思想是: (1)首先把二叉树的根节点送入队列; (2)队首的节点出队列并访问之,然后把它的右子节点和左子节点分别入队列; (3)重复上面两步操作,直至队空。

//广度优先遍历二叉树,使用队列实现
void breadthFirstOrder(BinaryTreeNode* root){
    if(root==NULL) return;
    queue<BinaryTreeNode*> queue;
    queue.push(root);
    while(!queue.empty()){
        BinaryTreeNode* cur=queue.front();
        cout<<" "<<cur->m_key;//visit
        queue.pop();
        if(cur->m_pLeft!=NULL)
            queue.push(cur->m_pLeft);
        if(cur->m_pRight!=NULL)
            queue.push(cur->m_pRight);
    }
}    

4.验证结果

以上面建立的二叉树为例,进行各种遍历,验证代码如下:

#include  <iostream>
#include <stack>
#include <queue>  
using namespace std;

int main(){
    //先根序列
    int preOrder[8]={1,2,4,7,3,5,6,8};
    //中根序列
    int midOrder[8]={4,7,2,1,5,3,8,6};
    //建树
    BinaryTreeNode* root=construct(preOrder,midOrder,8);

    cout<<"---preOrder---"<<endl;
    cout<<"recursion version: ";
    preOrderRecursion(root);
    cout<<endl<<"stack version: ";
    preOrderStack(root);

    cout<<endl<<endl<<"---midOrder---"<<endl;
    cout<<"recursion version: ";
    midOrderRecursion(root);
    cout<<endl<<"stack version1: ";
    postOrderStack1(root);
    cout<<endl<<"stack version2: ";
    postOrderStack2(root);

    cout<<endl<<endl<<"---postOrder---"<<endl;
    cout<<"recursion version: ";
    postOrderRecursion(root);
    cout<<endl<<"stack version: ";
    postOrderStack1(root);


    cout<<endl<<endl<<"---Breadth First Order---"<<endl;
    breadthFirstOrder(root);
}

实验结果如下图: 实验结果markdown编辑器不知道出啥问题了,上传不了照片,报Object reference not set to an instance of an object.的错误。所以截图就换成如下文字描述:

---preOrder---
recursion version:  1 2 4 7 3 5 6 8
stack version:  1 2 4 7 3 5 6 8

---midOrder---
recursion version:  4 7 2 1 5 3 8 6
stack version:  4 7 2 1 5 3 8 6

---postOrder---
recursion version:  7 4 2 5 8 6 3 1
stack version1: 7 4 2 5 8 6 3 1 
stack version2: 7 4 2 5 8 6 3 1 

---Breadth First Order---
 1 2 3 4 5 6 7 8

5.小结

本文内容还不够完善,如以扩充二叉树来构建二叉树还未给出其实现原理与代码,再如先序+中序构建二叉树可以用非递归的方法来实现,等等,鄙人后续会继续完善的。


参考文献

[1]二叉树的非递归遍历. [2]百度百科.二叉树.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏青蛙要fly的专栏

Android技能树 — 树基础知识小结(一)

现在安卓面试,对于数据结构的问题也越来越多了,也经常看到别人发的面试题都是问什么红黑树,二叉树查找等,所以我们虽然不会马上就会各种难的面试题,但起码树的基础知识...

9930
来自专栏大闲人柴毛毛

剑指offer代码解析——面试题23从上往下打印二叉树

本题的详细分析过程均在代码注释中: import java.util.Queue; import java.util.concurrent.LinkedBloc...

29880
来自专栏恰童鞋骚年

剑指Offer面试题:5.重建二叉树

  在二叉树的前序遍历序列中,第一个数字总是树的根结点的值。但在中序遍历序列中,根结点的值在序列的中间,左子树的结点的值位于根结点的值的左边,而右子树的结点的值...

11340
来自专栏Android机动车

数据结构——二叉树的存储结构

二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标,要能体现结点之间的逻辑关系,如双亲与孩子的关系,左右兄弟的关系等。

13650
来自专栏高性能服务器开发

算法导论第十二章 二叉搜索树

二叉搜索树(又名二叉查找树、二叉排序树)是一种可提供良好搜寻效率的树形结构,支持动态集合操作,所谓动态集合操作,就是Search、Maximum、Minimum...

14620
来自专栏深度学习与计算机视觉

数据结构-二叉树遍历总结

二叉树结构 二叉树是一种特殊的树,每个父结点最多只能用有两个子结点。 ? 在树中,按照结点的“继承”关系可以分为父结点和子结点; 按照结点的位置...

20750
来自专栏深度学习与计算机视觉

算法-重建二叉树

题目: 输入某二叉树的前序遍历与中序遍历结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果均无重复数字,前序遍历序列为{},中序遍历序列为{},则重...

197100
来自专栏猿人谷

二叉树的非递归遍历(递归和非递归)

二 叉树是一种非常重要的数据结构,很多其它数据结构都是基于二叉树的基础演变而来的。对于二叉树,有前序、中序以及后序三种遍历方法。因为树的定义本身就是 递归定义,...

201100
来自专栏拭心的安卓进阶之路

Java 集合深入理解(6):AbstractList

今天心情比天蓝,来学学 AbstractList 吧! ? 什么是 AbstractList ? AbstractList 继承自 AbstractCollec...

244100
来自专栏电光石火

java获取当前时间和前一天日期

String basePath = request.getScheme()+"://"+request.getServerName()+":"+request....

29680

扫码关注云+社区

领取腾讯云代金券