【LeetCode 二叉树专项】二叉树的后序遍历(145)

1. 题目

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

1.1 示例

  • 示例 1 1 1
  • 输入: [1, null, 2, 3]
  • 输出: [3, 2, 1]

在这里插入图片描述

1.2 说明

1.3 进阶

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

2. 解法一(递归法)

2.1 分析

类似【LeetCode 二叉树专项】二叉树的前序遍历(144),二叉树的后序遍历使用递归的方式很容易实现,即按照访问 左子树 -> 右子树 -> 根节点 的方式遍历这棵树(如下动图所示),而在访问左子树或者右子树的时候,按照同样的方式遍历,直到遍历完整棵树。可以看出,整个遍历过程天然具有递归的性质,因此可以直接用递归函数来模拟这一过程。

在这里插入图片描述

2.2 实现

from typing import List


class TreeNode:
    def __init__(self, val=0, left: 'TreeNode' = None, right: 'TreeNode' = None):
        self.val = val
        self.left = left
        self.right = right


class Solution:
    def __init__(self):
        self.tree = []

    def _postorder(self, root: TreeNode):
        if not root:
            return
        self._postorder(root.left)
        self._postorder(root.right)
        self.tree.append(root.val)

    def postorder_traversal(self, root: TreeNode) -> List[int]:
        self._postorder(root)
        return self.tree

2.3 复杂度

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的节点数。每一个节点恰好被遍历一次。
  • 空间复杂度: O ( n ) O(n) O(n) ,为递归过程中递归栈的开销和实例属性 self.tree 所占用空间两者组成,其中前者平均情况下为 O ( log ⁡ n ) O(\log n) O(logn) ,最坏情况下树呈现链状,为 O ( n ) O(n) O(n)

3. 解法二(迭代法)

3.1 双栈

3.1.1 分析

实际上,从上述递归实现可以看出,在实现的过程中有两组递归调用。那么对应地,在使用栈作为辅助的数据结构时,也自然考虑使用两个栈。

使用两个栈作为辅助数据结构实现迭代形式的后序遍历时,考虑栈“先进后出,后进先出”的特点,可以进一步将实现目标细化为:如何不断操作两个栈使得其中一个栈最终保存了后序遍历的逆序序列,然后依次将该栈中元素弹出,即可得到后序遍历序列。

具体地,使用两个栈来实现迭代形式的后序遍历,其主要步骤如下:

  • 步骤一: 将根结点压入第一个栈的顶部;
  • 步骤二: 在第一个栈不为空的前提下,不断循环执行下列步骤:
    • (a):从第一个栈的顶部弹出一个元素后将该元素压入第二个栈的顶部;
    • (b):将上述弹出元素的左子节点和右子节点依次压入第一个栈的顶部;
  • 步骤三: 依次从第二个栈的顶部弹出元素进行遍历。

下图演示了上述算法的执行过程:
在这里插入图片描述
在给出具体实现之前,下面先给出对于上述案例使用两个栈实现迭代形式后续遍历的详细步骤:

  1. 1 对应的节点压入第一个栈 stack1

    stack1 = [1]
    stack2 = []

  2. 1 对应节点从 stack1 顶部弹出并压入 stack2 ;将 1 对应节点的左右子节点依次压入 stack1 顶部:

    stack1 = [2, 3]
    stack2 = [1]

  3. 3 对应节点从 stack1 顶部弹出并压入 stack2 ;将 3 对应节点的左子节点压入 stack1 顶部:

    stack1 = [2, 6]
    stack2 = [1, 3]

  4. 6 对应节点从 stack1 顶部弹出并压入 stack2

    stack1 = [2]
    stack2 = [1, 3, 6]

  5. 2 对应节点从 stack1 顶部弹出并压入 stack2 ;将 2 对应节点的左右子节点依次压入 stack1 顶部:

    stack1 = [4, 5]
    stack2 = [1, 3, 6, 2]

  6. 5 对应节点从 stack1 顶部弹出并压入 stack2

    stack1 = [4]
    stack2 = [1, 3, 6, 2, 5]

  7. 4 对应节点从 stack1 顶部弹出并压入 stack2

    stack1 = []
    stack2 = [1, 3, 6, 2, 5, 4]

  8. 依次将 stack2 中元素弹出,其弹出的顺序满足后序遍历的顺序。

