大家好,今天我们的问题是给定一个二叉树的中序遍历数据序列和一个前序遍历数据序列,写一个重现这棵二叉树。假设每个节点都是唯一的值。例如给定序列如下:

中序遍历(in-order):F B A E H C D I G

前序遍历(pre-order):H B F E A C D G I

需要重现的二叉树如下图:

 

 

重温基础知识

这个问题乍一看是有一点困难,让我们先来重温一下关于中序遍历()和前序遍历的基础知识。

 

中序遍历

对于中序遍历的规则是首先找出所有左子树的节点,然后是根节点,最后是右子树的节点。以上一张二叉树图为例:

第一步,遍历根节点H的左子树,将其标记为H’L,然后H的右子树标记为H’R,则整棵树可以视作(H’L) H (H’R)。

第二步,单看H的左子树,发现这个左子树的根节点是B,则将这颗左子树视作(B’L) B(B’R),则整棵树可以视作(B’L) B (B’R) H (H’R)。

第三步,B的左子树只有一个F,所以F就被记录下来,添加到序列中,则整棵树可以视作F B (B’R) H (H’R)。

第四步,B的右子树,E是根节点,左子树是A,没有右子树,所以E和A也被添加到序列中,则整棵树可以视作F B A E H (H’R)。

以此类推,得到中序遍历结果为FB A E H C D I G。

 

前序遍历

前序遍历与中序遍历类似,但它的规则是首先找到根节点,然后是左子树,然后是右子树。以上一张二叉树图为例:

第一步,遍历根节点H的左子树,将其标记为H’L,然后H的右子树标记为H’R,则整棵树可以视作H (H’L) (H’R)。

第二步,单看H的左子树,发现这个左子树的根节点是B,则将这颗左子树视作B  (B’L)(B’R),则整棵树可以视作H B (B’L) (B’R) (H’R)。

第三步,B的左子树只有一个F,所以F就被记录下来,添加到序列中,则整棵树可以视作H B F (B’R) (H’R)。

第四步,B的右子树,E是根节点,左子树是A,没有右子树,所以E和A也被添加到序列中,则整棵树可以视作H B F E A (H’R)。

以此类推,得到中序遍历结果为HB F E A C D G I。

 

问题解决

有一个简单粗暴的方法来解决文章开始提出的问题,就基于中序遍历数据序列,我们可以穷举每个二叉树。然后再用前序遍历进行验证。因为有太多种可能,而且每次验证的时间复杂度都是O(n),所以总的时间是非常巨大的。

我们需要一些改进去提升算法的性能。我们都知道前序遍历一定会把整个树的根节点放到最前面,通过前序遍历数据序列的第一个元素,就能知道整棵树的根节点,这样我们就能在中序遍历数据序列找到适当的索引去区分左子树和右子树。在这个例子中,前序的第一个元素是H,所以根节点就是H,F B A E 就是左子树(标蓝),CD I G 就是右子树(标绿),如下所示:

中序遍历(in-order):F B A E H C D I G

前序遍历(pre-order):H B F E A C D G I

显而易见,蓝色的左子树和绿色的右子树又可以借助中序遍历和前序遍历的特征通过不断重复迭代(repeatrecursively),来重现整个树,如下图:

 

查看前序遍历的第一个元素,我们发现左子树的根节点是B,右子树的根节点是C,由此又可以得到新的左右子树,如下图所示

 

以此类推,最终得到如下图的二叉树:

 

明白了上面的过程,我们就会多一个问题,当我们知道了根节点,如何在中序遍历序列数据中找到节点对应索引。我们可以继续一个线性查询(linearsearch),即当我们一找到根节点就跳出,虽然每次查询是线性的,但是当我们递归调用的时候,我们需要为每个节点寻找对应索引,那么时间复杂度就变成了O(n²)。为此我们可以通过“空间换时间”的方法,优化性能。那么如何做呢?

 

算法提升

我们可以建立一个hash表去记录每个元素的索引,key用来存放节点,value用来存放索引,所以当给出一个节点,我们立马去查询hash表就可以直接得到索引,不需要再去通过查询数组来得到对应的索引。这个方法的时间复杂度是线性的,因为每个元素,我们只会去访问一次,所以时间复杂度是O(n+h)≈O(n)。

首先,有一个方法去构建一个hash表,并记录每个元素的索引,然后调用递归的helper方法得到整棵树,构建hash和调用helper方法的代码如下:

 

helper方法是递归方法,它的入参有前序遍历的列表,前序遍历的起始、结束标号,中序遍历的起始、结束标号以及两种遍历对应标号的hash映射,helper方法代码实现如下:

 

 

注意点:

1.基本情况(红框),由于是递归调用,一定要定义一个最简单的边界条件(boundarycondition),在我们这个递归调用时,如果遇到边界是非法的,就会返回null。

2.从前面的介绍中我们可以知道,在前序遍历和中序遍历中,叶子节点一定只有一个入口(entry),所以需要将叶子节点的左子树和右子树设置为null。

3.递归调用返回左子树(蓝框),前序遍历的结束标志(比如前例中H B F E A C D G I中前序遍历的开始标志为1(B),结束标志为0+1+4=5(C),左闭右开的区间)是基于左子树的长度的得到的。

4.递归调用返回右子树(绿框),原理类似蓝框中逻辑。

思考题

目前是已知前序遍历和中序遍历求二叉树,那如果知道前序遍历和后序遍历或者知道中序遍历和后序遍历求二叉树应该如何处理呢?欢迎你留言在下方。

这篇文章由我们的全新产品“刷题神器”BitTiger Pro中视频整理而成。