详解二叉树的遍历问题(前序后序中序层序遍历的递归算法及非递归算法及其详细图示)

Posted 薛定谔的猫ovo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解二叉树的遍历问题(前序后序中序层序遍历的递归算法及非递归算法及其详细图示)相关的知识,希望对你有一定的参考价值。


二叉树的遍历

 所谓二叉树的遍历,是指按照某条搜索路径访问树中的每个结点,使得每个结点均被访问一次,且仅被访问一次。
 由二叉树的递归定义可知,遍历一棵二叉树便要决定对根结点N、左子树L和右子树R的访问顺序。按照先遍历左子树再遍历右子树的原则,常见的遍历次序有先序(NLR)中序(LNR)后序(LRN)三种遍历算法,其中“序”指的是根结点在何时被访问

 二叉树的结点类型描述如下:

typedef struct BNode{
    char data;
    struct BNode *lchild;
    struct BNode *rchild;
}BNode;

 在给出具体算法之前,先给出算法中的实例,以供参考。
 需要遍历的二叉树:
在这里插入图片描述

 前序遍历:ABDECFG
 中序遍历:DBEAFCG
 后序遍历:DEBFGCA
 层序遍历:ABCDEFG



先序遍历(前序遍历)

先序遍历(PreOrder)的操作过程如下:
若二叉树为空,则什么也不做,否则:
1)访问根结点;
2)先序遍历左子树;
3)先序遍历右子树;


先序遍历的递归算法

void PreOrder(BNode *root){
    if(root != NULL){
        printf("%c ",root->data); //访问根结点
        PreOrder(root->lchild); //递归遍历左子树
        PreOrder(root->rchild); //递归遍历右子树
    }
}

先序遍历的非递归算法

 将二叉树的先序遍历的递归算法转换成非递归算法时,需要借助
声明一个遍历指针p,初始指向根结点。
如果p指向的结点不为空,则访问该结点,输出结点值,并将该根结点入栈,然后将p指向根结点的左子树。
如果p指向的结点为空,那么用top记录栈顶结点,将栈顶结点出栈,然后再往栈顶结点的右子树走。

 在先序遍历中,栈中的元素都是自己本身和自己的左孩子都访问过了,而右孩子还没有访问到的结点。

 实现代码:

void PreOrder2(BNode *root){
    stack<BNode *> treenode; //申请一个栈
    BNode *p = root; //p是遍历指针
    BNode *top; //指向当前的栈顶结点
    while(p != NULL || !treenode.empty()){//p不空或者栈不空时循环
        if(p != NULL){
            printf("%c ",p->data); //访问当前根结点
            treenode.push(p); //根结点进栈
            p = p->lchild; //指向当前结点的左子树
        }else{
            top = treenode.top(); //栈顶结点
            treenode.pop(); //出栈
            p = top->rchild; //再往右子树走
        }
    }
}

先序遍历的非递归算法的图示:

 首先给出需要遍历的二叉树:
在这里插入图片描述

1)创建一个空栈,初始化遍历指针p,其初始值指向根结点*root,并声明一个指针top,用于以后存储栈顶元素。

  注意:为了便于理解,在以下的图中栈中的内容为该结点的结点值,而在具体实现代码中,栈中存放的则是该结点地址。
在这里插入图片描述

2)p不空或栈不空时循环。

2.1)若p不为空,则访问当前根结点A,然后将该结点进栈,最后将p指向该根结点的左子树B。
在这里插入图片描述

2.2)若p不为空,则访问当前根结点B,然后将该结点进栈,最后将p指向该根结点的左子树D。
在这里插入图片描述

2.3)若p不为空,则访问当前根结点D,然后将该结点进栈,最后将p指向该根结点的左子树。
在这里插入图片描述

2.4)若p为空,则将top指向栈顶的结点D,然后将结点D出栈,再将遍历指针p指向该栈顶结点D的右子树。
在这里插入图片描述

2.5)若p为空,则将top指向栈顶结点B,然后将栈顶结点B出栈,再将遍历指针p指向栈顶结点B的右子树E。
在这里插入图片描述

2.6)若p不为空,则访问当前根结点E,然后将该结点进栈,最后将p指向该结点的左子树。
在这里插入图片描述

