算法题 从中序与后序遍历序列构造二叉树

LeetCode 106. 从中序与后序遍历序列构造二叉树

问题描述

给定两个整数数组 inorderpostorder,其中 inorder 是二叉树的中序遍历postorder 是同一棵树的后序遍历,请构造并返回这棵二叉树。

假设树中没有重复的元素

示例

输入: inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出: [3,9,20,null,null,15,7]

解释:
中序遍历: 9 → 3 → 15 → 20 → 7  (左 → 根 → 右)
后序遍历: 9 → 15 → 7 → 20 → 3  (左 → 右 → 根)

算法思路

核心

  • 后序遍历特点:最后一个元素总是当前子树的根节点
  • 中序遍历特点:根节点将数组分为左子树和右子树两部分

递归构造过程

  1. 从后序遍历的末尾获取根节点值
  2. 在中序遍历中找到根节点位置,分割左右子树
  3. 递归构造右子树
  4. 递归构造左子树
  5. 返回构造的根节点

关键

  • 处理顺序
    • 必须先构造右子树,再构造左子树
    • 因为后序遍历顺序是 [左子树, 右子树, 根]
    • 从后往前遍历时,先遇到右子树的根,再遇到左子树的根
  • 索引管理:使用全局索引或引用跟踪后序遍历的当前位置
  • 哈希优化:用HashMap存储中序遍历的值到索引的映射,避免每次线性搜索

代码实现

方法一:递归 + HashMap优化

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    private int postIndex; // 后序遍历的当前索引(从后往前)
    private Map<Integer, Integer> inorderMap; // 中序遍历值到索引的映射
    
    /**
     * 从中序和后序遍历序列构造二叉树
     * 
     * @param inorder 中序遍历序列
     * @param postorder 后序遍历序列
     * @return 构造的二叉树根节点
     */
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        // 初始化后序遍历索引(指向最后一个元素)
        postIndex = postorder.length - 1;
        
        // 构建中序遍历的值到索引的映射,优化查找效率
        inorderMap = new HashMap<>();
        for (int i = 0; i < inorder.length; i++) {
            inorderMap.put(inorder[i], i);
        }
        
        // 递归构造二叉树
        return buildTreeHelper(inorder, postorder, 0, inorder.length - 1);
    }
    
    /**
     * 递归构造指定范围内的子树
     * 
     * @param inorder 中序遍历数组
     * @param postorder 后序遍历数组
     * @param inStart 中序遍历的起始索引
     * @param inEnd 中序遍历的结束索引
     * @return 构造的子树根节点
     */
    private TreeNode buildTreeHelper(int[] inorder, int[] postorder, 
                                   int inStart, int inEnd) {
        // 递归终止条件:无效区间
        if (inStart > inEnd) {
            return null;
        }
        
        // 获取当前子树的根节点值(后序遍历的当前元素)
        int rootVal = postorder[postIndex];
        postIndex--; // 移动到下一个根节点
        
        // 创建根节点
        TreeNode root = new TreeNode(rootVal);
        
        // 在中序遍历中找到根节点的位置
        int inRootIndex = inorderMap.get(rootVal);
        
        // 重要:先构造右子树,再构造左子树!
        // 因为后序遍历顺序是 [左, 右, 根],从后往前遍历时先遇到右子树的根
        root.right = buildTreeHelper(inorder, postorder, 
                                   inRootIndex + 1, inEnd);
        root.left = buildTreeHelper(inorder, postorder, 
                                  inStart, inRootIndex - 1);
        
        return root;
    }
}

方法二:递归 + 线性搜索

class Solution {
    private int postIndex;
    
    /**
     * 从中序和后序遍历序列构造二叉树 - 无HashMap优化
     * 
     * @param inorder 中序遍历序列
     * @param postorder 后序遍历序列
     * @return 构造的二叉树根节点
     */
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        postIndex = postorder.length - 1;
        return buildTreeHelper(inorder, postorder, 0, inorder.length - 1);
    }
    
    private TreeNode buildTreeHelper(int[] inorder, int[] postorder, 
                                   int inStart, int inEnd) {
        if (inStart > inEnd) {
            return null;
        }
        
        int rootVal = postorder[postIndex--];
        TreeNode root = new TreeNode(rootVal);
        
        // 线性搜索根节点在中序遍历中的位置
        int inRootIndex = 0;
        for (int i = inStart; i <= inEnd; i++) {
            if (inorder[i] == rootVal) {
                inRootIndex = i;
                break;
            }
        }
        
        // 先右后左的构造顺序
        root.right = buildTreeHelper(inorder, postorder, inRootIndex + 1, inEnd);
        root.left = buildTreeHelper(inorder, postorder, inStart, inRootIndex - 1);
        
        return root;
    }
}

