递归的代码很简单
def buildTree(preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
def func(m,l,r):
"""
m表示树根节点在preorder的索引
l和r表示该树所有节点在inorder的区间索引(闭区间)
"""
# 递归终止条件,由于是闭区间,所以l大于r时才是失效
if l > r: return
# 首先创建根节点
root = TreeNode(preorder[m])
# 通过inorder_map得到根节点在中序遍历preorder的索引
tmp = inorder_map[preorder[m]]
# 处理根节点的左子树,该子树的根节点在preorder的索引一定紧跟其父节点
# 且该子树的节点在 父节点的左边,即[l,tmp-1]
root.left = func(m+1,l,tmp-1)
# 左子树的节点个数为 tmp-l,则右子树根节点在preorder的索引为m+tmp-l+1,
# 区间范围在[tmp+1,r]
root.right = func(tmp-l+1+m,tmp+1,r)
return root
# 利用无重复元素的特性构建map,从而能够得到preorder元素到inorder索引的映射
inorder_map = {v:key for key,v in enumerate(inorder)}
return func(0,0,len(inorder)-1)
时间复杂度为O(n),遍历了所有节点
空间复杂度:Hashmap O(n),递归栈空间O(h),h<n,空间复杂度为O(n)
迭代方法,首先要理解这样一个事实:
对于前序遍历中的任意两个连续节点 a 和 b,根据前序遍历的流程,我们可以知道 a 和 b 只有两种可能的关系:
1. a 是 b 的左儿子。这是因为在遍历到 a 之后,下一个遍历的节点就是 a 的左儿子,即 b;
a 没有左儿子,并且 b 是 a 的某个祖先节点(或者 a 本身)的右儿子。如果 a 没有左儿子,那么下一个遍历的节点就是 a 的右儿子。如果 a 没有右儿子,我们就会向上回溯,直到遇到第一个有右儿子(且 a 不在它的右儿子的子树中)的节点 A,那么 b 就是 A 的右儿子。
引入一个栈,这个栈的栈顶是当前节点,内容是 当前节点的所有还没有考虑过右儿子的祖先节点。引入一个指针,index,这个指针对应的是中序遍历的元素,该元素为:当前节点不断往左走能够到达的最终节点。
以下数为例:
3
/ \
9 20
/ / \
8 15 7
/ \
5 10
/
4
易知:
preorder = [3, 9, 8, 5, 4, 10, 20, 15, 7]
inorder = [4, 5, 8, 10, 9, 3, 15, 20, 7]
从根节点3出发,前序遍历首先往左走,所以我们将3入栈,3是第一个没有考虑过有儿子的节点,且目前他是栈顶元素,即当前节点。指针index指向的一定是4,即当前节点不断往左走达到的最终节点(利用中序遍历的性质),之后开始遍历preorder 3之后的元素。
对于9,易知9一定是3的左儿子,因为3往左走能达到的最远节点是4,如果9是3的右儿子,那说明3没有左节点,因为中序遍历之后马上就遍历到了9。此时我们将9入栈,index不变,因为目前没有任何情况说明当前节点不断往左走达到的最终节点存在任何的改变。
之后,我们再遍历8,当前节点为9,同同理8一定是9的左儿子,入栈;
遍历5,入栈;
遍历4,入栈;(遍历4时,当前节点为5,如果4是右儿子,那么index指向的应为5,所以4一定是左儿子)
此时栈内元素为:stack = [3,9,8,5,4];
继续遍历10:
此时当前节点为4,index指向的节点也为4,他们一定是同一个节点(题目指明没有重复元素),所以当前节点一定没有左儿子,则10应该是栈中某个节点的右儿子,因为栈中的节点的右儿子的元素都没有被考虑过,且10是节点没有左儿子之后前序遍历得到的第一个节点,所以他一定是一个右儿子,而不是孙子节点等。
这时,当前节点为4,栈顶元素为4,index指向的也是4,这个时候,4的右儿子是10吗? 一定不是,如果4的右儿子是10,那么在中序遍历之后,4之后紧邻的元素应为10,而实际情况是4之后紧邻的元素为5,而5一定不是4的右儿子。所以,我们考虑了4的右儿子,发现4没有右儿子,这个时候可以将4从栈中pop(),因为我们的栈内元素包含的是没有考虑右儿子的节点。
pop()之后栈顶元素为5,即当前元素为5,我们让index往右移动一个单位,由于4没有右儿子,所以往右移动一个单位之后,index指向的一定是4的父节点(中序遍历性质)。这个时候,当前节点为5,栈顶元素为5,index指向也是5,5的右儿子是10吗?一定不是,如果5的右儿子是10,那么在中序遍历之后,5之后紧邻的元素应为10,而实际情况是5之后紧邻的元素是8,而8一定不是5的右儿子。所以,我们考虑了5的右儿子,发现5没有右儿子,这个时候可以将5从栈中pop()。
pop()之后栈顶元素为8,即当前元素为8,我们让index往右移动一个单位,由于5没有右儿子,所以往右移动一个单位之后,index指向的一定是5的父节点(中序遍历性质)。这个时候,当前节点为8,栈顶元素为8,index指向也是8,8的右儿子是10吗?是的,因为如果10不是8的右儿子,又因为他紧邻8之后在中序遍历出现,所以10一定是8的父节点,而很显然这是错误的。
那么这里就是一个停顿点,这个时候要把10设置为8的右儿子,如果识别这个停顿点呢?如果栈继续pop(),index往右再移动一个单位,我们发现,栈顶元素不再等于index指向的元素了(即栈顶元素往左走最终能到达的节点不再是其本身了,这种情况下我们认为之前的pop()+index右移操作不合法,导致不合法出现的情况就是,在我们一步一步往终点前进的时候,有人出轨了(有人除了左儿子,还跟别人造出了右儿子),我的pop()得不到你的index对应,我俩在这个时候就应该停止了,缘分到头了),这就是停顿点。即在我们的pop()和移动index的过程中,如果发现栈顶元素不再等于index指向的元素,那么我们将最后一个pop()的元素作为目前前序遍历节点父节点。
遍历到10,经历了上述感情纠纷之后,终于弄清楚了10到底是谁的儿子。
此时,stack=[3,9,10],index指向10,那么继续往下走,遍历20,我们发现index指向的节点又变成了当前节点往左最终能到达的节点,那么我们就又要弄清楚,20到底是谁的儿子了,继续开启之前的感情纠纷。
出栈+index右移,当栈顶元素不再等于index指向的元素之后,将最后一个pop()出的元素作为当前前序遍历节点的父节点,即将3作为20的父节点(stack为空,index指向15,此时停顿,3是最后pop()出的元素)。并将20入栈,因为我们还没有考虑20的右儿子。
继续遍历15,当前index指向15,如果15是20的右儿子,那么当前index应该指向20,所以的15是20的左儿子。那么将15入栈。
此时stack = [20,15], index指向15。
继续遍历7,当前index指向的元素与栈顶元素相同,即15没有左儿子了,那么7是谁的右儿子?继续开启上述感情纠纷,直到栈顶元素不再等于index指向的元素之后,目前的情况是,stack为空,而index指向的元素为7,我们将7作为最后弹出的20的右儿子。并将7入栈。此时遍历结束。
算法:
我们用一个栈和一个指针辅助进行二叉树的构造。初始时栈中存放了根节点(前序遍历的第一个节点),指针指向中序遍历的第一个节点;
我们依次枚举前序遍历中除了第一个节点以外的每个节点。如果 index 恰好指向栈顶节点,那么我们不断地弹出栈顶节点并向右移动 index,并将当前节点作为最后一个弹出的节点的右儿子;如果 index 和栈顶节点不同,我们将当前节点作为栈顶节点的左儿子;
无论是哪一种情况,我们最后都将当前的节点入栈。
代码:
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
if not preorder:
return None
root = TreeNode(preorder[0])
stack = [root]
inorderIndex = 0
for i in range(1, len(preorder)):
preorderVal = preorder[i]
node = stack[-1]
if node.val != inorder[inorderIndex]:
node.left = TreeNode(preorderVal)
stack.append(node.left)
else:
while stack and stack[-1].val == inorder[inorderIndex]:
node = stack.pop()
inorderIndex += 1
node.right = TreeNode(preorderVal)
stack.append(node.right)
return root