2.7)若p为空,则将top指向栈顶结点E,然后将栈顶结点E出栈,再将遍历指针p指向该栈顶结点E的右子树。
在这里插入图片描述

2.8)若p为空,则将top指向当前栈顶结点A,然后将栈顶结点A出栈,再将遍历指针p指向该栈顶结点A的右子树C。
在这里插入图片描述

2.9)若p不为空,则访问当前根结点C,并将该结点入栈,最后将p指向该根结点C的左子树F。
在这里插入图片描述

2.10)若p不为空,则访问当前根结点F,并将该结点入栈,最后将p指向该根结点F的左子树。
在这里插入图片描述

2.11)若p为空,则将top指向栈顶结点F,然后将栈顶结点F出栈,最后将遍历指针p指向该栈顶结点F的右子树。
在这里插入图片描述

2.12)若p为空,则将top指向栈顶结点C,然后将该栈顶结点C出栈,最后将遍历指针p指向该栈顶结点C的右子树G。
在这里插入图片描述

2.13)若p不为空,则访问当前根结点G,并将该结点入栈,最后将p指向该根结点G的左子树。
在这里插入图片描述

2.14)若p为空,则将top指向栈顶结点G,然后将栈顶结点G出栈,最后将遍历指针p指向该栈顶结点G的右子树。
在这里插入图片描述

3)当p为空且栈为空时,结束循环,遍历完成。
在这里插入图片描述

 所以当前树的中序遍历为ABDECFG。

 显然非递归算法的执行效率要高于递归算法。



中序遍历

中序遍历(InOrder)的操作过程如下:
若二叉树为空,则什么也不做,否则:
1)中序遍历左子树;
2)访问根结点;
3)中序遍历右子树。


中序遍历的递归算法

void InOrder(BNode *root){
    if(root != NULL){
        InOrder(root->lchild); //递归遍历左子树
        printf("%c ",root->data); //访问根结点
        InOrder(root->rchild); //递归遍历右子树
    }
}

中序遍历的非递归算法

 对于中序遍历的递归算法,将其转换为非递归算法,需要借助
先扫描(并不是访问)根结点的所有左结点并将它们一一进栈,然后出栈一个结点*p(显然结点*p没有左孩子结点或左孩子结点均已访问过),访问结点*p。
然后扫描该结点的右孩子结点,将其进栈,再扫描该右孩子结点的所有左结点并一一进栈,如此继续,直到栈空为止。

 中序遍历与前序遍历不同的是,前序遍历时,栈中保存的元素是右子树还没有被访问到的结点的地址,而中序遍历时,栈中保存的元素是结点自身和它的右子树中没有被访问到的结点地址。

 实现代码:

void InOrder2(BNode *root){
    stack<BNode *> treenode; //申请一个栈
    BNode *p = root; //p是遍历指针
    while(p != NULL || !treenode.empty()){ //栈不空或p不空时循环
        if(p != NULL){ //根指针进栈,遍历左子树
            treenode.push(p); //每次遇到非空二叉树先向左走
            p = p->lchild;
        }else{ //根指针退栈,访问根结点,遍历右子树
            p = treenode.top(); //p指向栈顶元素
            treenode.pop(); //退栈
            printf("%c ",p->data); //访问当前根结点
            p = p->rchild; //再向右子树走
        }
    }
}

 细心的读者可能注意到,中序遍历时没有重新声明一个指针top记录栈顶元素,而是直接用p来记录,这两种方法都是可以的(最终p都会指向栈顶结点的右孩子)。


中序遍历非递归算法的图示:

 首先给出需要遍历的二叉树:
在这里插入图片描述

1)创建一个空栈,并初始化遍历指针p,初始值指向根结点*root。

  注意:为了便于理解,在以下的图中栈中的内容为该结点的结点值,而在具体实现代码中,栈中存放的则是该结点地址。
在这里插入图片描述

2)p不空或栈不空时循环。

2.1)如果p不空,则根指针进栈,p指向该根结点A的左子树B。
在这里插入图片描述

2.2)如果p不空,则当前根指针进栈,p指向该根结点B的左子树D。
在这里插入图片描述

