二叉树的前序、中序和后序遍历(迭代法+递归法)

144.二叉树的前序遍历

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

示例 1:

输入:root = [1,null,2,3]

输出:[1,2,3]

解释:

二叉树的前序、中序和后序遍历(迭代法+递归法)_第1张图片

示例 2:

输入:root = [1,2,3,4,5,null,8,null,null,6,7,9]

输出:[1,2,4,5,6,7,3,8,9]

解释:

二叉树的前序、中序和后序遍历(迭代法+递归法)_第2张图片

示例 3:

输入:root = []

输出:[]

示例 4:

输入:root = [1]

输出:[1]

提示:

  • 树中节点数目在范围 [0, 100] 内
  • -100 <= Node.val <= 100

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

解题思路1

1. 前序遍历的定义

前序遍历是一种二叉树遍历方式,访问顺序为:

  • 根节点 → 左子树 → 右子树
  • 特点是先访问当前节点,然后递归访问左子树,最后递归访问右子树。

2. 递归的适用性

  • 前序遍历天然适合递归实现,因为它遵循“分治法”的思想:对于每个节点,先处理自己,再将任务分解到左右子树。
  • 递归通过调用栈隐式地管理节点的访问顺序。

3. 实现步骤

  1. 定义结果存储:使用一个列表(List)存储遍历结果。
  2. 递归函数设计
    • 如果当前节点不为空:
      • 先将当前节点的值加入结果列表(根)。
      • 递归处理左子树。
      • 递归处理右子树。
    • 如果当前节点为空,直接返回(递归的终止条件)。
  3. 返回结果:遍历完成后,返回存储结果的列表。

代码1 

class Solution {
    // 定义一个全局 List 用于存储前序遍历的结果
    public List ans = new ArrayList();

    // 前序遍历的主函数,输入根节点,返回遍历结果
    public List preorderTraversal(TreeNode root) {
        // 如果当前节点不为空,继续处理
        if (root != null) {
            // 先访问根节点,将当前节点的值加入结果列表
            ans.add(root.val);
            
            // 如果左子节点不为空,递归遍历左子树
            if (root.left != null) {
                preorderTraversal(root.left);
            }
            
            // 如果右子节点不为空,递归遍历右子树
            if (root.right != null) {
                preorderTraversal(root.right);
            }
        }
        // 返回前序遍历的结果
        return ans;
    }
}

解题思路2

2. 为什么用栈?

  • 前序遍历的特点是“先访问根,再处理子树”,而栈的“后进先出”(LIFO)特性可以帮助我们先处理当前节点,然后再依次处理它的子节点。
  • 具体来说,栈保存待访问的节点,确保右子树在左子树之后处理。

3. 迭代实现的核心逻辑

  • 从根节点开始,将节点压入栈。
  • 每次从栈中弹出一个节点:
    • 访问该节点(加入结果列表)。
    • 将右子节点压入栈(如果存在)。
    • 将左子节点压入栈(如果存在)。
  • 由于栈是 LIFO,左子节点会在右子节点之前弹出并处理,符合前序遍历的“左子树优先于右子树”的顺序。

4. 实现步骤

  1. 初始化:定义结果列表 ans 和栈 stack1,若根节点为空,直接返回空列表。
  2. 压入根节点:将根节点压入栈作为起点。
  3. 循环处理
    • 只要栈不为空,弹出栈顶节点。
    • 将该节点的值加入结果列表。
    • 按右子节点、左子节点的顺序压入栈(因为栈是 LIFO,左子节点后入栈,会先被处理)。

代码2

class Solution {
    // 定义结果列表,用于存储前序遍历的结果
    List ans = new ArrayList<>();
    // 定义栈,用于保存待访问的节点
    Stack stack1 = new Stack<>();

