文章目录
【面试题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