文章目录
- 题目
- 标题和出处
- 难度
- 题目描述
- 要求
- 示例
- 数据范围
- 进阶
- 解法一
- 思路和算法
- 代码
- 复杂度分析
- 解法二
- 思路和算法
- 代码
- 复杂度分析
- 解法三
- 思路和算法
- 代码
- 复杂度分析
题目
标题和出处
标题:二叉树的后序遍历
出处:145. 二叉树的后序遍历
难度
3 级
题目描述
要求
给你二叉树的根结点 root \texttt{root} root,返回其结点值的后序遍历。
示例
示例 1:
输入:
root
=
[1,null,2,3]
\texttt{root = [1,null,2,3]}
root = [1,null,2,3]
输出:
[3,2,1]
\texttt{[3,2,1]}
[3,2,1]
示例 2:
输入:
root
=
[]
\texttt{root = []}
root = []
输出:
[]
\texttt{[]}
[]
示例 3:
输入:
root
=
[1]
\texttt{root = [1]}
root = [1]
输出:
[1]
\texttt{[1]}
[1]
数据范围
- 树中结点数目在范围 [0, 100] \texttt{[0, 100]} [0, 100] 内
- -100 ≤ Node.val ≤ 100 \texttt{-100} \le \texttt{Node.val} \le \texttt{100} -100≤Node.val≤100
进阶
递归解法很简单,你可以使用迭代解法完成吗?
解法一
思路和算法
二叉树的后序遍历的方法为:依次遍历左子树、右子树和根结点,对于左子树和右子树使用同样的方法遍历。由于遍历过程具有递归的性质,因此可以使用递归的方法实现二叉树的后序遍历。
递归的终止条件是当前结点为空。对于非终止条件,递归的做法如下。
-
对当前结点的左子树调用递归。
-
对当前结点的右子树调用递归。
-
将当前结点的结点值加入后序遍历序列。
遍历结束之后即可得到后序遍历序列。
代码
class Solution {
List<Integer> traversal = new ArrayList<Integer>();
public List<Integer> postorderTraversal(TreeNode root) {
postorder(root);
return traversal;
}
public void postorder(TreeNode node) {
if (node == null) {
return;
}
postorder(node.left);
postorder(node.right);
traversal.add(node.val);
}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。每个结点都被访问一次。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。空间复杂度主要是递归调用的栈空间,取决于二叉树的高度,最坏情况下二叉树的高度是 O ( n ) O(n) O(n)。
解法二
思路和算法
使用迭代的方法实现二叉树的后序遍历,则需要使用栈存储结点。
相比于前序遍历和中序遍历,后序遍历的迭代较为复杂。由于后序遍历对于每个子树都是最后访问根结点,因此需要记录上一个访问的结点,才能确保不会重复访问同一个结点。
从根结点开始遍历,遍历的终止条件是栈为空且当前结点为空。遍历的做法如下。
-
如果当前结点不为空,则将当前结点入栈,然后将当前结点移动到其左子结点,重复该操作直到当前结点为空。
-
将当前结点设为栈顶结点,判断其右子结点是否为空以及是否为上一个访问的结点。
-
如果当前结点的右子结点不为空且不为上一个访问的结点,则将当前结点移动到其右子结点。
-
如果当前结点的右子结点为空或者为上一个访问的结点,则将当前结点出栈,将当前结点的结点值加入后序遍历序列,将上一个访问的结点设为当前结点,将当前结点设为空。
-
-
重复上述操作,直到达到遍历的终止条件。
代码
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> traversal = new ArrayList<Integer>();
Deque<TreeNode> stack = new ArrayDeque<TreeNode>();
TreeNode node = root, prev = null;
while (!stack.isEmpty() || node != null) {
while (node != null) {
stack.push(node);
node = node.left;
}
node = stack.peek();
if (node.right != null && node.right != prev) {
node = node.right;
} else {
stack.pop();
traversal.add(node.val);
prev = node;
node = null;
}
}
return traversal;
}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。每个结点都被访问一次。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。空间复杂度主要是栈空间,取决于二叉树的高度,最坏情况下二叉树的高度是 O ( n ) O(n) O(n)。
解法三
思路和算法
莫里斯遍历是使用常数空间遍历二叉树的方法,由 J. H. Morris 提出。莫里斯遍历的核心思想是利用二叉树的空闲指针维护遍历顺序,达到省略栈空间的目的。
相比于前序遍历和中序遍历,后序遍历的莫里斯遍历较为复杂。
从根结点开始遍历,遍历的终止条件是当前结点为空。
-
对于每个结点,判断当前结点的左子树是否为空,执行相应的操作。
-
如果当前结点的左子树为空,则将当前结点移动到其右子结点。
-
如果当前结点的左子树不为空,则找到当前结点的前驱结点,前驱结点为当前结点的左子树中的最右边的结点,判断前驱结点的右子结点是否为空。
-
如果前驱结点的右子结点为空,则将前驱结点的右子结点设为当前结点,将当前结点移动到其左子结点。
-
如果前驱结点的右子结点不为空,则将前驱结点的右子结点设为空,将当前结点的左子结点至当前结点的前驱结点的路径上的所有结点的结点值倒序加入后序遍历序列,将当前结点移动到其右子结点。
-
-
-
重复上述操作,直到达到遍历的终止条件。最后将根结点的结点值加入后序遍历序列。
代码
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> traversal = new ArrayList<Integer>();
TreeNode node = root;
while (node != null) {
if (node.left == null) {
node = node.right;
} else {
TreeNode predecessor = node.left;
while (predecessor.right != null && predecessor.right != node) {
predecessor = predecessor.right;
}
if (predecessor.right == null) {
predecessor.right = node;
node = node.left;
} else {
predecessor.right = null;
addToTraversal(node.left, traversal);
node = node.right;
}
}
}
addToTraversal(root, traversal);
return traversal;
}
public void addToTraversal(TreeNode node, List<Integer> traversal) {
int size1 = traversal.size();
while (node != null) {
traversal.add(node.val);
node = node.right;
}
int size2 = traversal.size();
int left = size1, right = size2 - 1;
while (left < right) {
int val1 = traversal.get(left), val2 = traversal.get(right);
traversal.set(left, val2);
traversal.set(right, val1);
left++;
right--;
}
}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。使用莫里斯遍历,每个结点最多被访问两次。
-
空间复杂度: O ( 1 ) O(1) O(1)。注意返回值不计入空间复杂度。