构建二叉树可以使用前序遍历+中序,或者中序+后序,但是前序+后序无法构建,原因是前序遍历列表中,第一个元素一定是根节点,剩余的元素是左子树的前序遍历+右子树的前序遍历列表,即前序遍历结构为: [ r o o t + [ l e f t + r i g h t ] ] [root+[left+right]] [root+[left+right]];而后序遍历结构为: [ [ l e f t + r i g h t ] + r o o t ] [[left+right]+root] [[left+right]+root],如果给定前序遍历和后序遍历结果,仅能推断出来的是谁是当前树的根结点,谁是左子树和右子树的遍历结果,但是无法区分左子树与右子树的遍历序列。这也就是只给定前序和后序无法构建二叉树的原因。
递归实现
思路这样:可以将传入的
p
r
e
o
r
d
e
r
preorder
preorder和
i
n
o
r
d
e
r
inorder
inorder看作是一个子树的遍历结果,即当前我们所处理的仅仅是一棵子树,这样我们就可以置身于众多递归过程中的一个。
前序遍历的结构为:
[
r
o
o
t
+
[
l
e
f
t
]
+
[
r
i
g
h
t
]
]
[root+[left]+[right]]
[root+[left]+[right]]
中序遍历的结构为:
[
[
l
e
f
t
]
+
r
o
o
t
+
[
r
i
g
h
t
]
]
[[left]+root+[right]]
[[left]+root+[right]]
所以根据前序遍历的第一个元素很容易定位其在中序遍历中的位置,从而确定左子树和右子树的中序遍历结果,
r
o
o
t
root
root在中序遍历中的索引值其实也是左子树的结点个数,所以很容易从前序遍历序列中将左子树和右子树的前序遍历序列分离开来。这样我们就得到了左右子树的前序遍历和中序遍历结果,接下来进行递归即可。
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
if preorder == []:
return None
core = preorder[0]
inorder_core_index = inorder.index(core)
root = TreeNode(val=core)
# 从两种遍历中将左右子树的元素全部分离开来
#
# 这里只有在树中没有重复元素的时候才可以
left = inorder[: inorder_core_index]
right = inorder[inorder_core_index+1:]
# inorder_core_index的数值也是左子树的元素个数
# 前序遍历和中序遍历的共同点是,遍历结果的最后一个元素一定是当前树的右下角的叶子元素
root.left = self.buildTree(preorder[1: 1+inorder_core_index], left)
root.right = self.buildTree(preorder[1+inorder_core_index:], right)
return root
递归方法改进
容易看出来,递归算法每层递归的时候,都需要查找 r o o t root root在 i n o r d e r inorder inorder中的索引值,所以时间更多的花在了这里,为了减少时间复杂度,可以使用哈希表将 i n o r d e r inorder inorder中的结点索引信息存储起来,等到需要的时候直接查询即可,时间复杂度降为 O ( 1 ) O(1) O(1),而存储结点索引信息所使用的内存与 i n o r d e r inorder inorder的长度有关,将上述思路实现为代码如下:
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
def mybuildTree(preorder, inorder):
if preorder == []:
return None
core = preorder[0]
inorder_core_index = index[core]
root = TreeNode(val=core)
# 从两种遍历中将左右子树的元素全部分离开来
#
# 这里只有在树中没有重复元素的时候才可以
left = inorder[: inorder_core_index]
right = inorder[inorder_core_index+1:]
# inorder_core_index的数值也是左子树的元素个数
# 前序遍历和中序遍历的共同点是,遍历结果的最后一个元素一定是当前树的右下角的叶子元素
root.left = mybuildTree(preorder[1: 1+inorder_core_index], left)
root.right = mybuildTree(preorder[1+inorder_core_index:], right)
return root
# 构建哈希映射表
index = {val: ind for ind, val in enumerate(inorder)}
return mybuildTree(preorder, inorder)
拿去运行,代码并不能跑通,问题出在哪里?
没有优化的时候,我们查找索引直接用的是传进来的 i n o r d e r . i n d e x ( ) inorder.index() inorder.index()来实现的,所以问题就在于构建哈希表所使用的是一开始传过来的 i n o r d e r inorder inorder,而自定义函数所使用的是切片,索引自然不同,为了让这个构建的哈希表能用上,还不能动一开始的 p r e o r d e r preorder preorder和 i n o r d e r inorder inorder。如何实现呢?
思考我们传进去自定义函数的参数是切片,切片的核心就是原列表和索引,而原列表已经有了,所以我们可以将传入切片换成传入索引来实现,需要注意的是,将切片换为索引之后,求左子树的遍历列表长度不能直接用inorder_core_ndex变量的索引值,要手动计算。代码实现如下:
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
def mybuildTree(preorder_begin, preorder_end, inorder_begin, inorder_end):
if preorder_begin > preorder_end:
return None
core = preorder[preorder_begin]
inorder_core_index = index[core]
left_size = inorder_core_index - inorder_begin # 左子树的大小不再是根节点的索引值
root = TreeNode(val=core)
# 前序遍历和中序遍历的共同点是,遍历结果的最后一个元素一定是当前树的右下角的叶子元素
root.left = mybuildTree(preorder_begin+1, left_size+preorder_begin, inorder_begin, inorder_core_index)
root.right = mybuildTree(left_size+preorder_begin+1, preorder_end, inorder_core_index+1, inorder_end)
return root
length = len(preorder)
# 构建哈希映射表
index = {val: ind for ind, val in enumerate(inorder)}
return mybuildTree(0, length-1, 0, length-1)
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是树中的节点个数。
空间复杂度: O ( n ) O(n) O(n),除去返回的答案需要的 O ( n ) O(n) O(n)空间之外,我们还需要使用 O ( h ) O(h) O(h)(其中 h h h是树的高度)的空间存储栈。这里 h < n h<n h<n,所以(在最坏情况下)总空间复杂度为 O ( n ) O(n) O(n)。
将切片换成索引还有一个优点,内存消耗变小了,因为python中的切片其实是浅拷贝,对于非嵌套列表,浅拷贝之后的列表和原来的列表将不再是同一个列表,这就意味着将重新开辟一块内存空间来存储这段数据,将切片换成索引传参之后就可以很好的解决这个问题。下图中,上方的运行结果为改进之后的递归方法,下面的运行结果为改进前的递归方法,时间和内存消耗相差非常大。