二叉树非递归遍历算法分析

  以前没有学习过树的相关算法,只是了解一些皮毛,最近开始认真学习它。看视频或者网上查资料,可以知道怎么去遍历一棵树,但是算法为什么是这样的呢?少有讲到。如果有一天,我忘记了这个算法,我需要重新去看视频,看文档,这不是我想要的。我想要的是,知道这个算法是怎么设计出来的。下次我忘记的时候,我需要一支笔,一张纸,重新设计出这个算法,而不是去找资料看视频。我想要知道的是,为什么如此,而不是仅仅知道如此而已。

一、三种遍历顺序

  怎么记住三种遍历顺序:前序、中序、后序;前、中、后指的是什么。一棵树是怎么发展起来的:me,my->left_child,my->right_child,这是最基本的关系,me是中心,left和right跟me建立了一种联系,left和right再各自发展自己的child,于是一棵树就越来越庞大了。遍历过程,就是要把所有节点走一遍,而最基本的就是me/left/right这三者,所以根据访问它们的顺序,划分出了三种顺序,首先是先左后右,其次是me放在哪个位置,根据me的位置对遍历顺序进行命名,me在前即前序,在中即中序,在后即后序。

二、递归遍历的实现

  递归是符合我们的思维习惯的,一个复杂的问题总是可以分解为若干简单的问题。无论一棵树多么庞大,我从它的根节点出发,看到的是最多3个节点:我自己,我的左孩子,我的右孩子。我自己可以直接访问,然后是访问左孩子和右孩子。访问左孩子和右孩子的方法是一样的,访问它们自己节点,以及它们的左右孩子。这就是递归,这就是分治,把一个问题分解为几个子问题,分别考虑每个子问题,如果子问题跟我的处理逻辑一样,那么就成了递归。

// 这里以前序遍历为例,中序/后序只需要调整visit(node)所在位置即可
void traverse(node_t *node) {
    visit(node);
    traverse(node->left);
    traverse(node->right);
}

  但是递归存在一个问题:如果树很深,那么函数的栈可能溢出。通常情况下栈的大小是固定的,不会动态扩展(go语言除外,它的栈可以伸缩,所以用go写递归,不用考虑栈溢出的问题)。每调用一个函数,就消耗一定的函数栈空间,如果这个函数没有结束,那么它会一直占用栈空间;在这个函数内部调用一个函数,又开辟了新的栈空间,如果树太深,那么栈就可能耗尽,然后访问越界,发生踩内存,coredump等未知问题。

  所以,我们需要非递归,至少我们可以仿照函数栈的方式,自己实现栈,组织树的节点,完成遍历。当树太深的时候,我们可以动态扩展栈的空间,最终完成树的遍历。怎么实现栈的动态扩展,网上可以找到很多相关介绍。我的另外一篇博客实现了一种栈扩展的方法。在这里,只需要知道栈怎么用的:push/pop,后进先出,不考虑栈空间的扩展问题(假设它已经可以自动扩展了)。

三、怎么设计出非递归遍历算法

  函数栈实现了递归,我们需要用自己的栈替换掉函数的栈,组织树的节点。一开始,我尝试分析函数栈的过程,发现太复杂了。函数栈很通用,一个节点会反复的入栈出栈,栈中的元素带了状态信息,这些状态信息就是我们的栈变量,所以每次处理当前栈顶元素的时候,总是可以知道应该做什么。以上面的前序递归函数为例,执行函数traverse(node),首先需要访问node这个节点,于是开辟新的函数栈调用函数visit(node),当visit结束后,重新回到traverse(node),这时候pc指针已经指向了visit之后的代码,会调用traverse(node->left)。如果我们要用这种方式,那么需要压栈的时候,不仅把node入栈,还需要把node已经完成了多少工作也压栈,即

typedef enum {
    STATE_INIT,           // 刚刚开始
    STATE_VISITED_MYSELF, // 已经访问了自己
    STATE_LEFT_HANDLED,   // 已经处理了左孩子
    STATE_RIGHT_HANDLED,  // 已经处理了右孩子
} node_state_t;

typedef struct {
    node_t *node;
    node_state_t state;
} node_info_t;

也就是说,每次入栈的元素是node_info_t,每次处理之后修改state信息,直到node_info_t处理完之后从栈里面弹出销毁。我没有继续走下去,我认为这么做是可以行得通的,但是比较复杂,显然我们知道的非递归遍历算法,只是把node压栈了,没有这么多信息入栈,所以需要寻找更简单的方法。我放弃了分析函数栈推导非递归算法这条路。

  梳理一下思路:实现非递归遍历算法,需要用到栈,人工遍历是可以知道结果的。所以,我需要一张纸,一支笔,人工遍历,如果走到某一步发现走不下去了,看看可以怎么利用栈来帮我记录信息,然后重新开始,最终,应该可以找到一种方法,利用栈,实现非递归遍历。

  设计遍历算法的步骤是:

    1、明确遍历的顺序:前序、中序、还是后序;

    2、人工遍历;

    3、发现人工遍历进行不下去的时候,回头看看,怎么利用栈保存需要的信息;这一步可以总结出一些规则;

    4、根据步骤3总结的规则,从步骤2重新开始,如此反复几次,应该就可以归纳出遍历的方法,最终完成遍历,并设计出一种算法。

四、推导非递归算法——前序遍历

        (没有画图,感兴趣的朋友,可以自己找一张纸,一支笔,在纸上画画。下面的树只是用文字简单描述一下形状。)

                     <00>
      
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小瓶子36

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值