【树】力扣105:从前序与中序遍历序列构造二叉树

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

示例:

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

  • 先序遍历的顺序:[ 根结点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]

    • 特点:根结点始终出现在数组的第一位,根据前序数组可以确定 root
  • 中序遍历的顺序:[ [左子树的中序遍历结果], 根结点, [右子树的中序遍历结果] ]

    • 特点:根结点 root 出现在数组的中间位置,根据 root 可以划分左右子树

递归

总体思想:先序找根,划分左右,再递归构造左右子树

image
前序数组的 左子树部分 + 根结点 是 [1,2,4,5],中序数组的 左子树部分+ 根结点 是 [4,2,5,1]。这两者的数组长度是一样的

因此,可以根据中序数组的中间位置 1 的下标 idx 来确定前序数组的左右部分。由于前序数组的第一个元素是根结点,所以前序数组的左边部分是:[1: idx + 1],右边部分是 [idx + 1:]。中序数组的左边部分是[: idx],右边部分是[idx + 1:]。

要注意这个范围是否需要加一或减一。因为 Arrays.copyOfRange 的范围是[from, to),不包含 idx + 1。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        if not preorder or not inorder: # 递归终止条件。and 和 or 都可以,也可以只写一个
            return None

        rootVal = preorder[0] # 根据前序遍历数组确定根结点
        root = TreeNode(rootVal)
        i = inorder.index(rootVal) # 根结点值在中序遍历数组中的下标
        # 构建左子树:左子树的前序数组是 pre 的左边部分,中序数组是 in 的左边部分,递归到 pre 和 in 中
        root.left = self.buildTree(preorder[1: i + 1], inorder[: i]) # 下标包前不包后,比较两个数组的长度确定是否写正确了
        # 构建右子树:右子树的前序数组是 pre 的右边部分,中序数组是 in 的右边部分,递归到 pre 和 in 中
        root.right = self.buildTree(preorder[i + 1:], inorder[i + 1:])
        return root

时间复杂度:O(n),其中 n 是树中的结点个数。

空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h < n,所以总空间复杂度为 O(n)。

迭代

实际运行时间和空间少很多。

例子:

        3
       / \
      9  20
     /  /  \
    8  15   7
   / \
  5  10
 /
4

前序遍历和中序遍历分别为 preorder = [3, 9, 8, 5, 4, 10, 20, 15, 7],inorder = [4, 5, 8, 10, 9, 3, 15, 20, 7]。

用一个栈 stack 来维护「当前结点的所有还没有考虑过右孩子的祖先结点」,栈顶就是当前结点。也就是说,只有在栈中的结点才可能连接一个新的右孩子。同时,用一个指针 idx 指向中序遍历的某个位置,初始值为 0。idx 对应的结点是「当前节点不断往左走达到的最终结点」,这也是符合中序遍历的。

  • 首先,将根结点 3 入栈,再初始化 idx 所指向的结点为 4,随后对于前序遍历中的每个结点,依次判断它是栈顶结点的左孩子,还是栈中某个结点的右孩子。

  • 遍历 9。9 一定是栈顶结点 3 的左孩子。使用反证法,假设 9 是 3 的右孩子,那么 3 没有左孩子,idx 应该恰好指向 3,但实际上为 4,因此产生了矛盾。所以将 9 作为 3 的左孩子,并将 9 入栈。

    • stack = [3, 9]
    • idx -> inorder[0] = 4
  • 遍历 8、5 和 4。同理可得它们都是上一个结点(栈顶结点)的左孩子,所以它们会依次入栈。

    • stack = [3, 9, 8, 5, 4]
    • idx -> inorder[0] = 4
  • 遍历 10,这时情况就不一样了:idx 恰好指向当前的栈顶结点 4,也就是说 4 没有左孩子,那么 10 必须为栈中某个结点的右孩子。

    那么如何找到这个结点呢?栈中的结点的顺序和它们在前序遍历中出现的顺序是一致的,而且每一个结点的右孩子都还没有被遍历过,那么这些结点的顺序和它们在中序遍历中出现的顺序一定是相反的

    这是因为栈中的任意两个相邻的结点,前者都是后者的某个祖先。栈中的任意一个结点的右孩子还没有被遍历过,说明后者一定是前者左孩子的子树中的结点,那么后者就先于前者出现在中序遍历中。

    因此可以把 idx 不断向右移动,并与栈顶结点进行比较。如果 idx 对应的元素恰好等于栈顶结点,说明在中序遍历中找到了栈顶结点,所以将 idx 增加 1 并弹出栈顶结点,直到 idx 对应的元素不等于栈顶结点。按照这样的过程,弹出的最后一个结点 x 就是 10 的双亲结点,这是因为 10 出现在了 xx 在栈中的下一个结点的中序遍历之间,因此 10 就是 x 的右孩子。

    所以现在依次从栈顶弹出元素,同时 idx 向右移动,直到 栈顶结点 不等于 idx 指向的元素。这样就从栈顶弹出 4、5 和 8,并且将 idx 向右移动三次。将 10 作为最后弹出的结点 8 的右孩子,并将 10 入栈。

    • stack = [3, 9, 10]
    • idx -> inorder[3] = 10
  • 遍历 20。同理,idx 恰好指向当前栈顶结点 10,那么依次从栈顶弹出 10、9 和 3,并将 idx 向右移动三次。将 20 作为最后弹出的结点 3 的右孩子,并将 20 入栈。

    • stack = [20]
    • idx -> inorder[6] = 15
  • 遍历 15,将 15 作为栈顶结点 20 的左孩子,并将 15 入栈。

    • stack = [20, 15]
    • idx -> inorder[6] = 15
  • 遍历 7。idx 恰好指向当前栈顶结点 15,那么依次从栈顶弹出 15 和 20,并且将 idx 向右移动两次。将 7 作为最后弹出的结点 20 的右孩子,并将 7 入栈。

    • stack = [7]
    • idx -> inorder[8] = 7

此时遍历结束,构造出了正确的二叉树。

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        if not preorder: # 空树
            return None

        root = TreeNode(preorder[0]) # 前序遍历的第一个结点 preorder[0] 为 根结点 root
        stack = [root] # 将根结点入栈
        idx = 0 # 初始化扫描中序数组的指针
        for i in range(1, len(preorder)): # 注意范围,从 1 开始
            preorderVal = preorder[i]
            node = stack[-1]
            if node.val != inorder[idx]: # 若 栈顶元素的值 与 中序数组指针所指的值 不相同,表明当前结点存在左子树,一直遍历左结点
                node.left = TreeNode(preorderVal)
                stack.append(node.left)
            else: # 若 栈顶元素的值 与 中序数组指针所指的值 相同,表明当前结点不存在子树,即为树的最左下角的结点,开始操作
                while stack and stack[-1].val == inorder[idx]: # 当 栈非空 且 栈顶元素的值 与 中序数组指针所指的值 相同,弹出栈中所有与当前指针所指元素值相同的结点
                    node = stack.pop()
                    idx += 1 # 弹出后,中序数组指针向右移动一个
                node.right = TreeNode(preorderVal) # while循环结束后,当前 node 所指向的结点就是需要重建右子树的结点
                stack.append(node.right)
        return root

时间复杂度:O(n),其中 n 是树中的节点个数。

空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,我们还需要使用 O(h)(其中 h 是树的高度)的空间存储栈。这里 h < n,所以(在最坏情况下)总空间复杂度为 O(n)。

posted @   Vonos  阅读(245)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
点击右上角即可分享
微信分享提示