    // 前序遍历的主函数,输入根节点,返回遍历结果
    public List preorderTraversal(TreeNode root) {
        // 如果根节点为空,直接返回空的结果列表
        if (root == null) {
            return ans;
        }

        // 将根节点压入栈,作为遍历的起点
        stack1.push(root);

        // 当栈不为空时,继续处理
        while (!stack1.empty()) {
            // 创建一个临时节点变量(实际上可以直接用 pop() 返回值,这里多余)
            TreeNode node1 = new TreeNode(); // 默认构造函数,这里未定义,可能需要调整
            // 弹出栈顶节点
            node1 = stack1.pop();
            // 将当前节点的值加入结果列表(根节点优先访问)
            ans.add(node1.val);

            // 如果右子节点不为空,压入栈(后处理)
            if (node1.right != null) {
                stack1.push(node1.right);
            }
            // 如果左子节点不为空,压入栈(先处理)
            if (node1.left != null) {
                stack1.push(node1.left);
            }
        }

        // 返回前序遍历的结果
        return ans;
    }
}

解题思路3

1. 前序遍历的定义

前序遍历的访问顺序是:根节点 → 左子树 → 右子树

  • 在迭代实现中,我们需要用栈显式管理访问顺序,确保根节点优先访问。

2. 空指针标记法的思想

  • 这段代码通过在栈中插入 null 作为标记,区分节点的两种状态:
    • 未访问状态:节点刚被压入栈时,表示需要处理它的左右子树。
    • 已访问状态:遇到 null 后,表示该节点的左右子树已安排好,可以访问该节点。
  • 与传统的前序遍历迭代方法不同,这里使用 peek() 检查栈顶元素,并结合 null 标记来控制流程。

3. 实现步骤

  1. 初始化
    • 定义结果列表 ans 和栈 stack1。
    • 如果根节点为空,返回空列表。
    • 将根节点压入栈,初始化 node1 为根节点。
  2. 循环处理
    • 只要栈不为空:
      • 查看栈顶元素(peek()):
        • 如果是普通节点(!= null):
          • 弹出该节点。
          • 按右子节点、左子节点、当前节点的顺序压入栈(因为栈是 LIFO,当前节点后入栈会先处理)。
          • 再压入 null 作为标记。
        • 如果是 null:
          • 弹出 null。
          • 弹出前一个节点(真正的节点),将其值加入结果列表。
  3. 结束
    • 栈为空时,返回结果。

4. 与标准迭代法的区别

  • 标准前序遍历迭代法直接弹出节点并访问,然后压入右子节点和左子节点。
  • 此代码通过 null 标记和多次压入同一节点,模拟了“先安排子树,再访问根”的过程,虽然最终结果正确,但逻辑稍显复杂。

代码3

class Solution {
    // 定义结果列表,用于存储前序遍历的结果
    List ans = new ArrayList<>();
    // 定义栈,用于保存待访问的节点和标记
    Stack stack1 = new Stack<>();

    // 前序遍历的主函数,输入根节点,返回遍历结果
    public List preorderTraversal(TreeNode root) {
        // 如果根节点为空,直接返回空的结果列表
        if (root == null) {
            return ans;
        }

        // 将根节点压入栈,作为遍历的起点
        stack1.push(root);
        // 初始化 node1 为根节点(后续会被栈顶元素覆盖)
        TreeNode node1 = new TreeNode(); // 默认构造函数,未定义,可能需调整
        node1 = root;

        // 当栈不为空时,继续处理
        while (!stack1.empty()) {
            // 查看栈顶元素(不弹出)
            node1 = stack1.peek();

            // 如果栈顶是普通节点(非 null)
            if (node1 != null) {
                // 弹出当前节点
                stack1.pop();
                // 如果右子节点不为空,压入栈(后处理)
                if (node1.right != null) stack1.push(node1.right);
                // 如果左子节点不为空,压入栈(先于右子节点处理)
                if (node1.left != null) stack1.push(node1.left);
                // 将当前节点重新压入栈(等待访问)
                stack1.push(node1);
                // 压入 null 作为标记,表示下次遇到时可以访问 node1
                stack1.push(null);
            } 
            // 如果栈顶是 null,表示前一个节点可以访问
            else {
                // 弹出 null 标记
                stack1.pop();
                // 弹出真正的节点
                node1 = stack1.pop();
                // 将节点值加入结果列表
                ans.add(node1.val);
            }
        }

        // 返回前序遍历的结果
        return ans;
    }
}