方法三:迭代法(使用栈)

import java.util.Stack;

class Solution {
    /**
     * 从中序和后序遍历序列构造二叉树 - 迭代实现
     * 
     * @param inorder 中序遍历序列
     * @param postorder 后序遍历序列
     * @return 构造的二叉树根节点
     */
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        if (postorder.length == 0) return null;
        
        // 创建根节点
        TreeNode root = new TreeNode(postorder[postorder.length - 1]);
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        
        int inIndex = inorder.length - 1; // 中序遍历从后往前
        
        // 后序遍历从倒数第二个元素开始(最后一个已作为根节点)
        for (int i = postorder.length - 2; i >= 0; i--) {
            TreeNode node = new TreeNode(postorder[i]);
            
            // 如果栈顶节点不等于当前中序遍历的节点
            // 说明当前节点是栈顶节点的右子节点
            if (stack.peek().val != inorder[inIndex]) {
                stack.peek().right = node;
            } else {
                // 找到需要挂载左子节点的位置
                TreeNode parent = null;
                while (!stack.isEmpty() && stack.peek().val == inorder[inIndex]) {
                    parent = stack.pop();
                    inIndex--;
                }
                parent.left = node;
            }
            stack.push(node);
        }
        
        return root;
    }
}

算法分析

  • 时间复杂度
    • 方法一:O(n) - 每个节点访问一次,HashMap查找O(1)
    • 方法二:O(n²) - 每次线性搜索根节点位置
    • 方法三:O(n) - 每个节点入栈出栈各一次
  • 空间复杂度
    • 方法一:O(n) - HashMap存储 + 递归栈空间
    • 方法二:O(n) - 递归栈空间
    • 方法三:O(n) - 显式栈空间
  • 方法对比
    • 方法一:最优解,时间空间效率都很好
    • 方法二:易于理解,但效率较低
    • 方法三:避免递归,但逻辑复杂

算法过程

输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]

