文章目录
1. 题目
给定一个二叉树,返回它的 后序 遍历。
1.1 示例
- 示例 1 1 1:
- 输入:
[1, null, 2, 3]
- 输出:
[3, 2, 1]
1.2 说明
- 来源: 力扣(LeetCode)
- 链接: https://leetcode-cn.com/problems/binary-tree-postorder-traversal
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
对应的节点压入第一个栈stack1
:stack1 = [1]
stack2 = []
-
将
1
对应节点从stack1
顶部弹出并压入stack2
;将1
对应节点的左右子节点依次压入stack1
顶部:stack1 = [2, 3]
stack2 = [1]
-
将
3
对应节点从stack1
顶部弹出并压入stack2
;将3
对应节点的左子节点压入stack1
顶部:stack1 = [2, 6]
stack2 = [1, 3]
-
将
6
对应节点从stack1
顶部弹出并压入stack2
:stack1 = [2]
stack2 = [1, 3, 6]
-
将
2
对应节点从stack1
顶部弹出并压入stack2
;将2
对应节点的左右子节点依次压入stack1
顶部:stack1 = [4, 5]
stack2 = [1, 3, 6, 2]
-
将
5
对应节点从stack1
顶部弹出并压入stack2
:stack1 = [4]
stack2 = [1, 3, 6, 2, 5]
-
将
4
对应节点从stack1
顶部弹出并压入stack2
:stack1 = []
stack2 = [1, 3, 6, 2, 5, 4]
-
依次将
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
引用其左子节点;
- (a):首先将
- 步骤三: 从栈
stack
的顶部弹出一个节点并使用root
引用该节点:- (a):如果弹出的节点有右子节点且该右子节点处于栈顶,则弹出该右子节点并将
root
引用的节点压栈,然后使得root
引用其右子节点; - (b):否则,执行对
root
的遍历操作且使得root
引用None
。
- (a):如果弹出的节点有右子节点且该右子节点处于栈顶,则弹出该右子节点并将
- 步骤四: 重复步骤二和三,直到栈
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) 的级别。