Leetcode刷题:剑指offer【面试题07 重建二叉树】

【面试题07】重建二叉树

难度: 中等
限制: 0 <= 节点个数 <= 5000

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

Leetcode题目对应位置: 面试题07:重建二叉树

复习:

二叉树的三种遍历方式:

  • 前序遍历:根 左 右
  • 中序遍历:左 根 右
  • 后序遍历:左 右 根

要恢复一颗二叉树,必须知道它的中序序列(因为中序序列可以知道根节点及其左右子树的位置),再结合前序或后序的任意一个即可,只知道前序和后序序列无法恢复二叉树。

树的遍历方式:

  • 深度优先遍历,前/中/后序遍历属于深度遍历的一种实现
  • 广度优先遍历

遍历二叉树的实现方式:

  • 递归:代码简洁容易理解,但开销可能比较大
  • 非递归(栈)

手动推导:

前序序列:[1, 2, 4, 7, 3, 5, 6, 8]
中序序列:[4, 7, 2, 1, 5, 3, 8, 6]

递归恢复二叉树过程:

1)前序序列第一个节点(1)即根节点,在中序序列找到 1 所在的位置,则 1 前面所有的都为左子树节点(4、7、2),1 后面所有的都为右子树节点(5、3、8、6);
2)左子树的前序序列 [2, 4, 7] 第一个节点(2)即左子树的根节点,在中序序列 [4, 7, 2] 找到 2 所在的位置,则 2 前面所有的都为 2 的左子树节点(4、7),2 后面没有值,说明 2 没有右子树。
3)以此类推,最终就能确定二叉树的结构为 [1,2,3,4,null,5,6,null,7,null,null,8],这是广度优先遍历的结果。

思路 1:递归

重建二叉树就是个递归的过程,只要将递归函数写出来就 ok 了,在主函数里一条 return 直接带走。

递归函数 createCore:

1)传入 当前子树 的先序序列和中序序列的开始下标 preS、inS 和结束下标 preE、inE。比如第一次调用就是整颗树,先序序列的开始、结束下标为 preS = 0, preE = len(preorder) - 1,中序序列一样 inS = 0, inE = len(inorder)
2)此时传入的 preS 就是当前子树的根节点下标,然后在中序序列中查找该根节点的下标 inRoot
3)开始新一轮递归,root.left = createCore(preS + 1, preS + inRoot - inS, inS, inRoot - 1)root.right = createCore(preS + inRoot - inS + 1, preE, inRoot + 1, inE)。实际上 inRoot - inS 就是左子树节点的个数。返回值 root 就是当前递归层级的根节点,它也是上一递归层级根节点的左或右子节点。当 preS > preE 时,说明不再有节点,直接 return。

Python 代码:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        self.preo = preorder
        self.ino = inorder
        if not preorder or not inorder or len(preorder) != len(inorder):
            return -1
        return self.createCore(0, len(preorder)-1, 0, len(inorder)-1)

    def createCore(self, preS, preE, inS, inE):
        if preS > preE:
            return 
        root = TreeNode(self.preo[preS])
        
        # 查找中序遍历中根节点的下标
        i = inS
        while (i <= inE):
            if self.ino[i] == root.val:
                inRoot = i
                break
            else: i += 1
            
        leftNum = inRoot - inS
        #rightNum = inE - inRoot
        root.left = self.createCore(preS+1, preS+leftNum, inS, inRoot-1)
        root.right = self.createCore(preS+leftNum+1, preE, inRoot+1, inE) 
        return root

在这里插入图片描述
但这样时间非常长,主要是因为每次都要在中序序列中查找根节点的位置,所以考虑为中序序列构造一个哈希表,实现 O(1) 的查找效率。

时间复杂度:O(n),最差情况下递归深度为 n,树退化为链表。
空间复杂度:O(n),需要为中序序列建立哈希表。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        self.preo = preorder
        self.ino = inorder
        self.indict = {}
        if not preorder or not inorder or len(preorder) != len(inorder):
            return -1
        # 中序序列哈希表
        for i in range(len(inorder)):
            self.indict[inorder[i]] = i
        return self.createCore(0, len(preorder)-1, 0, len(inorder)-1)

    def createCore(self, preS, preE, inS, inE):
        if preS > preE:
            return 
        root = TreeNode(self.preo[preS])
        
        # 查找中序遍历中根节点的位置
        inRoot = self.indict[root.val]

        leftNum = inRoot - inS
        root.left = self.createCore(preS+1, preS+leftNum, inS, inRoot-1)
        root.right = self.createCore(preS+leftNum+1, preE, inRoot+1, inE) 
        return root