94.二叉树的中序遍历

给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。

示例 1:

二叉树的前序、中序和后序遍历(迭代法+递归法)_第3张图片

输入:root = [1,null,2,3]
输出:[1,3,2]

示例 2:

输入:root = []
输出:[]

示例 3:

输入:root = [1]
输出:[1]

提示:

  • 树中节点数目在范围 [0, 100] 内
  • -100 <= Node.val <= 100

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

解题思路1

1. 中序遍历的定义

中序遍历是一种二叉树遍历方式,访问顺序为:左子树 → 根节点 → 右子树

  • 特点是先递归处理左子树,然后访问当前节点,最后递归处理右子树。
  • 这种顺序在二叉搜索树(BST)中会按升序访问节点。

2. 递归的适用性

  • 中序遍历天然适合递归实现,因为它遵循“分治法”思想:对于每个节点,先处理左子树,再处理自己,最后处理右子树。
  • 递归通过调用栈隐式地管理节点的访问顺序。

3. 实现步骤

  1. 定义结果存储:使用一个列表(List)存储遍历结果。
  2. 递归函数设计
    • 如果当前节点不为空:
      • 递归处理左子树(如果存在)。
      • 将当前节点的值加入结果列表(根)。
      • 递归处理右子树(如果存在)。
    • 如果当前节点为空,直接返回(递归终止条件)。
  3. 返回结果:遍历完成后,返回存储结果的列表。

代码1

class Solution {
    // 定义一个全局 List 用于存储中序遍历的结果
    public List ans = new ArrayList();

    // 中序遍历的主函数,输入根节点,返回遍历结果
    public List inorderTraversal(TreeNode root) {
        // 如果当前节点不为空,继续处理
        if (root != null) {
            // 如果左子节点不为空,递归遍历左子树
            if (root.left != null) {
                inorderTraversal(root.left);
            }
            // 访问当前节点,将值加入结果列表(左子树处理完后访问根)
            ans.add(root.val);
            // 如果右子节点不为空,递归遍历右子树
            if (root.right != null) {
                inorderTraversal(root.right);
            }
        }
        // 返回中序遍历的结果
        return ans;
    }
}

解题思路2

1. 中序遍历的定义

中序遍历的访问顺序是:左子树 → 根节点 → 右子树

  • 在递归实现中,先处理左子树,再访问根节点,最后处理右子树。
  • 在迭代实现中,需要用栈显式管理访问顺序,确保左子树优先处理。

2. 为什么用栈?

  • 中序遍历需要先访问最左边的节点,因此需要将路径上的所有节点暂时保存起来。
  • 栈的“后进先出”(LIFO)特性适合保存这些节点,并在左子树处理完毕后依次弹出访问。

3. 实现步骤

  1. 初始化
    • 定义结果列表 ans 和栈 stack1。
    • 如果根节点为空,返回空列表。
    • 用 node1 作为当前节点指针,初始指向根节点。
  2. 循环处理
    • 只要栈不为空或当前节点不为空:
      • 如果当前节点不为空:
        • 将当前节点压入栈。
        • 移动到左子节点。
      • 如果当前节点为空:
        • 从栈中弹出一个节点(此时已到达最左节点或某节点的左子树已处理完)。
        • 访问该节点(加入结果列表)。
        • 移动到右子节点。
  3. 结束
    • 栈为空且当前节点为空时,返回结果。

代码2

class Solution {
    // 定义结果列表,用于存储中序遍历的结果
    List ans = new ArrayList<>();
    // 定义栈,用于保存待访问的节点
    Stack stack1 = new Stack<>();

