二叉树的前、中、后序遍历都是面试中相当重要的内容,本节我们就来讲讲其中比较复杂的后序遍历,如果后序遍历学懂了,前序和中序也就都能学会了。
后序遍历的非递归实现有三种基本的思路:反转法、访问标记法、和Morris法。
其中反转法最容易理解,也比较容易实现。而访问标记法是利用栈的先进后出逆向思考的方法,看过代码思路后相对容易理解。最后的Morris法是一个老外发明的巧妙思想:不使用栈,而是用好树中的null指针,但是实现后序仍然非常麻烦,个人认为这是比较难理解的一个解法。
1. 反转法
这里的反转法,指的是 "迭代 + 反转",如下图,我们先观察后序遍历的结果是seq={9 5 7 4 3},如果我们将其整体反转的话就是new_seq={3 4 7 5 9}。
即后序遍历原来是 左右中 的顺序,我们先把它 按 中右左 顺序排好,再进行反转。
要得到new_seq的方法和前序遍历思路几乎一致,只不过是左右反了。前序是先中间,再左边然后右边,而这里是先中间,再后边然后左边。代码如下:
public static List<Integer> postOrderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>(); // 创建一个空的结果列表,用于保存后续遍历的结果
if (root == null) return res; // 如果二叉树的根节点为空,直接返回空的结果列表
Stack<TreeNode> stack = new Stack<>(); // 创建一个栈,用于辅助遍历二叉树
TreeNode node = root; // 创建一个指针node,并将其指向根节点
while (!stack.isEmpty() || node != null) {
// 循环条件为栈不为空或者当前节点node不为空,循环过程中会遍历整个二叉树
while (node != null) {
// 进入一个内部循环,将当前节点node的右子树节点依次入栈,并将当前节点的值加入结果列表res
// 这是因为后序遍历的顺序是“左子树 -> 右子树 -> 根节点”
// 先遍历右子树,并将右子树节点压入栈中
res.add(node.val); // 将当前节点的值加入结果列表
stack.push(node); // 将当前节点压入栈中
node = node.right; // 将当前节点指针node设为其右子节点,继续循环
}
// 当node为null时,说明当前节点已经遍历完毕,从栈中弹出节点并将其赋值给node
// 这时候需要遍历当前节点的左子树
node = stack.pop(); // 从栈中弹出节点,并将其赋值给node
node = node.left; // 将node设为其左子节点,继续执行下一次循环,直到node为null,表示已经遍历到了叶子节点
}
// 当栈为空且当前节点node为null时,表示二叉树的后序遍历完成
Collections.reverse(res); // 将结果列表res中的元素逆序,因为前面在遍历的过程中,是先遍历了根节点的右子树,再遍历了左子树,所以需要将结果逆序
return res;
}
2. 访问标记法
后序遍历的访问标记法是一种迭代实现的方法,使用栈来辅助遍历。对于每个节点,使用一个标记表示是否已经访问过其左子树和右子树。在栈中保存节点和对应的标记。当一个节点的左子树和右子树都已访问过时,才可以访问该节点。在访问该节点之后,将其出栈,并将其标记为已访问,然后继续检查栈顶节点。(访问标记法本身有点绕,建议画简单的二叉树进行模拟,能够加深理解)
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
public class PostorderTraversal {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.peek();
// 如果该节点的左子树和右子树都已访问过,则可以访问该节点
if (node.left == null && node.right == null) {
result.add(node.val);
stack.pop(); // 将已访问的节点出栈
} else {
// 由于后续遍历是“左子树 -> 右子树 -> 根节点”,因此应该先将右子树入栈,再将左子树入栈
if (node.right != null) {
stack.push(node.right);
node.right = null; // 访问过右子树后,将右子节点置为null,以区分是否已访问
}
if (node.left != null) {
stack.push(node.left);
node.left = null; // 访问过左子树后,将左子节点置为null,以区分是否已访问
}
}
}
return result;
}
}
3. Morris法
Morris本身对于理解有些难度,如果刚开始看不懂,可以先跳过,不属于非常重要的知识。(因为我自己刚学的时候就卡了好几个小时)
Morris法是一种用于遍历二叉树的非递归算法,它将后续遍历的访问标记法的空间复杂度从 O(n) 降低到 O(1)。它基于线索二叉树的思想,通过在二叉树的叶子节点中建立回指(指向父节点)来实现遍历。
Morris法的具体步骤如下:
- 初始化当前节点为根节点
cur = root
。 - 如果
cur
的左子树不为空,找到cur
的左子树的最右节点,记为mostRight
。- 如果
mostRight
的右指针为空,将其指向cur
,并将cur
移向其左子节点,即cur = cur.left
。 - 如果
mostRight
的右指针指向cur
,说明左子树已经遍历完毕,将其右指针重置为空,然后将cur
移向其右子节点,即cur = cur.right
。
- 如果
- 如果
cur
的左子树为空,将cur
移向其右子节点,即cur = cur.right
。 - 重复步骤 2 和步骤 3,直到遍历完成。
下面是使用Morris法实现后续遍历的Java代码:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
public class MorrisPostorderTraversal {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
TreeNode dummy = new TreeNode(-1); // 创建一个虚拟节点,作为根节点的前驱节点
dummy.left = root;
TreeNode cur = dummy; // 当前节点
while (cur != null) {
if (cur.left == null) {
cur = cur.right;
} else {
// 找到cur的左子树的最右节点mostRight
TreeNode mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
mostRight.right = cur; // 建立回指
cur = cur.left;
} else {
// 左子树已经遍历完毕,进行后序遍历
addPath(result, cur.left);
mostRight.right = null; // 恢复回指为null
cur = cur.right;
}
}
}
// 将根节点的前驱节点排除,并添加到结果列表中
result.remove(0);
return result;
}
// 添加节点路径
private void addPath(List<Integer> result, TreeNode node) {
List<Integer> path = new ArrayList<>();
while (node != null) {
path.add(node.val);
node = node.right;
}
Collections.reverse(path);
result.addAll(path);
}
}