【LeetCode 热题 100】105. 从前序与中序遍历序列构造二叉树——(解法一)O(n^2)

Problem: 105. 从前序与中序遍历序列构造二叉树

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

文章目录

整体思路

这段代码旨在解决一个经典的二叉树构造问题:从前序遍历和中序遍历序列构造二叉树 (Construct Binary Tree from Preorder and Inorder Traversal)。这是一个典型的分治(Divide and Conquer)算法应用。

算法的核心思想基于前序遍历和中序遍历的根本性质:

  1. 前序遍历 (Preorder)[根节点, [左子树的前序遍历], [右子树的前序遍历]]
    • 第一个元素 永远是当前(子)树的根节点
  2. 中序遍历 (Inorder)[[左子树的中序遍历], 根节点, [右子树的中序遍历]]
    • 根节点会把整个序列分割成两部分:左边是其左子树的所有节点,右边是其右子树的所有节点。

该递归算法的逻辑步骤如下:

  1. 确定根节点 (Divide)

    • 根据前序遍历的性质,preorder 数组的第一个元素 preorder[0] 就是当前树的根节点。
  2. 分割子树 (Divide)

    • inorder 数组中找到这个根节点值的位置。这个位置(我们称之为 i)至关重要。
    • inorder 数组中,索引 i 左边的所有元素,构成了根节点的左子树的中序遍历
    • 索引 i 右边的所有元素,构成了根节点的右子树的中序遍历
    • 左子树的大小 leftSize 就等于 i
  3. 构建子树的遍历序列 (Divide)

    • 根据 leftSize,我们现在可以从 preorder 数组中也分割出左右子树的前序遍历序列。
      • preorder 数组中,从索引 1 开始,长度为 leftSize 的部分,是左子树的前序遍历
      • preorder 数组剩下的部分,是右子树的前序遍历
    • 【效率瓶颈】 :此代码通过 Arrays.copyOfRange 来创建四个全新的、更小的数组,分别代表左右子树的前序和中序遍历。这一步虽然逻辑清晰,但涉及大量的数组复制,是性能不佳的主要原因。
  4. 递归构造 (Conquer)

    • 将分割出的左子树的前序和中序遍历数组,递归地传入 buildTree 函数,构造出左子节点 left
    • 同样地,将右子树的两个数组传入,构造出右子节点 right
    • 递归的基线条件是当传入的数组为空时,返回 null
  5. 合并结果 (Combine)

    • 创建一个新的 TreeNode,其值为 preorder[0],并将其 leftright 指针分别指向上面递归构造出的左右子节点。
    • 返回这个新创建的根节点。

完整代码

java 复制代码
class Solution {
    /**
     * 根据前序遍历和中序遍历的结果,构造二叉树。
     * @param preorder 前序遍历数组
     * @param inorder  中序遍历数组
     * @return 构造出的二叉树的根节点
     */
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        int n = preorder.length;
        // 递归的基线条件:如果遍历序列为空,则子树也为空。
        if (n == 0) {
            return null;
        }
        
        // 步骤 1: 确定根节点。前序遍历的第一个元素就是根。
        // 步骤 2: 在中序遍历中找到根节点的位置,以确定左子树的大小。
        int leftSize = 0;
        for (int i = 0; i < n; i++) {
            if (inorder[i] == preorder[0]) {
                leftSize = i;
                break; // 找到后即可退出循环
            }
        }
        
        // 步骤 3: 【效率瓶颈】分割数组,为左右子树创建新的遍历序列。
        // 这会创建四个新的数组,导致时间和空间开销较大。
        
        // 获取左子树的前序遍历
        int[] pre1 = Arrays.copyOfRange(preorder, 1, 1 + leftSize);
        // 获取右子树的前序遍历
        int[] pre2 = Arrays.copyOfRange(preorder, 1 + leftSize, n);
        // 获取左子树的中序遍历
        int[] in1 = Arrays.copyOfRange(inorder, 0, leftSize);
        // 获取右子树的中序遍历
        int[] in2 = Arrays.copyOfRange(inorder, 1 + leftSize, n);
        
        // 步骤 4: 递归地构建左右子树
        TreeNode left = buildTree(pre1, in1);
        TreeNode right = buildTree(pre2, in2);
        
        // 步骤 5: 创建根节点,并连接其左右子树
        return new TreeNode(preorder[0], left, right);
    }
}

时空复杂度

时间复杂度:O(N^2)

  1. 根节点查找 :在每次 buildTree 调用中(处理一个大小为 k 的子问题),for 循环会在中序遍历数组中进行线性扫描以找到根节点。此操作的时间复杂度为 O(k)。
  2. 数组复制Arrays.copyOfRange 操作需要复制元素。在处理大小为 k 的子问题时,总共复制 k-1 个元素来创建新的子数组。此操作的时间复杂度也是 O(k)。
  3. 递归结构
    • T(N) 为处理 N 个节点的时间复杂度。
    • T(N) = O(N) + T(leftSize) + T(N - 1 - leftSize)。其中 O(N) 是当前层级的查找和复制开销。
    • 在最坏的情况下(例如,树退化成一个链表),leftSize 始终为 0N-1。此时,递归关系变为 T(N) = T(N-1) + O(N)
    • 展开这个递推式:T(N) = O(N) + O(N-1) + O(N-2) + ... + O(1)。这是一个等差数列求和,结果为 O(N^2)

综合分析 :由于在递归的每一步都进行了线性的数组扫描和数组复制,该算法的时间复杂度为 O(N^2)

空间复杂度:O(N^2)

  1. 递归调用栈 :递归的深度等于树的高度 H。在最坏情况下(链状树),H=N,递归栈本身会占用 O(N) 的空间。
  2. 辅助数组 :这是空间复杂度的主要来源。在递归的每一层,我们都创建了新的数组副本。
    • 考虑最坏情况(链状树),在递归深度为 d 时,会创建大小约为 N-d 的数组。
    • 递归栈上会同时存在多层调用,每一层都持有自己创建的数组。例如,第一层持有大小为 N-1 的数组,第二层持有大小为 N-2 的数组,以此类推。
    • 峰值空间占用发生在最深的递归路径上,总空间约为 (N-1) + (N-2) + ... + 1,这也是 O(N^2) 级别。

综合分析 :由于递归调用中创建了大量的数组副本,导致总的空间消耗非常大。因此,在最坏情况下,空间复杂度为 O(N^2)

【LeetCode 热题 100】105. 从前序与中序遍历序列构造二叉树------(解法二)O(n)

参考灵神