    // 中序遍历的主函数,输入根节点,返回遍历结果
    public List inorderTraversal(TreeNode root) {
        // 如果根节点为空,直接返回空的结果列表
        if (root == null) {
            return ans;
        }

        // 初始化当前节点指针(注意:new TreeNode() 未定义构造函数,可能需调整)
        TreeNode node1 = new TreeNode(); // 默认构造函数,可能编译错误
        node1 = root; // 直接赋值为根节点

        // 当栈不为空或当前节点不为空时,继续处理
        while (!stack1.empty() || node1 != null) {
            // 如果当前节点不为空,向左子树深入
            if (node1 != null) {
                stack1.push(node1); // 将当前节点压入栈
                node1 = node1.left; // 移动到左子节点
            }
            // 如果当前节点为空,处理栈顶节点
            else {
                node1 = stack1.pop(); // 弹出栈顶节点(左子树已处理完)
                ans.add(node1.val);   // 访问当前节点,加入结果
                node1 = node1.right;  // 移动到右子节点
            }
        }

        // 返回中序遍历的结果
        return ans;
    }
}

解题思路3

1. 中序遍历的定义

中序遍历的访问顺序是:左子树 → 根节点 → 右子树

  • 在迭代实现中,需要确保左子树先处理完,然后访问根节点,最后处理右子树。

2. 空指针标记法的思想

  • 这段代码通过在栈中插入 null 作为标记,区分节点的两种状态:
    • 未处理状态:节点刚被压入栈时(!= null),表示需要安排它的左右子树。
    • 已处理状态:遇到 null 时,表示该节点的左子树已处理完,可以访问该节点。
  • 使用 peek() 检查栈顶元素,结合 null 标记控制流程。

3. 实现步骤

  1. 初始化
    • 定义结果列表 ans 和栈 stack1。
    • 如果根节点为空,返回空列表。
    • 将根节点压入栈作为起点。
  2. 循环处理
    • 只要栈不为空:
      • 查看栈顶元素(peek()):
        • 如果是普通节点(!= null):
          • 弹出该节点。
          • 按右子节点、当前节点、null、左子节点的顺序压入栈。
          • 这样确保左子树先处理(后入栈先出),当前节点在左子树后访问,右子树最后处理。
        • 如果是 null:
          • 弹出 null。
          • 弹出前一个节点(真正的节点),将其值加入结果列表。
  3. 结束
    • 栈为空时,返回结果。

代码3 

class Solution {
    // 定义栈,用于保存待访问的节点和标记
    Stack stack1 = new Stack<>();
    // 定义结果列表,用于存储中序遍历的结果
    List ans = new ArrayList<>();

    // 中序遍历的主函数,输入根节点,返回遍历结果
    public List inorderTraversal(TreeNode root) {
        // 如果根节点为空,直接返回空的结果列表
        if (root == null) {
            return ans;
        }

        // 初始化临时节点变量(注意:new TreeNode() 未定义构造函数,可能需调整)
        TreeNode node1 = new TreeNode(); // 默认构造函数,可能编译错误
        // 将根节点压入栈,作为遍历起点
        stack1.push(root);

        // 当栈不为空时,继续处理
        while (!stack1.empty()) {
            // 查看栈顶元素(不弹出)
            node1 = stack1.peek();

            // 如果栈顶是普通节点(非 null)
            if (node1 != null) {
                stack1.pop(); // 弹出当前节点
                // 如果右子节点不为空,压入栈(最后处理)
                if (node1.right != null) stack1.push(node1.right);
                // 将当前节点重新压入栈(等待左子树处理完后访问)
                stack1.push(node1);
                // 压入 null 作为标记,表示下次遇到时可以访问 node1
                stack1.push(null);
                // 如果左子节点不为空,压入栈(最先处理)
                if (node1.left != null) stack1.push(node1.left);
            }
            // 如果栈顶是 null,表示前一个节点的左子树已处理完
            else {
                stack1.pop(); // 弹出 null 标记
                node1 = stack1.pop(); // 弹出真正的节点
                ans.add(node1.val);   // 将节点值加入结果列表
            }
        }

        // 返回中序遍历的结果
        return ans;
    }
}

145.二叉树的中序遍历

给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 

示例 1:

输入:root = [1,null,2,3]

输出:[3,2,1]

解释:

二叉树的前序、中序和后序遍历(迭代法+递归法)_第4张图片

示例 2:

