【数据结构】二叉树的实现和三种遍历方式的两种实现(前序、中序、后续遍历 / 递归法、非递归法)

二叉树是一种重要的数据结构,初学的我们先要了解如何建立一个二叉树,以及如何去遍历这个二叉树。


①二叉树的概念和建立

“二叉树”极其类似于变相的链表,只是,其中的每个节点需要存放两个指针:“左支指针left”和“右支指针right”,作为父子结点之间连接的纽带。那么,我们只需要建立一个表征Tree结点的结构体。

struct TreeNode    //树节点,表示树中的每一个元素
{
    elemtype data;
    struct TreeNode* left;      //左支指针
    struct TreeNode* right;     //右支指针
};

附带一些定义(后续讲解不再重复解释):

typedef int elemtype;
typedef struct TreeNode TreeNode;
typedef struct TreeNode* PTree;     //指向树元素的指针更名为PTree

②二叉树的三种遍历(一):概念

<1>前序遍历:每个父子树单元的遍历顺序为“根、左、右”。
<2>中序遍历:每个父子树单元的遍历顺序为“左、根、右”。
<3>后续遍历:每个父子树单元的遍历顺序为“左、右、根”。

*不难看出两点:
(1)所谓的“序”,其实是根据每个单元根节点被遍历的顺序所规定的。【p.s:我所说的“父子树单元”即一个任意一个节点和它的左右支】
(2)不管是哪种遍历方式,单元的“左支”总比“右支”先遍历到。


③二叉树的三种遍历(二):递归法

我们理解递归法遍历,其实只需要看每一个小单元是如何遍历的,然后递归使用一个函数即可。

*比如前序遍历:
【数据结构】二叉树的实现和三种遍历方式的两种实现(前序、中序、后续遍历 / 递归法、非递归法)_第1张图片
它的代码就是:

void PreOrderSearch(PTree L)    //前序遍历树L<递归法>
{
    if (L!=NULL)
    {
        printf("%d ",GetNode(L));     //根          
        PreOrderSearch(L->left);      //左
        PreOrderSearch(L->right);     //右
    }
}

结合代码来理解:
如果索引指针不指向空,那么就先打印根节点的数据(即表示遍历到了),然后看这个节点的左子树,左子树也是重复这个索引过程,右子树同理,这种做同样事情的重复调用函数即为递归。

理解这一点,中序和后续遍历也可以写出来了:

void MidOrderSearch(PTree L)    //中序遍历树L<递归法>
{
    if (L!=NULL)
    {
        MidOrderSearch(L->left);     //左
        printf("%d ",GetNode(L));    //根
        MidOrderSearch(L->right);    //右
    }
}
void LastOrderSearch(PTree L)    //后序遍历树L<递归法>
{
    if (L!=NULL)
    {
        LastOrderSearch(L->left);    //左
        LastOrderSearch(L->right);   //右
        printf("%d ",GetNode(L));    //根
    }
}

可以看出,递归法实现的遍历程序短小精悍,三者大同小异,在程序不是很复杂的情况下,当然可以使用递归法去遍历。但是,递归这玩意,一旦程序大起来,狗都不用!狗都知道递归有的时候会发生“栈溢出”的错误。所以,接下来我要介绍非递归法实现的三种遍历!


④二叉树“前序遍历”非递归实现

实现非递归的遍历,我们需要借助另一个数据结构:“栈”,原因是我们需要实现链表的回退操作(即回到上一个结点的位置),用栈储存结点的地址,回退操作即“去栈顶元素”。但是链式栈未免有些复杂,在实际操作中,我们更多地是将一个数组抽象为栈,而栈顶的出入即通过下标的控制来实现。
请看:

PTree stack[1000];  //创建一个栈,这个栈是用数组实现的,存放结点
int stacknum=-1;    //初始化栈下标(为了匹配数组下标,所以从-1开始)

接下来才是重点!
遍历思路是:

往左走,一直走到不能走(null)的位置,同时记录走过的路径(入栈)。当无路可走时,如果栈不为空,则获取栈顶元素地址后依次出栈。

在每个左支结点出栈的同时,要看一下当前出栈结点有没有右支。
<1>如果没有右支,那么好,不需要多余的遍历工作了,继续往下出栈。
<2>如果有右支的话,就要跳转到遍历右支。这个时候,把右支看成一个新的根节点,对这个树进行遍历,重复上述的操作,直到无路可走。

在一个结点的右支处理完以后,就跳到下一个结点,继续重复操作。

