[路飞]_前端算法第二十四弹-145. 二叉树的后序遍历

303 阅读4分钟

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

给定一个二叉树,返回它的 后序 遍历。

示例:

输入: [1,null,2,3]
1
 \
   2
 /
3

输出: [3,2,1]

进阶: 递归算法很简单,你可以通过迭代算法完成吗?

既然说递归方法很简单,那我们就先来看简单的递归方法

递归

首先,我们需要了解什么是二叉树的后序遍历,按照左→右→根节点的方式遍历这棵树,所得到的便是二叉树的后序遍历。而在访问其子树时,无论左右子树,都也应按照这个顺序遍历。直至整棵树完成。这整个遍历过程,似乎自带递归属性,让我们第一眼看到,就会想到递归方法。

我们定义一个postorder(root)表示当前遍历到的root节点。先递归遍历左子树,root.left,遍历完成之后再递归遍历右子树root.right,最后记录根节点。终止条件为root全部遍历。

function postorder(root, res=[]){
    if(!root) return;
    postorder(root.left,res);
    postorder(root.right,res);
    res.push(root.val)
    return res;
}

复杂度分析

  • 时间复杂度:O(n),其中 n 是二叉搜索树的节点数。每一个节点恰好被遍历一次。
  • 空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(log⁡n),最坏情况下树呈现链状,为 O(n)。

既然递归方法已经想出来了,那么自然要挑战一下迭代方法。

迭代方法

既然题目有要求了,那我们自然是可以使用迭代方法完成的。这两种方法其实是等价的,区别就在于递归的时候是隐式的维护了一个栈,而迭代时,我们将这个栈模拟出来。

    3
 ↙     ↘
9       4
     ↙    ↘
    5       7

我们以上面的二叉树为例。我们按中→右→左入栈,则出栈顺序便是左→右→中。但是我们在遍历的时候,我们无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。

  • 新建一个stack栈[],一个res = []

  • 3入栈,[3]

  • 入栈空节点,代表3访问过。stack=[3,null]

  • 判断right存在,4入栈,stack= [3, null, 4]

  • 判断left存在,9入栈,stack=[3, null, 4, 9]

  • 第一次循环结束,stack不为空,则取出栈顶元素。

  • node = 9,9不为空,代表没有访问过,9入栈,加入标记stack=[3, null, 4, 9, null]

  • 判断左右子节点都不存在,循环结束。

  • stack不为空,则取出栈顶元素,node=null,代表有元素访问结束,需要出栈。

  • 取出栈顶元素,9,传入res中,res=[9]。循环结束。

  • stack不为空,则取出栈顶元素。node=4,代表没有访问过,4入栈。加入标记stack=[3, null, 4, null]

  • 判断,左右子节点存在,入栈。stack=[3, null, 4, null,7,5]。循环结束。

  • 取出栈顶元素,判断非空,入栈,加入null标记stack=[3, null, 4, null, 7, 5, null]。

  • 判断左右子节点为空,进入下一个循环。

  • 取出栈顶元素,判断为空,出栈栈顶元素stack=[3, null, 4, null, 7]。res=[9,5]。进入下一个循环。

  • 取出栈顶元素,判断非空,入栈,加入null标记stack=[3, null, 4, null, 7, null]。

  • 判断左右子节点为空,进入下一个循环。

  • 取出栈顶元素,判断为空,出栈栈顶元素stack=[3, null, 4, null,]。res=[9,5,7]。进入下一个循环。

  • 取出栈顶元素,判断为空,出栈栈顶元素stack=[3, null]。res=[9,5,7,4]。进入下一个循环。

  • 取出栈顶元素,判断为空,出栈栈顶元素stack=[]。res=[9,5,7,4,3]。进入下一个循环。

  • stack为空,遍历结束。返回res

    var postorderTraversal = function(root, res = []) { const stack = []; if (root) stack.push(root); while(stack.length) { const node = stack.pop(); if(!node) { // 拿到标记节点,处理上一个节点 res.push(stack.pop().val); continue; } stack.push(node); // 中 stack.push(null); // 节点访问过,但是还没有处理,加入空节点做为标记。 if (node.right) stack.push(node.right); // 右 if (node.left) stack.push(node.left); // 左 }; return res; };

复杂度分析

  • 时间复杂度:O(n),其中 n 是二叉搜索树的节点数。每一个节点恰好被遍历一次。
  • 空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。