算法通关村—迭代实现二叉树的后序遍历

二叉树的前、中、后序遍历都是面试中相当重要的内容,本节我们就来讲讲其中比较复杂的后序遍历,如果后序遍历学懂了,前序和中序也就都能学会了。

后序遍历的非递归实现有三种基本的思路:反转法、访问标记法、和Morris法。

其中反转法最容易理解,也比较容易实现。而访问标记法是利用栈的先进后出逆向思考的方法,看过代码思路后相对容易理解。最后的Morris法是一个老外发明的巧妙思想:不使用栈,而是用好树中的null指针,但是实现后序仍然非常麻烦,个人认为这是比较难理解的一个解法

1. 反转法

这里的反转法,指的是 "迭代 + 反转",如下图,我们先观察后序遍历的结果是seq={9 5 7 4 3},如果我们将其整体反转的话就是new_seq={3 4 7 5 9}。

即后序遍历原来是 左右中 的顺序,我们先把它 按 中右左 顺序排好,再进行反转。

算法通关村—迭代实现二叉树的后序遍历_第1张图片

要得到new_seq的方法和前序遍历思路几乎一致,只不过是左右反了。前序是先中间,再左边然后右边,而这里是先中间,再后边然后左边。代码如下:

public static List postOrderTraversal(TreeNode root) {
    List res = new ArrayList<>(); // 创建一个空的结果列表,用于保存后续遍历的结果
    if (root == null) return res; // 如果二叉树的根节点为空,直接返回空的结果列表

    Stack 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 postorderTraversal(TreeNode root) {
        List result = new ArrayList<>();
        if (root == null) {
            return result;
        }

        Stack 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法的具体步骤如下:

  1. 初始化当前节点为根节点 cur = root
  2. 如果 cur 的左子树不为空,找到 cur 的左子树的最右节点,记为 mostRight
    • 如果 mostRight 的右指针为空,将其指向 cur,并将 cur 移向其左子节点,即 cur = cur.left
    • 如果 mostRight 的右指针指向 cur,说明左子树已经遍历完毕,将其右指针重置为空,然后将 cur 移向其右子节点,即 cur = cur.right
  3. 如果 cur 的左子树为空,将 cur 移向其右子节点,即 cur = cur.right
  4. 重复步骤 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 postorderTraversal(TreeNode root) {
        List 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 result, TreeNode node) {
        List path = new ArrayList<>();
        while (node != null) {
            path.add(node.val);
            node = node.right;
        }
        Collections.reverse(path);
        result.addAll(path);
    }
}

你可能感兴趣的:(算法,数据结构,java,笔记)