构造过程

  1. 初始状态postIndex = 4, postorder[4] = 3

    • 根节点 = 3
    • 中序中3的位置 = 1
    • 左子树范围:[0,0] → [9]
    • 右子树范围:[2,4] → [15,20,7]
  2. 构造右子树postIndex = 3):

    • 根节点 = 20(postorder[3]
    • 中序中20的位置 = 3
    • 左子树范围:[2,2] → [15]
    • 右子树范围:[4,4] → [7]
  3. 构造右子树的右子树postIndex = 2):

    • 根节点 = 7(postorder[2]
    • 中序中7的位置 = 4
    • 左右子树都为空
  4. 构造右子树的左子树postIndex = 1):

    • 根节点 = 15(postorder[1]
    • 中序中15的位置 = 2
    • 左右子树都为空
  5. 构造左子树postIndex = 0):

    • 根节点 = 9(postorder[0]
    • 中序中9的位置 = 0
    • 左右子树都为空

最终树结构

    3
   / \
  9  20
    /  \
   15   7

测试用例

public class BuildTreeTest {
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode() {}
        TreeNode(int val) { this.val = val; }
        TreeNode(int val, TreeNode left, TreeNode right) {
            this.val = val;
            this.left = left;
            this.right = right;
        }
    }
    
    // 中序遍历验证
    static List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        inorderHelper(root, result);
        return result;
    }
    
    static void inorderHelper(TreeNode node, List<Integer> result) {
        if (node == null) return;
        inorderHelper(node.left, result);
        result.add(node.val);
        inorderHelper(node.right, result);
    }
    
    // 后序遍历验证
    static List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        postorderHelper(root, result);
        return result;
    }
    
    static void postorderHelper(TreeNode node, List<Integer> result) {
        if (node == null) return;
        postorderHelper(node.left, result);
        postorderHelper(node.right, result);
        result.add(node.val);
    }
    
    public static void main(String[] args) {
        Solution solution = new Solution();
        
        // 测试用例1:标准示例
        int[] inorder1 = {9,3,15,20,7};
        int[] postorder1 = {9,15,7,20,3};
        TreeNode root1 = solution.buildTree(inorder1, postorder1);
        System.out.println("Test 1 inorder:  " + inorderTraversal(root1));  // [9,3,15,20,7]
        System.out.println("Test 1 postorder: " + postorderTraversal(root1)); // [9,15,7,20,3]
        
        // 测试用例2:只有左子树
        int[] inorder2 = {3,2,1};
        int[] postorder2 = {3,2,1};
        TreeNode root2 = solution.buildTree(inorder2, postorder2);
        System.out.println("Test 2 inorder:  " + inorderTraversal(root2));  // [3,2,1]
        System.out.println("Test 2 postorder: " + postorderTraversal(root2)); // [3,2,1]
        
        // 测试用例3:只有右子树
        int[] inorder3 = {1,2,3};
        int[] postorder3 = {3,2,1};
        TreeNode root3 = solution.buildTree(inorder3, postorder3);
        System.out.println("Test 3 inorder:  " + inorderTraversal(root3));  // [1,2,3]
        System.out.println("Test 3 postorder: " + postorderTraversal(root3)); // [3,2,1]
        
        // 测试用例4:单节点
        int[] inorder4 = {1};
        int[] postorder4 = {1};
        TreeNode root4 = solution.buildTree(inorder4, postorder4);
        System.out.println("Test 4 inorder:  " + inorderTraversal(root4));  // [1]
        System.out.println("Test 4 postorder: " + postorderTraversal(root4)); // [1]
        
        // 测试用例5:空树
        TreeNode root5 = solution.buildTree(new int[]{}, new int[]{});
        System.out.println("Test 5: " + (root5 == null ? "null" : "not null")); // null
        
        // 测试用例6:完全二叉树
        int[] inorder6 = {4,2,5,1,6,3,7};
        int[] postorder6 = {4,5,2,6,7,3,1};
        TreeNode root6 = solution.buildTree(inorder6, postorder6);
        System.out.println("Test 6 inorder:  " + inorderTraversal(root6));  // [4,2,5,1,6,3,7]
        System.out.println("Test 6 postorder: " + postorderTraversal(root6)); // [4,5,2,6,7,3,1]
        
        // 测试用例7:复杂树
        int[] inorder7 = {1,2,3,4,5,6,7,8,9};
        int[] postorder7 = {1,3,2,5,7,6,9,8,4};
        TreeNode root7 = solution.buildTree(inorder7, postorder7);
        System.out.println("Test 7 inorder:  " + inorderTraversal(root7));  // [1,2,3,4,5,6,7,8,9]
        System.out.println("Test 7 postorder: " + postorderTraversal(root7)); // [1,3,2,5,7,6,9,8,4]
    }
}

关键点

  1. 构造顺序

    • 必须先构造右子树,再构造左子树
    • 因为后序遍历从后往前时,右子树的根先出现
  2. HashMap优化

    • 避免每次在中序数组中线性搜索根节点位置
    • 将时间复杂度从O(n²)优化到O(n)
  3. 索引管理

    • postIndex 全局变量跟踪后序遍历位置
    • 中序遍历使用起始结束索引界定子树范围
  4. 边界条件

    • 空数组返回null
    • 单节点树正确处理
    • 递归终止条件:inStart > inEnd

常见问题

  1. 为什么必须先构造右子树?

    • 后序遍历顺序:[左子树序列, 右子树序列, 根]
    • 从后往前遍历时,顺序变为:根, 右子树根, 左子树根
    • 所以必须先处理右子树,才能正确获取左子树的根
  2. 如果没有HashMap优化会怎样?

    • 每次都要在中序数组中线性搜索根节点位置
    • 最坏情况下(链表状树)时间复杂度为O(n²)
  3. 能否从前序和中序构造?

    • 可以!LeetCode 105题就是这个问题
    • 区别在于:前序遍历从前往后,先构造左子树再构造右子树
  4. 为什么不能仅用后序遍历构造?

    • 后序遍历无法确定左右子树的边界
    • 需要中序遍历来分割左右子树
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值