「力扣」105+106. 由前序/后序和中序遍历序列构造二叉树

(一)解题思路

两道题的解题思路相同,详细图解去看LeetCode题解区视频,总结为如下图示与步骤划分:
在这里插入图片描述
在这里插入图片描述

(二)基本代码

基本代码采用的都是:在原数组基础上分割后以复制方式生成的新数组,将新数组变量作为递归函数的形参。

不断递归复制生成切分新数组,把新数组作为形参的好处是程序简单易懂,逻辑清晰,缺点在于复制新变量的操作导致了额外的时间和空间复杂度,在后面(四)中有总结

下面是基本代码:

前序+中序

class Solution(object):
    def buildTree(self, preorder, inorder):
        """
        :type preorder: List[int]
        :type inorder: List[int]
        :rtype: TreeNode
        """
        def create(preo, ino):
            if len(preo) == 0:			# 递归终止条件:前序或中序遍历子数组长度==0
                return
            root = TreeNode(preo[0])	# 前序遍历的第1个元素即为根节点
            mid = ino.index(preo[0])	# 用index方法查找元素而非哈希表
            root.left = create(preo[1: mid + 1], ino[0: mid])	# 区间划分
            root.right = create(preo[mid + 1:], ino[mid+1:])	# 区间划分
            return root
        return create(preorder, inorder)

后序+中序

class Solution(object):
    def buildTree(self, inorder, postorder):
        """
        :type inorder: List[int]
        :type postorder: List[int]
        :rtype: TreeNode
        """
        def create(ino, posto):
            if len(ino) == 0:
                return
            root = TreeNode(posto[-1])
            mid = ino.index(posto[-1])
            root.left = create(ino[0:mid], posto[0:mid])
            root.right = create(ino[mid+1:], posto[mid:-1])
            return root
        
        return create(inorder, postorder)

对比

两题的主要代码区别在于:

  1. 生成根节点的节点不同——105:preo[0]; 106:posto[-1]
  2. 前序与后序的子区间划分位置不同——如图

(三)优化代码

官方解答的代码用了很多索引号index来作为形参,而非传入整个复制的数组,这样做的优缺点:

  • 缺点:代码不清晰不易懂,index索引看上去不直接
  • 优点:大幅降低了时间和空间复杂度

详细在(四)中论述,代码如下:

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        def myBuildTree(preorder_left: int, preorder_right: int, inorder_left: int, inorder_right: int):
            if preorder_left > preorder_right:
                return None
            
            # 前序遍历中的第一个节点就是根节点
            preorder_root = preorder_left
            # 在中序遍历中定位根节点
            inorder_root = index[preorder[preorder_root]]
            
            # 先把根节点建立出来
            root = TreeNode(preorder[preorder_root])
            # 得到左子树中的节点数目
            size_left_subtree = inorder_root - inorder_left
            # 递归地构造左子树,并连接到根节点
            # 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
            root.left = myBuildTree(preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1)
            # 递归地构造右子树,并连接到根节点
            # 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
            root.right = myBuildTree(preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right)
            return root
        
        n = len(preorder)
        # 构造哈希映射,帮助我们快速定位根节点
        index = {element: i for i, element in enumerate(inorder)}
        return myBuildTree(0, n - 1, 0, n - 1)
来源:力扣(LeetCode)

(四)降低复杂度的代码优化方法总结

在看罢了105题的官方题解,和下面关于 “为什么形参要搞成这么多不直观的index” 的问题讨论后,发现了官方解答的妙处,主要有以下两点:

(1)技巧一:把数组的索引作为形参,而非数组本身

在这里插入图片描述

  • 如果参数是pre和inorder的子列表,优点在于方便理解,但是需要在递归过程中不断的从原始序列中复制出新的子列表,这个操作导致了额外的空间和时间消耗,导致递归函数空间时间复杂度均为 O(k),(k是当前状态复制的元素的个数)
  • 而如果参数是pre和inorder的元素索引,而且是对应着原始序列(对应原始序列的操作用哈希表来实现),则完全没有数组的拷贝操作,虽然需要额外思考索引和子序列的对应关系,但是子模块的 时间和空间复杂度都从 O(k) 降低到 O(1)
  • 进一步的,由于这是个递归过程,递归函数模块自己的 O(k) 的复杂度会导致main函数整体 O(k!) 的阶乘次复杂度

(2)技巧二:使用哈希表降低数组查找复杂度

用哈希表的目的:降低数组查找的时间复杂度

官方题解有如下描述:
在这里插入图片描述
所以在题解中做了如下操作:

	# 构造哈希映射,帮助我们快速定位根节点
	index = {element: i for i, element in enumerate(inorder)}

(3)总结:程序设计的精髓——指针

  • 通过上述的原因能看到,指针的巧妙使用能够将时间和空间复杂度降低到常数级别,所以在设计和优化程序时可以考虑的方式是巧用 指针+哈希表 的配合来从大型的数据结构中查找目标值
  • 此处的指针操作,有一种 “根据序列号在图书馆到处找书” 的感觉,非常妙

(五)更多注意

(1)迭代方式题解

在看LeetCode他人解答时,发现耗时最少的前几名写的基本上都是迭代法,而非递归,之前也有看到过相关说法称 “迭代法更接近计算机的计算本质,而递归其实是一种极大的浪费资源的算法”
在这里插入图片描述
所以有如下问题:

  • 迭代法是否比递归的耗时更少?有没有理论基础?
  • 编程的时候最好写迭代的方式吗?
  • 机试面试时是否会要求用迭代和递归两种解法解决?是否需要掌握迭代?
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值