全栈开发知识库

72.从前序与中序遍历序列构造二叉树

题目

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

Image

示例 1:

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

示例 2:

  • 输入: preorder = [-1], inorder = [-1]
  • 输出: [-1]

提示:

  • 1 <= preorder.length <= 3000
  • inorder.length == preorder.length
  • -3000 <= preorder[i], inorder[i] <= 3000
  • preorder 和 inorder 均 无重复 元素
  • inorder 均出现在 preorder
  • preorder 保证 为二叉树的前序遍历序列
  • inorder 保证 为二叉树的中序遍历序列

思路

前序遍历: 访问顺序是根 --> 左子树 --> 右子树
中序遍历: 访问顺序是左子树 --> 根 --> 右子树

规律一: 前序遍历的第一个元素,一定是整棵树的根

因为前序是 “根 --> 左 --> 右”,开头第一个是根。
例子里preroder=[3,....],所以根就是3.

规律二: 在中序遍历里找到根,那根的左边全是左子树,右边全是右子树

因为中序是 “左 --> 根 --> 右”,根把中序序列天然切成两半,根的左边都属于左子树,根的右边都属于右子树。

例子里inorder = [9,3,15,20,7],根3在中间。

所以我们一下就知道了: 根的左子树有一个节点(就是9),根的右子树有3个节点(15、20、7)

有了这两条规律,重建过程就是一个天然的递归:

  1. 拿前序的第一个元素当根
  2. 在中序里定位到这个根,切成左右两部分,从而知道左、右子树各有多少个节点
  3. 根据左子树的节点数量,把前序剩下的部分也切成两段:

    • 第一段是左子树的前序
    • 后面是右子树的前序 前序数组本身分不出左右子树的界线,但"左子树有几个节点"这个数量是从中序数组算出来的。知道了这个数量(比如 1 个),就在前序里"根后面数出这么多个"当左子树,剩下的当右子树。 4. 递归的用左子树的前序 + 左子树的中序构建左子树,用右子树的前序 + 右子树的中序构建右子树
  4. 把构建好的左右子树挂到跟上,返回根。


class Solution {
    fun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {
        // base case:没有节点可构建,返回 null
        if (preorder.isEmpty()) {
            return null
        }

        // 1. 前序第一个元素是根
        val rootVal = preorder[0]
        val root = TreeNode(rootVal)

        // 2. 在中序里找到根的位置,它左边是左子树,右边是右子树
        val rootIndexInInorder = inorder.indexOf(rootVal)

        // 左子树的节点个数 = 中序里根左边元素的数量
        val leftSize = rootIndexInInorder

        // 3. 切分中序:根左边是左子树中序,根右边是右子树中序
        val leftInorder = inorder.copyOfRange(0, rootIndexInInorder)
        val rightInorder = inorder.copyOfRange(rootIndexInInorder + 1, inorder.size)

        // 4. 切分前序:根之后的 leftSize 个是左子树前序,再往后是右子树前序
        val leftPreorder = preorder.copyOfRange(1, 1 + leftSize)
        val rightPreorder = preorder.copyOfRange(1 + leftSize, preorder.size)

        // 5. 递归构建左右子树,挂到根上
        root.left = buildTree(leftPreorder, leftInorder)
        root.right = buildTree(rightPreorder, rightInorder)

        return root
    }
}

进阶: 索引 + 哈希表 面试推荐

优点:

  1. 不复制数组,改用下标范围(起止索引)来表示当前处理的是哪一段

这是改进版最核心、也最需要转变思路的地方。

直观版是真的把数组切成小块(物理上复制出新数组)。改进版换个思路:我不复制数组,始终用原来那两个大数组,只是用几个"下标"来标记'我现在关心的是哪一段'。

打个比方:

直观版:像用剪刀把一长条纸真的剪成好几小段,每段单独拿在手里。 改进版:纸不剪,就用两根手指按住"从第几个到第几个",告诉别人"我现在只看这一段"。手指一移,就表示换了一段,纸本身从头到尾没动过。 所以改进版的递归函数,参数不再是"小数组",而是四个下标:前序的起止 [preStart, preEnd]、中序的起止 [inStart, inEnd],用它们圈定"当前正在处理的是哪一段"。

  1. 用哈希表预存中序里每个值的位置,把indexOf的O(n)查找变成O(1)

既然每次都要在中序里找某个值的位置,那我一开始就把"每个值 → 它在中序的下标"全部记到一张哈希表里。以后要找根的位置,直接查表,O(1) 瞬间拿到,不用再一个个比对。

// 预处理:中序里每个值对应的下标,存进哈希表 val inorderIndexMap = HashMap() for (i in inorder.indices) { inorderIndexMap[inorder[i]] = i } 举例:inorder = [9, 3, 15, 20, 7],建好的哈希表是:

{ 9→0, 3→1, 15→2, 20→3, 7→4 } 以后想知道根 20 在中序哪个位置,直接 inorderIndexMap[20] 就得到 3,不用遍历。

class Solution {
    // 哈希表:值 → 它在中序数组里的下标,O(1) 定位根的位置
    private lateinit var inorderIndexMap: HashMap<Int, Int>
    private lateinit var preorder: IntArray

    fun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {
        this.preorder = preorder
        // 预处理:记录中序里每个值的下标
        inorderIndexMap = HashMap()
        for (i in inorder.indices) {
            inorderIndexMap[inorder[i]] = i
        }
        // 用下标范围递归构建整棵树
        return build(0, preorder.size - 1, 0, inorder.size - 1)
    }

    /**
     * 用前序区间 [preStart, preEnd] 和中序区间 [inStart, inEnd] 构建子树
     */
    private fun build(preStart: Int, preEnd: Int, inStart: Int, inEnd: Int): TreeNode? {
        // base case:区间无效,说明没有节点
        if (preStart > preEnd) {
            return null
        }

        // 1. 前序区间的第一个元素是根
        val rootVal = preorder[preStart]
        val root = TreeNode(rootVal)

        // 2. 在中序里 O(1) 定位根,算出左子树节点数
        val rootIndex = inorderIndexMap[rootVal]!!
        val leftSize = rootIndex - inStart

        // 3. 递归构建左子树:
        //    前序区间:根后面的 leftSize 个 → [preStart+1, preStart+leftSize]
        //    中序区间:根左边 → [inStart, rootIndex-1]
        root.left = build(preStart + 1, preStart + leftSize, inStart, rootIndex - 1)

        // 4. 递归构建右子树:
        //    前序区间:剩下的 → [preStart+leftSize+1, preEnd]
        //    中序区间:根右边 → [rootIndex+1, inEnd]
        root.right = build(preStart + leftSize + 1, preEnd, rootIndex + 1, inEnd)

        return root
    }
}

复杂度

版本一(切分子数组):

  • 时间 O(n²):每层递归有 indexOf(O(n))和数组复制(O(n)),最坏(退化成链)总共 O(n²)。
  • 空间 O(n²):不断复制子数组。

版本二(索引 + 哈希表):

  • 时间 O(n):哈希表预处理 O(n),之后每个节点只处理一次、O(1) 定位根,总共 O(n)。
  • 空间 O(n):哈希表 O(n) + 递归栈 O(h)。面试首选这个版本。


  • 邮箱 :charon.chui@gmail.com
  • Good Luck!
本页由 Markdown 自动渲染 · 共 628 篇笔记