2.3)如果p不空,则当前根指针进栈,p指向该根结点D的左子树。
在这里插入图片描述

2.4)如果p为空,则当前栈顶根指针退栈,访问当前根结点,再向右子树走。
先将p指向要访问的根结点D,输出结点值后,再将p指向该根结点的右子树。
在这里插入图片描述

2.5)如果p为空,则栈顶根结点退栈,访问该根结点,然后再向右子树走。
先将p指向要访问的根结点B,输出结点值后,再将p指向该根结点的右子树E。
在这里插入图片描述

2.6)如果p不空,则将当前根指针入栈,p指向该根结点E的左子树。
在这里插入图片描述

2.7)如果p为空,则当前栈顶的根结点退栈,访问该根结点E,然后再往右子树走。
在这里插入图片描述

2.8)如果p为空,则当前栈顶的根结点退栈,访问该根结点A,然后再往右子树C走。
在这里插入图片描述

2.9)如果p不空,则将根指针入栈,然后将p指向该根结点C的左子树F。
在这里插入图片描述

2.10)如果p不空,则将该根指针入栈,将p指向该根结点F的左子树。
在这里插入图片描述

2.11)如果p为空,则将当前栈顶的根结点退栈,访问该根结点F,然后再往右子树走。
在这里插入图片描述

2.12)如果p为空,则当前栈顶的根结点C退栈,访问该根结点,然后再往右子树G走。
在这里插入图片描述

2.13)如果p不空,则将该根结点入栈,然后将p指向该结点G的左子树。
在这里插入图片描述

2.14)如果p为空,则将当前栈顶的根结点出栈,访问该根结点G,然后再往右子树走。
在这里插入图片描述

3)如果p为空且栈也为空,则退出循环,遍历完成。
在这里插入图片描述

 所以当前树的中序遍历为DBEAFCG。

 显然非递归算法的执行效率要高于递归算法。



后序遍历

后序遍历(PostOrder)的操作过程如下:
若二叉树为空,则什么也不做,否则:
1)后序遍历左子树;
2)后序遍历右子树;
3)访问根结点。


后序遍历的递归算法

void PostOrder(BNode *root){
    if(root != NULL){
        PostOrder(root->lchild); //递归遍历左子树
        PostOrder(root->rchild); //递归遍历右子树
        printf("%c ",root->data); //访问根结点
    }
}

后序遍历的非递归算法

 后序遍历的递归算法转换为非递归算法时,也是需要借助来保存元素。
从根结点开始,沿着左子树一直往下走,直到左子树为空。
然后再用top指针记录栈顶元素:
如果top指针指向的结点右子树为空或者last指向的结点,则说明top的右子树不存在或者已经遍历过了,那么弹出栈顶元素,访问top指向的结点,输出该结点值。
否则,说明是从左子树返回当前根结点的,那么就将遍历指针p指向top的右子树,去遍历右子树。

 栈中保存的是它的右子树和自身都没有被遍历到的结点,与中序遍历不同地方的是,先访问右子树,在访问完右子树后再输出根结点的值。这就需要多一个last指针来指向上一次访问到的结点,用来确认是从根结点的左子树还是右子树返回的

 实现代码:

void PostOrder2(BNode *root){
    stack<BNode *> treenode;
    BNode *p = root; //p是遍历指针
    BNode *top,*last = NULL; 
    while(p != NULL || !treenode.empty()){ //p不空或者栈不空时循环
        if(p != NULL){ //走到最左边的结点
            treenode.push(p); //当前根指针入栈
            p = p->lchild; //向左子树走
        }else{ //向右
            top = treenode.top(); //top栈顶指针
            if(top->rchild == NULL || top->rchild == last){ //右子树为空或者已经被遍历过
                treenode.pop(); //栈顶指针出栈
                printf("%c ",top->data); //输出该结点值
                last = top; //last指针指向当前top指针指向的结点
                p = NULL; //重置p指针
            }else{
                p = top->rchild; //p指向当前top指针指向的结点的右子树
            }
        }
    }
}

后序遍历非递归算法的图示:

 首先给出需要遍历的二叉树:
在这里插入图片描述

