二叉树的基本操作(先序序列创建二叉树/先序遍历递归算法/中序遍历递归算法/后序遍历递归算法/求二叉树深度的递归算法)

      二叉树的存储结构有顺序存储和链式存储两种存储方式,这里我们采用使用频率较高的链式存储方式(二叉链表)来存储二叉树.

      下面给出二叉树结点的定义.

struct BiTreeNode//二叉树结点定义
{
    BiTreeNode* LChild;//左孩子指针域
    int data;
    BiTreeNode* RChild;//右孩子指针域
};

      在上面的定义中,LChild指针指向该结点的左孩子结点,RChild指针指向该结点的右孩子结点,另外data变量存储该结点的数据信息. 值得一提的是,由于二叉树的某个结点对应的后继可能有两个(而不是至多只有一个),故二叉树这种结构是非线性结构,其操作难度要明显大于线性结构(比如顺序表、单链表). 

      有了二叉树结点的定义,我们再来分析该如何在主存中创建一棵二叉树.

      下面给出以先序遍历序列创建二叉树的具体过程.

void CreateBiTree(BiTreeNode* &T)//以先序序列创建二叉树
{
    char ch;
    cin>>ch;
    if(ch!='#')
    {
        T=(BiTreeNode*)malloc(sizeof(BiTreeNode));
        T->data=ch;
        CreateBiTree(T->LChild);
        CreateBiTree(T->RChild);
    }
    else
    {
        T=NULL;
    }
}

        在上述过程中,我们可以发现,当输入的字符为‘#’时, 创建空树,否则创建一个"实在"的结点. 之后再递归创建该结点的左孩子结点和右孩子结点. 从上述算法的书写过程中我们可以清晰地看出,这是一个递归算法:相信很多朋友和我之前一样,一提到递归算法就会感到"头大"——其实这是畏难情绪在作怪罢了. 当你初步掌握递归算法的设计套路后, 你会发现其实递归算法是比较容易写出的. 

        已掌握二叉树先序遍历递归算法的朋友可以对比这两种算法的结构,对比后应该能发现,其实它们在结构上是一回事. 

        为此,下面给出先序遍历二叉树的递归算法. 

void InOrderTraverse(BiTreeNode* &T)//先序遍历访问二叉树
{
    if(T!=NULL)
    {
        putchar(T->data);//在屏幕上显示字符, 若直接用cout<<T->data, 则输出的字符的ASCII码.
        cout<<" ";
        InOrderTraverse(T->LChild);
        InOrderTraverse(T->RChild);
    }
    else
    {
        ;
    }
}

        在此,我感觉很有必要和大家说明为什么可以用递归算法来实现二叉树的先序遍历操作:对于每一个结点来说,先序遍历操作的流程都是"遍历该结点---->先序遍历该结点的左子树---->先序遍历该结点的右子树". 我们可以发现,这种操作流程对于每一个结点来说都是相同的,所以我们可以设计出解决该问题的递归算法. 

        递归算法的设计除了要给出每次流程相同的操作外,还要给出一个明确的递归出口,否则程序将无穷无尽的执行下去.

        二叉树的先序遍历算法的明确出口应该为"当结点为空",也即"T!=NULL". 综合上述分析,便得出二叉树先序遍历的递归算法.

        我们趁热打铁,趁机再掌握二叉树中序遍历递归算法和二叉树后序遍历递归算法.

        下面给出二叉树中序遍历的递归算法.

void InOrderTraverse(BiTreeNode* &T)//中序遍历访问二叉树
{
    if(T!=NULL)
    {
        InOrderTraverse(T->LChild);
        putchar(T->data);//在屏幕上显示字符, 若直接用cout<<T->data, 则输出的字符的ASCII码.
        cout<<" ";
        InOrderTraverse(T->RChild);
    }
    else
    {
        ;
    }
}

        再给出二叉树后序遍历的递归算法. 

void InOrderTraverse(BiTreeNode* &T)//后序遍历访问二叉树
{
    if(T!=NULL)
    {
        InOrderTraverse(T->LChild);
        InOrderTraverse(T->RChild);
        putchar(T->data);//在屏幕上显示字符, 若直接用cout<<T->data, 则输出的字符的ASCII码.
        cout<<" ";
    }
    else
    {
        ;
    }
}

        对比看二叉树的三种递归遍历算法,它们在形式上非常类似,读者只需记住每种遍历算法对应的结构即可,个人认为不必深究递归算法的执行流程. 不过喜欢刨根问底的朋友一定很想弄清执行流程, 其实也不难, 在日后的博客中我会和大家分享这一部分的具体分析过程. 如果仅是为了准备考试,可以选择性忽略这一部分的具体分析过程;但如果是为了提升能力, 那么最好花时间将此过程吃透, 以为更高级别的算法设计打下坚实的基础.

        上面的算法是二叉树一切操作的基础,读者应牢牢掌握. 下面我们再来分享一个关于二叉树的算法——求二叉树深度的递归算法. 

        先给出算法的执行流程.

int Depth(BiTreeNode* &T)//计算二叉树的深度
{
    int m,n;
    if(T!=NULL)//如果T不是空树
    {
        m=Depth(T->LChild);//递归计算左子树的深度
        n=Depth(T->RChild);//递归计算右子树的深度
        if(m>n)//如果左子树深度>右子树深度
        {
            return m+1;//那么返回左子树深度+1
        }
        else//如果右子树深度>左子树深度
        {
            return n+1;//那么返回右子树深度+1
        }
    }
    else//如果T是空树
    {
        return 0;//那么返回0
    }
}

        由于在二叉树深度的计算过程中,对于每一个结点来说都要执行"求该结点的左子树深度---->求该结点的右子树深度---->比较该结点左子树和右子树的深度---->将‘深度较大者+1’作为该结点构成的二叉树的深度",故依旧可以使用递归思想解决该问题.

        我个人认为用递归思想解决某一复杂问题时,重要的不是用代码实现出来并运行验证,而是分析该问题的思维流程.

        使用递归算法求解问题对算法设计者而言是十分友好的,但凡事总有利弊. 算法设计者的确舒服了,但计算机可就没那么轻松了. 看似短短几行的递归算法简洁明了,可真正执行时,要耗费大量的主存空间,即空间效率极低. 从这里我们也可以得出一个结论,做什么事情时都不要剑走偏锋,一定要多方面考虑,最终选择最优者. 

        为了给读者进一步加深这种思想的渗透,在高等数学一元函数极限问题求解时的思维过程也是一个活生生的例子. 在求解一元函数极限问题时,我们可以使用"等价无穷小替换"、"洛必达法则"和"泰勒公式展开"三种工具进行求解:可如果你只使用"等价无穷小替换"作为求解工具,那么很多难题你都无法解决;如果你只使用"洛必达法则",那么你很有可能会掉入出题人的陷阱中;如果你只使用高大上的"泰勒公式",那么你可能会将简单的问题复杂化,从而浪费大量的解题时间. 

        所以,大家在学习时,一定要多方位思考,而不要仅停留在某种思想上. 如果你平时掌握了两种甚至三种求解思路,那么在考场上,你就可以随心所欲地按照自己的喜好来选择,而不是明知某种方法求解很困难却又只能这么做.

        好的,本篇博文就和大家分享至此,感谢大家的阅读!

原创文章 266 获赞 62 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_42048463/article/details/105873808