时间花费大大减少啦~
在这里插入图片描述
另外看到其他大佬写的代码,简洁明了,学习一下~

# 参考代码1
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        self.dic, self.po = {}, preorder
        for i in range(len(inorder)):
            self.dic[inorder[i]] = i
        return self.recur(0, 0, len(inorder) - 1)

    def recur(self, pre_root, in_left, in_right):
        if in_left > in_right: return # 终止条件:中序遍历为空
        root = TreeNode(self.po[pre_root]) # 建立当前子树的根节点
        i = self.dic[self.po[pre_root]]    # 搜索根节点在中序遍历中的索引,从而可对根节点、左子树、右子树完成划分。
        root.left = self.recur(pre_root + 1, in_left, i - 1) # 开启左子树的下层递归
        root.right = self.recur(i - in_left + pre_root + 1, i + 1, in_right) # 开启右子树的下层递归
        return root # 返回根节点,作为上层递归的左(右)子节点

代码来源:mian-shi-ti-07-zhong-jian-er-cha-shu-di-gui-fa-qin

思路 2:迭代/非递归

迭代不是很好想,以防万一会考到非递归的方案,还是也做一下吧。

给一个例子好解释:

          3
         /  \
        9   20
       /    / \
      8    15  7
     / \
    5  10
   /
 4
 
前序遍历:[3,9,8,5,4,10,20,15,7]
中序遍历:[4,5,8,10,9,3,15,20,7]

观察前序和中序序列,有几个基本事实:

  • 前序序列的第 1 个节点是整棵树的根节点
  • 若存在左子树,则中序序列的第 1 个节点是整棵树的最左节点
  • 若存在左子树,则前序序列从第 1 个节点到中序序列的第 1 个节点上所有的值都属于左子树上的左子节点,且每一个节点都是下一个节点的父节点(例如:前序序列第 1 个节点为 3,中序序列第 1 个节点为 4,则前序序列从 3 到 4:[3,9,8,5,4] 这一段的节点都属于左子树上的左子节点,且每一个是后一个的父节点)
  • 以中序序列第 1 个节点值为分界,前序序列在这个值之前(包括该值)都属于左子树的左子节点,在这个值之后的值可能是左子树的右子节点,也可能是右子树
  • 仍然观察前序序列的这一段 [3,9,8,5,4],如果将它反转过来 [4,5,8,9,3] 并和中序序列 4 到 3 这一段儿 [4,5,8,10,9,3] 进行比较,会发现差别仅在 10 上,由于 10 在前序序列 4 之后 3 之前,说明 10 是左子树的右子节点
  • 由于中序遍历遵循 “左-根-右” 的原则,则 10 就是其前一个节点的右子节点,即 8 的右子节点

那么在迭代重建二叉树时,由于前序序列的节点要逆序与中序序列进行比较,所以用栈来保存前序序列中遍历过的节点。

1)初始时,指针指向中序序列的第 1 个元素,用前序序列的第 1 个元素创建根节点,并入栈;
2)遍历前序序列,判断当前元素的上一个元素(即栈顶元素)是否等于中序序列当前指针所指向的元素;

  • 2.1 若当前元素的上一个元素(即栈顶元素)不等于 中序序列当前指针所指向的元素,则将当前元素入栈,并令 上一个元素 -> left = 当前元素,即当前元素是上一个元素的左子节点;
  • 2.2 若当前元素的上一个元素(即栈顶元素)等于 中序序列当前指针所指向的元素,则从栈中弹出栈顶元素,同时中序序列指针位置 + 1,继续比较栈顶元素和中序序列当前所指向元素是否相等,若相等则重复:弹出-比较,直至栈空或不相等,此时令当前中序序列元素为最后一次相等时元素的右子节点

3)循环以上过程,遍历结束时返回根节点。

按照上面的思路写一个 Python 版本的代码:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        pre_len = len(preorder)
        in_len = len(preorder)
        if not preorder or not inorder or pre_len != in_len:
            return
        stack = []
        root = TreeNode(preorder[0])
        stack.append(root)
        inIndex = 0
        for i in range(1, pre_len):
            preVal = TreeNode(preorder[i])
            if stack[-1].val != inorder[inIndex]:
                stack[-1].left = preVal
                stack.append(preVal)
            else:
                while stack and stack[-1].val == inorder[inIndex]:
                    node = stack.pop()
                    inIndex += 1
                node.right = preVal
                stack.append(node.right)
        return root

在这里插入图片描述


参考资料:

[1] 剑指 offer 第二版
[2] 参考题解:重建二叉树 作者:Krahets

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不吃饭就会放大招

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值