输入:root = [1,2,3,4,5,null,8,null,null,6,7,9]

输出:[4,6,7,5,2,9,8,3,1]

解释:

二叉树的前序、中序和后序遍历(迭代法+递归法)_第5张图片

示例 3:

输入:root = []

输出:[]

示例 4:

输入:root = [1]

输出:[1]

提示:

  • 树中节点的数目在范围 [0, 100] 内
  • -100 <= Node.val <= 100

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

解题思路1

1. 后序遍历的定义

后序遍历是一种二叉树遍历方式,访问顺序为:左子树 → 右子树 → 根节点

  • 特点是先递归处理左子树,然后处理右子树,最后访问当前节点。
  • 这种顺序常用于需要先处理子节点再处理父节点的场景(如删除树或计算树的高度)。

2. 递归的适用性

  • 后序遍历天然适合递归实现,因为它遵循“分治法”思想:对于每个节点,先处理左右子树,再处理自己。
  • 递归通过调用栈隐式管理节点的访问顺序。

3. 实现步骤

  1. 定义结果存储:使用一个列表(List)存储遍历结果。
  2. 递归函数设计
    • 如果当前节点不为空:
      • 递归处理左子树(如果存在)。
      • 递归处理右子树(如果存在)。
      • 将当前节点的值加入结果列表(根)。
    • 如果当前节点为空,直接返回(递归终止条件)。
  3. 返回结果:遍历完成后,返回存储结果的列表。

代码1

class Solution {
    // 定义一个全局 List 用于存储后序遍历的结果
    List ans = new ArrayList<>();

    // 后序遍历的主函数,输入根节点,返回遍历结果
    public List postorderTraversal(TreeNode root) {
        // 如果当前节点不为空,继续处理
        if (root != null) {
            // 如果左子节点不为空,递归遍历左子树
            if (root.left != null) {
                postorderTraversal(root.left);
            }
            // 如果右子节点不为空,递归遍历右子树
            if (root.right != null) {
                postorderTraversal(root.right);
            }
            // 访问当前节点,将值加入结果列表(左右子树处理完后访问根)
            ans.add(root.val);
        }
        // 返回后序遍历的结果
        return ans;
    }
}

解题思路2

1. 后序遍历的定义

后序遍历的访问顺序是:左子树 → 右子树 → 根节点(LRN)。

  • 直接用栈实现后序遍历较复杂,因为需要在访问根节点前确保左右子树都已处理。

2. 反转前序遍历的思想

  • 前序遍历(NLR: 根-左-右)可以用栈轻松实现。
  • 观察到后序遍历(LRN)和前序遍历的反转(RLN)有相似之处:
    • 前序遍历:NLR。
    • 前序遍历反转:RLN。
    • 后序遍历:LRN。
  • 如果将前序遍历的顺序改为 根-右-左(NRL),然后反转结果(LRN),就得到了后序遍历。
  • 此代码正是利用这一技巧:先按“根-左-右”顺序遍历,再反转列表。

3. 实现步骤

  1. 初始化
    • 定义结果列表 ans 和栈 stack1。
    • 如果根节点为空,返回空列表。
    • 将根节点压入栈。
  2. 前序遍历变种
    • 弹出栈顶节点,访问它(加入 ans)。
    • 按左子节点、右子节点的顺序压入栈(左子节点后入栈,先处理)。
    • 重复直到栈为空。
  3. 反转结果
    • 将 ans 反转,得到后序遍历顺序。
  4. 返回:返回反转后的结果。

4. 为什么有效?

  • 前序遍历(NLR)反转后是 RLN,与 LRN(后序)仅左右顺序相反。
  • 此代码实际执行的是 NLR(根-左-右),反转为 RLN,不是严格的后序遍历(LRN)。若要正确实现后序遍历,应改为“根-右-左”(NRL),再反转。

代码2

class Solution {
    // 定义结果列表,用于存储遍历结果(最终反转为后序)
    List ans = new ArrayList<>();
    // 定义栈,用于保存待访问的节点
    Stack stack1 = new Stack<>();