3.1.2 实现

import stat
from typing import List


class TreeNode:
    def __init__(self, val=0, left: 'TreeNode' = None, right: 'TreeNode' = None):
        self.val = val
        self.left = left
        self.right = right


class Solution:
    def __init__(self):
        self.tree = []

    def iterative_postorder(self, root: TreeNode) -> List[int]:
        if not root:
            return self.tree
        stack1 = [root]
        stack2 = []
        while stack1:
            node = stack1.pop()
            if node.left:
                stack1.append(node.left)
            if node.right:
                stack1.append(node.right)
            stack2.append(node)
        while stack2:
            self.tree.append(stack2.pop().val)
        return self.tree


def main():
    root = TreeNode(1)
    root.left = TreeNode(2)
    root.right = TreeNode(3)
    root.left.left = TreeNode(4)
    root.left.right = TreeNode(5)
    root.right.left = TreeNode(6)
    root.right.right = TreeNode(7)
    sln = Solution()
    print(sln.iterative_postorder(root))  # [4, 5, 2, 6, 7, 3, 1]


if __name__ == '__main__':
    main()

3.1.3 复杂度

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n)

3.2 单栈

3.2.1 分析

二叉树迭代形式的后序遍历仍然借用栈作为辅助的数据结构,但是要比前序遍历的迭代形式中序遍历的迭代形式都要复杂,实现后序遍历迭代形式的主要思想是

  • 不断利用节点的 left 指针域向下移动至最左边的节点,在向下移动的过程中,先后将 root 节点和其右子节点压入栈中;
  • 一旦抵达最左侧节点,如果:
    • 该节点没有右子节点,则执行对该节点的遍历操作;
    • 该节点有右子节点,则修改 root 变量所引用的节点,使得右子节点先被遍历。

实现迭代形式的后序遍历主要步骤如下:

  • 步骤一: 创建一个空的栈 stack
  • 步骤二:root 不为 None 的情况下,重复下列步骤:
    • (a):首先将 root 的右子节点和其自身分别先后压入栈 stack
    • (b):然后让 root 引用其左子节点;
  • 步骤三: 从栈 stack 的顶部弹出一个节点并使用 root 引用该节点:
    • (a):如果弹出的节点有右子节点且该右子节点处于栈顶,则弹出该右子节点并将 root 引用的节点压栈,然后使得 root 引用其右子节点;
    • (b):否则,执行对 root 的遍历操作且使得 root 引用 None
  • 步骤四: 重复步骤二和三,直到栈 stack 为空。

