二叉树非递归版的后序遍历算法

你会学到什么?

树的递归遍历算法很容易理解,代码也很精简,但是如果想要从本质上理解二叉树常用的三种遍历方法,还得要思考树的非递归遍历算法。

读完后的收获:

  • 将学到二叉树的后序遍历的非递归版本
  • 明白栈这种数据结构该怎么使用

讨论的问题是什么?

主要讨论二叉树的非递归版后序遍历该如何实现,包括借助什么样的数据结构,迭代的构思过程等。

相关的概念和理论

遍历

Traversal 指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题。

二叉树组成

二叉树由根结点及左、右子树这三个基本部分组成。

后序遍历

Postorder Traversal 访问根结点的操作发生在遍历其左、右子树之后。

思考过程

后序遍历是从左子树,再到右子树,最后到根节点的遍历次序。如果借助栈来实现,我们自然想到先推入根节点,再推入右子树,最后推入左子树,然后出栈的顺序便是与推入顺序相反的。

那么这个问题,岂不是非常简单,是的,如果这棵树的左子树仅包括一个节点,右子树也仅仅包括一个节点,就像下图这样,


二叉树非递归版的后序遍历算法_第1张图片

很显然,我们这样考虑树结构,思维是很局限的,我们要考虑的是节点2下还重复以上结构,节点3当然也是如此,每个节点都有可能重复这种结构,或仅有左右孩子之一,或都没有也就是叶子。

那么,如此递归结构,该如何思考写出非递归算法呢?


二叉树非递归版的后序遍历算法_第2张图片

  • A 我们还是坚持从定义出发,不管一颗多么复杂的二叉树,在后序遍历中,一定先遍历的节点都位于节点2这一枝上,如图所示,所以我们会沿着2这一枝往下遍历,一直找,一直入栈,直到找到一个没有左孩子的节点,假设为节点2;
  • B 接下来要看下节点2有没有右孩子,如果没有,则表明节点2为叶子节点,直接访问这个节点2,然后出栈节点2;
  • C 如果节点2存在右孩子,如上图所示,那么我们该怎么办呢?我们把这个右孩子看成以其为根的子树,然后重复step A。如图所示,我们假设2下的右孩子是个叶子节点。分析这个过程,我们当然要入栈这个节点,然后观察到它没有左孩子,也没有右孩子,符合上文提到的step B;
  • D 这个节点出栈后,此时的栈顶元素为节点2,也就是我们下一个想要访问的元素,这是这个算法最难的地方,不是很容易能想到。

此处的访问条件不再是step B中的条件,而是刚才这个节点2的右孩子我们已经访问过了,所以该轮到其父节点2了。

需要用到一个指针存储着上一迭代的访问过的节点。

以上就是后序遍历非递归版的思路。

实现代码

这里我们以二叉树为例,讨论二叉树的后序遍历的非递归版实现。

我们先看下二叉树的节点TreeNode的数据结构定义。

节点的数据域的类型定义为泛型 T,含有左、右子树,及一个带有数据域的构造函数。

  public class TreeNode
    {
        public T val { get; set; }

        public TreeNode left { get; set; }
        public TreeNode right { get; set; }

        public TreeNode(T data)
        {
            val = data;
        }
    }

二叉树的后序遍历的非递归版实现代码:

public static IList PostorderTraversal(TreeNode root)
        {
            IList rtn = new List();
            var s = new Stack>();
            if (root == null) return rtn;
            s.Push(root);
            TreeNode cur = root;
            TreeNode r = null; //标记访问过的右子树
            while (s.Count > 0)
            {
                 //延伸到左子树
                if (cur != null && cur.left != null)
                {
                    s.Push(cur.left);
                    cur = cur.left;
                }
                else
                {
                    cur = s.Peek();
                   //访问右子树的操作
                    if (cur.right != null && cur.right != r)
                    {
                        cur = cur.right;
                        s.Push(cur);
                    }
                    else //访问节点的操作
                    {
                        rtn.Add(s.Pop().val);
                        r = cur;
                        cur = null;
                    }
                }
            }
            return rtn;
        }

代码分析

代码中用到了两个非常重要的指针 cur,r 。

cur是每次迭代都会更新的指针,它没有非常明确的语义,它在某步迭代中可能是某个子树上的左节点抑或是右节点,也可能含义是标记着上一步迭代中访问到了一个叶子节点,此时需要从栈顶中拿值了。

r 的语义很明确,它是某次迭代过程中,如果发生了访问节点的操作,那么它指向这个访问过的节点,目的是为了判断是否向右子树展开,如下决定是否伸向向右子树的一个且条件的一部分。

也就是同时满足右子树不为空,并且右子树不等于上一迭代中的节点,然后才伸向右子树。

                 //访问右子树的操作
                if (cur.right != null && cur.right != r)
                {
                    cur = cur.right;
                    s.Push(cur);
                }

算法评价

非递归版后序遍历算法的时间复杂度为 O(n),空间复杂度为栈所占的内存空间为 O(n)。

总结

讨论了二叉树的非递归版后序遍历算法,算法借助栈,相比于前序遍历和中序遍历,它多了一个指针指向上一迭代中访问过的节点,目的是为了判断是否向右子树展开,算法的时间和空间复杂度都为 O(n)。

欢迎阅读本公众号已经推送的其他相关文章:

二叉树的非递归版前序遍历

二叉树的非递归版中序遍历

欢迎关注《算法思考与应用》公众号

二叉树非递归版的后序遍历算法_第3张图片

你可能感兴趣的:(算法/LeetCode,经典算法,LeetCode题目研究)