【用最最通俗的话来讲,就是:往左走到尽头,一步一步回退,每一步都要看看右边有没有又生出了子树需要遍历,有的话就去遍历吧(遍历过程和刚刚讲的一样),没有就继续回退!】

先看程序,结合程序去理解思路和注意点最为有效!

void PreStackSearch(PTree L)   /*前序遍历树L<非递归法>*/
{
    PTree p=L;    //用指针p遍历树L
    PTree stack[1000];  //创建一个栈,这个栈是用数组实现的,存放结点
    int stacknum=-1;    //初始化栈下标(为了匹配数组下标,所以从-1开始)

    while (stacknum!=-1 || p!=NULL)   //遍历终止的标志是:遍历指针p指向NULL(到了最下方)
    {
        while (p!=NULL)   //看左
        {
            printf("%d ",p->data);    //看左时,看到即输出,同时入栈,为回退和看右做准备
            stack[++stacknum]=p;
            p=p->left;
        }
       /*循环结束,此时到了叶节点下方的NULL处,开始回退、出栈并看右*/
        if (stacknum!=-1)
        {
            p=stack[stacknum];    //p回退到栈顶的元素处
            stacknum--;     //出栈
            p=p->right;     //看右
         //(为什么不要加上if(p->right!=NULL)的条件?因为如果P->right指向了NULL,在下一次循环是会跳过第一个循环,直接回退)
        }
    }
}

思路与重点都在代码中注释,请读者结合理解!


⑤二叉树“中序遍历”非递归实现

理解了前序遍历,理解中序则会非常快速,代码也是顺理成章。
“中序遍历”与“前序遍历”非常相似,但有所不同。一开始,p也要遍历到最左边叶节点下方的NULL处,也要遍历时入栈,但是这个时候不要打印出来。回退也前序机制一样,此时就要先打印当前回退到的结点,然后再看右,再重复上述操作。

代码很快写出:

void MidStackSearch(PTree L)   /*中序遍历树L<非递归法>*/
{
    PTree p=L;
    PTree stack[1000];
    int stacknum=-1;

    while (stacknum!=-1 || p!=NULL)
    {
        while (p!=NULL)    //看左,入栈,不打印
        {
            stack[++stacknum]=p;
            p=p->left;
        }
        /*到达最左端叶节点下方的NULL,开始回退,出栈,输出和看右*/
        if (stacknum!=-1)
        {
            p=stack[stacknum];
            stacknum--;
            printf("%d ",p->data);
            p=p->right;
        }
    }
    
}

⑥二叉树“后序遍历”非递归实现

相较于“前序”和“中序”,“后序遍历”则较难实现,逻辑也较为复杂。

复杂在哪里呢?我们需要知道的是:后序遍历的每一个小的根节点,必须在其左右子树都被遍历过之后才能输出。那么问题来了,我们怎么样才能知道,这个单元根节点的左右支有没有没遍历过?

所以,我们要规定一个指针PVisited去标志已经遍历过的结点,这样才能判断该小的根节点现在是否需要输出了。

结合代码来理解吧~(讲也讲不明白其实,还是要靠自己理解的=。=)

void LastStackSearch(PTree L)  /*后续遍历树L<非递归法>*/
{
    PTree p=L;
    PTree stack[1000];
    int stacknum=-1;

    PTree PVisited=NULL;    //PVisited记录已经遍历过的结点,初始化为NULL

    while (p!=NULL)    //看左到最左NULL处
    {
        stack[++stacknum]=p;
        p=p->left;
    }
    while (stacknum!=-1)
    {
        p=stack[stacknum];    //退位
        stacknum--;

        if (p->left==NULL || p->right==PVisited)   //如果左右都被遍历过了(其实主要是右结点有没有遍历过的问题),那么就可以输出
        {
            printf("%d ",p->data);
            PVisited=p;         //标识已经遍历过这个节点
        }
        else    //如果右结点没有被遍历过,那么就去右支
        {
            stack[++stacknum]=p;
            p=p->right;      //去右支,然后继续重复上述(即看左支,入栈,等待下一次的外循环退位输出等操作)
            while (p!=NULL)
            {
                stack[++stacknum]=p;
                p=p->left;
            }
        }
    }
}

可以自己写一写树,测试一下输出!
笔者也为初学,其中难免会有纰漏与误解。但还是希望这篇文章能够帮到你!

你可能感兴趣的:(C语言数据结构与算法,二叉树,数据结构)