Leetcode145. Morris后序遍历


前言

前面我们遇到了Morris中序遍历算法,它可以实现O(1)的空间复杂度和O(n)的时间复杂度。那么在后序遍历时可以使用该算法来降低对空间的使用吗?

幸运的是,确实存在Morris后序遍历算法,它和Moriis中序遍历算法相似,大家学习后可以自行比较两个算法之间的关联与区别。

下面我们结合Leetcode中第145题二叉树的后序遍历,来学习Morris后遍历算法

以下使用python3~


一、Morris后序遍历算法详解

本部分参考leetcode145官方题解
后序遍历的顺序是左-右-中,在不使用递归的情况下,我们需要遍历完左子树可以再回到当前节点,因此仍然要像Morris中序遍历那样,找到当前节点的前驱节点。

找当前节点cur的前驱节点:(关键)

pre = cur.left
if pre:
   	while pre.right and pre.right!=cur:
       	pre = pre.right
    # while循环结束后,得到的pre即为cur在中序遍历下的前驱节点

由于后序是右子树在根之前被遍历,我们就不能在遍历到某个节点时直接将其值放入输出序列中,而是要将当前节点的左子节点到其前驱节点的路径倒序放入结果中。

1. 具体算法

从根节点root出发

  1. 如果当前节点的左子节点为空,则遍历当前节点的右子节点;
  2. 如果当前节点的左子节点不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点;
  • 如果前驱节点的右子节点为空,将前驱节点的右子节点设置为当前节点,当前节点更新为当前节点的左子节点。
    - 如果前驱节点的右子节点为当前节点,将它的右子节点重新设为空。倒序输出从当前节点的左子节点到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右子节点。

和Morris中序遍历的区别:
1、不能直接遍历当前节点的值,要调用函数获取当前节点之前的节点路径(因为后序中根节点最后访问)
2、当前驱节点的右子节点为当前节点时,一定要将它的右子节点重新设为空,才能进行后续操作,这样能保证正确获取路径。而在中序遍历中,不置空也可以。
其他部分都是一样的,大家对比两个算法的代码框架就能进一步理解了~

2. 代码实现

ans = []
def morris_postorder(root):
	def add_path(cur):
		global ans
	# 倒序输出从当前节点的左子节点,到其前驱节点这条路径上的所有节点。
	    tmp = []
	    while cur:
	        tmp.append(cur.val)
	        cur = cur.right
	    ans += tmp[::-1]
	cur = root
	while cur:
	    pre = cur.left
	    if pre:
	        while pre.right and pre.right!=cur:
	            pre = pre.right
	        if not pre.right:
	            pre.right = cur
	            cur = cur.left
	            continue
	        else:
	            pre.right = None
	            add_path(cur.left)
	    cur = cur.right
	add_path(root)
	return ans

从程序中可以看出,当cur为空时就退出while循环了,此时cur只是走到了树的叶子最右端,但并没有加入从最右叶子到树根节点的这条路径。
想象一下当root节点的所有右子树节点都没有左子树时,cur可以一路向右走,当走到叶子节点的right时,cur变为空,退出循环。退出时还没来得及添加路径。

因此while结束后,再进行一次add_path(root)。

二、Leetcode 145题目描述

在这里插入图片描述

三、解题方法(×3)

1. 简单递归

解题思路

递归是最常用和最直观的遍历算法,后序遍历时,先递归左子树,再递归右子树,最后遍历当前节点的值。
时间复杂度O(n),空间复杂度O(n)

完整代码

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def postorderTraversal(self, root: TreeNode) -> List[int]:
        self.res = []

        def postorder(root):
            if not root: return
            postorder(root.left)
            postorder(root.right)
            self.res += [root.val]
        
        postorder(root)
        return self.res 

2. 利用栈的迭代

解题思路

递归算法得以实现的原因是其内部使用了栈,我们在迭代过程中也使用一个栈来辅助,帮助我们能够返回上一层节点,就可以获得递归的效果。
我们先按照中-右-左的顺序获得遍历结果,最后将其翻转就是后序遍历结果啦。

  • 遇到一个非空节点时,就将其入栈并遍历它的值(先中),并走到它的右子节点上去(再右);
  • 当节点为空时,将栈顶弹出,并走到栈顶节点的左子节点上去(后左);
  • 循环前两步,直到栈空结束,将最终得到的遍历结果反转。
    (注意:入栈的节点一定已经被遍历过了)

时间复杂度O(n),空间复杂度O(n)

完整代码

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def postorderTraversal(self, root: TreeNode) -> List[int]:
        ans = []
        def postorder(root, ans):
            # 当前遍历顺序为 中-右-左,最后将得到结果反转,即是后续遍历的顺序。
            stack = []
            while stack or root:
                if root:
                    # 先遍历当前节点,再将当前节点转移到其右子树
                    stack.append(root)
                    ans.append(root.val)
                    root = root.right
                else:
                    # 当右子树到头时,再返回上一节点,遍历左边
                    root = stack.pop().left
            return ans.reverse()
        postorder(root, ans)
        return ans

看到这里,我们简单变通一下就可以得到前序遍历的迭代算法~ 代码如下
大家可以在Leetcode144. 二叉树的前序遍历上试一下。

前序:中-左-右,在迭代时先走到左子树,再弹出栈顶,并到其右子树即可。最后也不需要反转。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def preorderTraversal(self, root: TreeNode) -> List[int]:
        ans = []
        def postorder(root, ans):
            # 当前遍历顺序为 中-右-左,即是前序遍历顺序。
            stack = []
            while stack or root:
                if root:
                    # 先遍历当前节点,再将当前节点转移到其左子树
                    stack.append(root)
                    ans.append(root.val)
                    root = root.left
                else:
                    # 当右子树到头时,再返回上一节点,遍历右边
                    root = stack.pop().right
        postorder(root, ans)
        return ans

那么对于中序遍历呢? 想法可能稍复杂一些,中序:左-中-右。这要求我们在遍历每个节点时,将其入栈但不要把其值取出来。要先遍历到节点的最左边,再逐一出栈取值,这样才能满足先左后中的顺序。取值后再遍历右子树。即:
入栈不取值,出栈取值

代码如下

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def inorderTraversal(self, root: TreeNode) -> List[int]:
        stack = []
        res = []
        while stack or root:
            if root:
                stack.append(root)
                root = root.left      # 一路向左入栈
            else:
                root = stack.pop()
                res.append(root.val)  # 出栈时取节点值
                root = root.right
        return res

3. Morris后序遍历

解题思路

直接使用我们刚刚介绍的Morris后序遍历算法就可以啦~
时间复杂度O(n),空间复杂度O(1)

完整代码

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def postorderTraversal(self, root: TreeNode) -> List[int]:
        self.ans = []
        def add_path(cur):
            tmp = []
            while cur:
                tmp.append(cur.val)
                cur = cur.right
            self.ans += tmp[::-1]
        cur = root
        while cur:
            pre = cur.left
            if pre:
                while pre.right and pre.right!=cur:
                    pre = pre.right
                if not pre.right:
                    pre.right = cur
                    cur = cur.left
                    continue
                else:
                    pre.right = None
                    add_path(cur.left)
            cur = cur.right
        add_path(root)
        return self.ans
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值