为便于理解,对于形如下列形式的二叉树,使用上述迭代步骤的具体情况如下:

		            1
		          /   \
		        2      3
		      /  \	  /  \
		    4     5	 6	  7
  • 步骤一: 创建一个空的栈 stack
  • 步骤二:root -> 1 不为 None 的情况下,重复下列步骤:
    • (a):首先将 root -> 1 的右子节点和其自身分别先后压入栈 stack = [3, 1]
    • (b):然后让 root 引用其左子节点, root -> 2

    • (a):首先将 root -> 2 的右子节点和其自身分别先后压入栈 stack = [3, 1, 5, 2]
    • (b):然后让 root 引用其左子节点, root -> 4

    • (a):首先将 root -> 4 自身(此时其右子节点为 None )压入栈 stack = [3, 1, 5, 2, 4]
    • (b):然后让 root 引用其左子节点, root -> None
  • 步骤二: 不满足步骤二的执行条件;
  • 步骤三: 从栈 stack 的顶部弹出节点(stack = [3, 1, 5, 2])且 root -> 4 ,由于 4 对应的节点没有右子节点,则
    • (b):执行对该节点的遍历操作,然后 root -> None
  • 步骤三: 从栈 stack 的顶部弹出节点(stack = [3, 1, 5])且 root -> 2 ,由于:
    • (a):2 对应的节点有右子节点且该右子节点处于栈顶,则弹出栈顶节点(stack = [3, 1])并将 2 对应节点压栈(stack = [3, 1, 2]),然后使得 root 引用其右子节点 root -> 5
  • 步骤二:root -> 5 不为 None 的情况下,重复下列步骤:
    • (a):首先将 root -> 5 自身(此时其右子节点为 None )压入栈 stack = [3, 1, 2, 5]
    • (b):然后让 root 引用其左子节点, root -> None
  • 步骤三: 从栈 stack 的顶部弹出节点(stack = [3, 1, 2])且 root -> 5 ,由于 5 对应的节点没有右子节点,则
    • (b):执行对该节点的遍历操作,然后 root -> None
  • 步骤二: 不满足步骤二的执行条件;
  • 步骤三: 从栈 stack 的顶部弹出节点(stack = [3, 1])且 root -> 2 ,由于弹出的节点有右子节点但该右子节点不处于栈顶,因此:
    • (b):执行对 root 的遍历操作且使得 root 引用 None
  • 步骤二: 不满足步骤二的执行条件;
  • 步骤三: 从栈 stack 的顶部弹出节点(stack = [3])且 root -> 1 ,由于:
    • (a):1 对应的节点有右子节点且该右子节点处于栈顶,则弹出栈顶节点(stack = [])并将 1 对应节点压栈(stack = [1]),然后使得 root 引用其右子节点 root -> 3
  • 步骤二:root -> 3 不为 None 的情况下,重复下列步骤:
    • (a):首先将 root 的右子节点和其自身分别先后压入栈 stack = [1, 7, 3]
    • (b):然后让 root 引用其左子节点 root -> 6

    • (a):首先将 root 自身(其右子节点为 None)压入栈 stack = [1, 7, 3, 6]
    • (b):然后让 root 引用其左子节点 root -> None
  • 步骤三: 从栈 stack 的顶部弹出节点(stack = [1, 7, 3])且 root -> 6 ,由于弹出节点无右子节点,则:
    • (b):执行对 root 的遍历操作且使得 root 引用 None
  • 步骤二: 不满足步骤二的执行条件;
  • 步骤三: 从栈 stack 的顶部弹出节点(stack = [1, 7])且 root -> 3 ,由于:
    • (a):3 对应的节点有右子节点且该右子节点处于栈顶,则弹出栈顶节点(stack = [1])并将 3 对应节点压栈(stack = [1, 3]),然后使得 root 引用其右子节点 root -> 7
  • 步骤二:root -> 7 不为 None 的情况下,重复下列步骤:
    • (a):首先将 root 自身压入栈 stack = [1, 3, 7]
    • (b):然后让 root 引用其左子节点 root -> None
  • 步骤三: 从栈 stack 的顶部弹出节点(stack = [1, 3])且 root -> 7 ,由于弹出节点无右子节点,则:
    • (b):执行对 root 的遍历操作且使得 root 引用 None
  • 步骤二: 不满足步骤二的执行条件;
  • 步骤三: 从栈 stack 的顶部弹出节点(stack = [1])且 root -> 3

3.2.2 解答

from typing import List


class TreeNode:
    def __init__(self, val=0, left: 'TreeNode' = None, right: 'TreeNode' = None):
        self.val = val
        self.left = left
        self.right = right


class Solution:
    def __init__(self):
        self.tree = []

    def _peek(self, stack: List[int]):
        if len(stack) > 0:
            return stack[-1]
        return None

    def iterative_postorder(self, root: TreeNode) -> List[int]:
        if not root:
            return self.tree
        stack = []
        while True:
            while root:
                if root.right:
                    stack.append(root.right)
                stack.append(root)
                root = root.left
            root = stack.pop()
            if root.right and self._peek(stack) == root.right:
                stack.pop()
                stack.append(root)
                root = root.right
            else:
                self.tree.append(root.val)
                root = None
            if not stack:
                break
        return self.tree


def main():
    root = TreeNode(1)
    root.left = TreeNode(2)
    root.right = TreeNode(3)
    root.left.left = TreeNode(4)
    root.left.right = TreeNode(5)
    root.right.left = TreeNode(6)
    root.right.right = TreeNode(7)
    sln = Solution()
    print(sln.iterative_postorder(root))  # [4, 5, 2, 6, 7, 3, 1]


if __name__ == '__main__':
    main()

3.2.3 复杂度

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 为二叉树节点的个数。二叉树的遍历中每个节点会被访问一次且只会被访问一次。
  • 空间复杂度: O ( n ) O(n) O(n)。空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到 O ( n ) O(n) O(n) 的级别。

4. 解法三(Morris 遍历)

猜你喜欢

转载自blog.csdn.net/weixin_37780776/article/details/121159410