    // 后序遍历的主函数,输入根节点,返回遍历结果
    public List postorderTraversal(TreeNode root) {
        // 如果根节点为空,直接返回空的结果列表
        if (root == null) {
            return ans;
        }

        // 将根节点压入栈,作为遍历起点
        stack1.push(root);

        // 执行前序遍历变种(NLR: 根-左-右)
        while (!stack1.empty()) {
            // 创建临时节点变量(注意:new TreeNode() 未定义构造函数,可能需调整)
            TreeNode node1 = new TreeNode(); // 默认构造函数,可能编译错误
            node1 = stack1.pop(); // 弹出栈顶节点
            ans.add(node1.val);   // 访问当前节点(根优先)
            // 如果左子节点不为空,压入栈(先处理)
            if (node1.left != null) {
                stack1.push(node1.left);
            }
            // 如果右子节点不为空,压入栈(后处理)
            if (node1.right != null) {
                stack1.push(node1.right);
            }
        }

        // 将结果反转,从 NLR 变为 RLN(需调整为 NRL 再反转为 LRN)
        ans = ans.reversed(); // Java 21+ 的方法,之前版本用 Collections.reverse(ans)
        // 返回后序遍历的结果
        return ans;
    }
}

解题思路3

1. 后序遍历的定义

后序遍历的访问顺序是:左子树 → 右子树 → 根节点(LRN)。

  • 在迭代实现中,需要确保在访问根节点前,左右子树都已处理。

2. 空指针标记法的思想

  • 通过在栈中插入 null 作为标记,区分节点的处理状态:
    • 未处理状态:节点刚入栈(!= null),需要安排左右子树。
    • 已处理状态:遇到 null,表示该节点的子树已处理完,可以访问。
  • 结合结果反转,可能是想通过某种前序遍历变种(NLR 或 NRL)反转为 LRN。

3. 预期实现步骤

  1. 初始化:将根节点压入栈。
  2. 循环处理
    • 如果栈顶是普通节点(!= null):
      • 弹出节点,安排子树和标记。
      • 按某种顺序压入左右子节点和当前节点。
    • 如果栈顶是 null:
      • 弹出 null,访问前一个节点。
  3. 反转:将结果反转,试图得到 LRN。

代码3 

class Solution {
    // 定义结果列表,用于存储遍历结果
    List ans = new ArrayList<>();
    // 定义栈,用于保存待访问的节点和标记
    Stack stack1 = new Stack<>();

    // 后序遍历的主函数,输入根节点,返回遍历结果
    public List postorderTraversal(TreeNode root) {
        // 如果根节点为空,直接返回空的结果列表
        if (root == null) {
            return ans;
        }

        // 将根节点压入栈,作为遍历起点
        stack1.push(root);
        // 初始化临时节点变量(注意:new TreeNode() 未定义构造函数,可能需调整)
        TreeNode node1 = new TreeNode(); // 默认构造函数,可能编译错误
        node1 = root; // 赋值为根节点(此行冗余)

        // 当栈不为空时,继续处理
        while (!stack1.empty()) {
            // 查看栈顶元素(不弹出)
            node1 = stack1.peek();

            // 如果栈顶是普通节点(非 null)
            if (node1 != null) {
                stack1.pop(); // 弹出当前节点
                // 如果左子节点不为空,压入栈(先处理)
                if (node1.left != null) stack1.push(node1.left);
                // 如果右子节点不为空,压入栈(后处理)
                if (node1.right != null) stack1.push(node1.right);
                // 将当前节点重新压入栈(等待子树处理完)
                stack1.push(node1);
                // 压入 null 作为标记,表示下次遇到时可访问 node1
                stack1.push(null);
            }
            // 如果栈顶是 null,表示前一个节点的子树已处理完
            else {
                stack1.pop(); // 弹出 null 标记
                node1 = stack1.pop(); // 弹出真正的节点
                ans.add(node1.val);   // 将节点值加入结果列表
            }
        }

        // 反转结果,试图从某种顺序变为 LRN
        return ans.reversed(); // Java 21+ 方法,之前用 Collections.reverse(ans)
    }
}

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