1)创建一个空栈,初始化遍历指针p,其初始值指向根结点*root,并声明一个指针top,用于以后存储栈顶元素。声明一个last指针,初始值为空。

  注意:为了便于理解,在以下的图中栈中的内容为该结点的结点值,而在具体实现代码中,栈中存放的则是该结点地址。

在这里插入图片描述

2)当p不空或栈不空时循环。

2.1)若p不为空,则当前根指针进栈,p指向当前根结点A的左子树B。
在这里插入图片描述

2.2)若p不为空,则将当前根指针入栈,并将p指向当前根结点B的左子树D。
在这里插入图片描述

2.3)若p不为空,则将当前根指针入栈,并将p指向当前根节点D的左子树。
在这里插入图片描述

2.4)若p为空,则top等于当前栈顶指针,指向结点D(注意此时结点D还未出栈)。
 如果top指向的结点D的右子树为空或者为last指针指向的结点,说明top的右子树不存在或者是已经遍历过,则栈顶指针出栈,访问该结点。last指针指向当前top指针指向的结点
在这里插入图片描述

2.5)若p为空,则top等于当前栈顶指针,指向结点B
 如果top指向的结点B的右子树既不空也不是last指针指向的结点,那么p就指向当前top指针指向的结点的右子树E
在这里插入图片描述

2.6)若p不为空,则将当前根指针入栈,p指向当前根结点E的左子树。
在这里插入图片描述

2.7)若p为空,则top等于当前栈顶指针,指向结点E
 如果top指向结点的的右子树为空或者为last指针指向的结点,说明top的右子树不存在或者是已经遍历过,则栈顶指针出栈,访问该结点。last指针指向当前top指针指向的结点
在这里插入图片描述

2.8)如果p为空,则top等于当前栈顶指针,指向结点B。
 如果top指向的结点的右孩子为空或者为last指向的结点,说明top的右子树不存在或者已经遍历过,则栈顶指针出栈,访问该结点B。last指针指向当前top指针指向的结点
在这里插入图片描述

2.9)如果p为空,则top等于栈顶指针,指向结点A。
 如果top指向的结点A的右子树不为空且不是last指针指向的结点,则p指向当前top指向的结点的右子树C
在这里插入图片描述

2.10)若p不为空,则将当前根指针入栈,将p指向当前根结点C的左子树F。
在这里插入图片描述

2.11)若p不为空,则将当前根指针入栈,将p指向当前根结点F的左子树。
在这里插入图片描述

2.12)若p为空,则top等于当前栈顶结点指针,指向结点F。
 如果top指向的结点F的右子树为空或者为last指向的结点,则栈顶指针出栈,访问该结点F,last指针指向当前top指针指向的结点
在这里插入图片描述

2.13)若p为空,则top等于当前栈顶指针,指向结点C。
 如果top指向的结点的右子树不为空且不为last指向的结点,则p指向当前top指向的结点的右子树G。
在这里插入图片描述

2.14)若p不为空,则将当前根指针入栈,将p指向当前根结点G的左子树。
在这里插入图片描述

2.15)若p为空,则top等于当前栈顶指针,指向结点G。
 如果top指向的结点的右子树为空或者为last指向的结点,则说明top指向的结点的右子树不存在或者已经遍历过,则栈顶指针出栈,访问该结点G,将last指针指向当前top指针指向的结点
在这里插入图片描述

2.16)若p为空,则top等于当前栈顶指针,指向结点C。
 如果top指向的结点的右子树为空或者等于last指向的结点,则栈顶指针出栈,访问该结点C,将last指针指向top指针指向的结点
在这里插入图片描述

2.17)若p为空,则top等于当前栈顶指针,指向结点A。
 如果top指向的结点的右子树为空或者为last指针指向的结点,则将栈顶指针出栈,访问该结点A,最后将last指针指向top指针指向的结点A。
在这里插入图片描述

3)当p为空且栈为空时,结束循环,遍历完成。
在这里插入图片描述

 所以当前树的后序遍历为DEBFGCA。

 显然非递归算法的执行效率要高于递归算法。


层序遍历

 如下图所示即为二叉树的层序遍历,即按照从上到下、从左到右的顺序依次访问每一层,直至每个结点被访问且仅被访问一次。

