目录
(一)解题思路
两道题的解题思路相同,详细图解去看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)
对比
两题的主要代码区别在于:
- 生成根节点的节点不同——105:preo[0]; 106:posto[-1]
- 前序与后序的子区间划分位置不同——如图
(三)优化代码
官方解答的代码用了很多索引号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他人解答时,发现耗时最少的前几名写的基本上都是迭代法,而非递归,之前也有看到过相关说法称 “迭代法更接近计算机的计算本质,而递归其实是一种极大的浪费资源的算法”
所以有如下问题:
- 迭代法是否比递归的耗时更少?有没有理论基础?
- 编程的时候最好写迭代的方式吗?
- 机试面试时是否会要求用迭代和递归两种解法解决?是否需要掌握迭代?