在这里插入图片描述

 要进行层序遍历,需要借助一个队列
 先将二叉树的根结点入队,然后出队,访问该结点;若它有左子树,则将左子树根结点入队;若它有右子树,则将右子树根结点入队。然后出队,对出队结点进行访问,如此反复,直到队列为空。

void LevelOrder(BNode *root){
    queue<BNode *> treenode; //队列存储结点
    if(root != NULL)
        treenode.push(root); //根结点入队
    while(!treenode.empty()){
        BNode *p = treenode.front(); 
        treenode.pop(); //根结点出队
        printf("%c ",p->data); //输出队首元素,即当前访问的结点值
        if(p->lchild != NULL){
            treenode.push(p->lchild);//如果有左子树,则将左子树的根结点入队
        }
        if(p->rchild != NULL){
            treenode.push(p->rchild);//如果有右子树,则将右子树的根结点入队
        }
    }
}

层序遍历的算法图示:

 首先给出需要遍历的二叉树:
在这里插入图片描述

1)声明一个空队列用来存储结点指针。当根结点不为空时,根结点入队。
 为了便于理解,以下队列中的元素均为该结点值.
在这里插入图片描述

2)当队列不空时循环。
2.1)声明一个指针p,p指向当前队首元素所指向的结点。将当前根结点出队,并访问该根结点A。
 如果该根结点有左子树,将左子树的根结点B入队。
 如果该根结点有右子树,将右子树的根结点C入队。
在这里插入图片描述

2.2)p指向队首元素所指向的结点B。将队首根结点出队,并访问该结点B。
 如果该根结点有左子树,将左子树的根结点D入队。
 如果该根结点有右子树,将右子树的根结点E入队。
在这里插入图片描述

2.3)p指向队首元素所指向的结点C。将队首根结点出队,并访问该结点C。
 如果该根结点有左子树,将左子树的根结点F入队。
 如果该根结点有右子树,将右子树的根结点G入队。
在这里插入图片描述

2.4)p指向队首元素所指向的结点D。将队首根结点出队,并访问该结点D。
 由于该根结点没有左子树,也没有右子树,故if中的语句不执行。
在这里插入图片描述

2.5)p指向队首元素所指向的结点E。将队首根结点出队,并访问该结点E。
 由于该根结点没有左子树,也没有右子树,故if中的语句不执行。
在这里插入图片描述

2.6)p指向队首元素所指向的结点F。将队首根结点出队,并访问该结点F。
 由于该根结点没有左子树,也没有右子树,故if中的语句不执行。
在这里插入图片描述

2.7)p指向队首元素所指向的结点G。将队首根结点出队,并访问该结点G。
 由于该根结点没有左子树,也没有右子树,故if中的语句不执行。
在这里插入图片描述

3)队列为空时,结束循环,遍历完成。
在这里插入图片描述

 故该二叉树的层序遍历为ABCDEFG。


二叉树的建立

二叉树的前序遍历存储在字符数组str中,根据这个字符序列建立一个二叉树,其中’#'表示空结点。

//建立二叉树
int i; //标记当前处理的字符位置
char str[100]; //读取的字符串
BNode *BuildTree(){
    if(str[i] == '#'){ //空结点
        i++; //准备处理下一个字符
        return NULL;
    }else{
        //新建一个结点
        BNode *p = (BNode 以上是关于详解二叉树的遍历问题(前序后序中序层序遍历的递归算法及非递归算法及其详细图示)的主要内容,如果未能解决你的问题,请参考以下文章

二叉树的前序中序后序层序遍历,递归和迭代两大类解题思路,每类细分不同解法完整版附PDF文档

Java实现二叉树的前序中序后序层序遍历(递归方法)

二叉树的四种遍历方式以及中序后序前序中序前序后序层序创建二叉树专为力扣刷题而打造

二叉树的四种遍历方式以及中序后序前序中序前序后序层序创建二叉树专为力扣刷题而打造

二叉树的四种遍历方式以及中序后序前序中序前序后序层序创建二叉树专为力扣刷题而打造

手撕二叉树的4种遍历:前序中序后序层序