本文讲解一个经典的面试题,使用 python 通过迭代和递归两种方法重构二叉树。

题目描述

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如,给出:

前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]

返回如下的二叉树:

 3/ \9  20/  \15   7

限制:0 <= 节点个数 <= 5000。

递归方法

二叉树的前序遍历顺序是:根节点、左子树、右子树,每个子树的遍历顺序同样满足前序遍历顺序。

二叉树的中序遍历顺序是:左子树、根节点、右子树,每个子树的遍历顺序同样满足中序遍历顺序。

前序遍历的第一个节点是根节点,只要找到根节点在中序遍历中的位置,在根节点之前被访问的节点都位于左子树,在根节点之后被访问的节点都位于右子树,由此可知左子树和右子树分别有多少个节点。

由于树中的节点数量与遍历方式无关,通过中序遍历得知左子树和右子树的节点数量之后,可以根据节点数量得到前序遍历中的左子树和右子树的分界,因此可以进一步得到左子树和右子树各自的前序遍历和中序遍历,可以通过递归的方式,重建左子树和右子树,然后重建整个二叉树。

递归方法的基准情形有两个:判断前序遍历的下标范围的开始和结束,若开始大于结束,则当前的二叉树中没有节点,返回空值 null。若开始等于结束,则当前的二叉树中恰好有一个节点,根据节点值创建该节点作为根节点并返回。若开始小于结束,则当前的二叉树中有多个节点。在中序遍历中得到根节点的位置,从而得到左子树和右子树各自的下标范围和节点数量,知道节点数量后,在前序遍历中即可得到左子树和右子树各自的下标范围,然后递归重建左子树和右子树,并将左右子树的根节点分别作为当前根节点的左右子节点。

展开来说,以下面的图为例子:

根据前序遍历得知,3 是根节点。

根据中序遍历得知,9 是左子树且节点数为1,[15, 20, 7] 是右子树且节点数为3。

根据上一步得到的左子树右子树节点个数,可以将前序遍历划分为左子树、右子树。

前序遍历中左子树的节点包含 [9] ,于是 9 就是左子树根节点。另外,还可以知道左子树在前序遍历中的左右边界为 [1, 1],由于左右边界相同,说明当前的左子树其实是叶节点,续划分左子树右子树的话,应该返回None。

前序遍历中右子树的节点包含 [20, 15, 7] ,于是 20 是右子树根节点。另外,右子树在前序遍历中的边界为 [2, 4],左右边界不相同,需要以20作为根节点继续划分子树。如下图所示:

中序遍历中发现 20 的左子树有一个节点,右子树有一个节点,因此在前序遍历中可以确定 [15] 为左子树,[7] 为右子树。由于 20 的左右子树在前序遍历中边界相同,所以是叶节点,树构建完毕。

根据上面的理论,可以得到下面的代码实现:

class TreeNode:def __init__(self, x):self.val = xself.left = Noneself.right = Noneclass Solution:# 递归(1) 56 msdef buildTree(self, preorder, inorder):# 将前序遍历序列保存为属性,方便递归时得到 valself.po = preorder# 构造一个字典,方便确定边界self.dic = {}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):# 由于叶子节点没有左右子树,弱继续划分则应该得到None# 因此,当左边界大于右边界时结束递归if in_left > in_right:return # 根据根节点的位置得到 valnode = self.po[pre_root]# 构造根节点root = TreeNode(node)# 得到根节点的 val 在中序遍历序列中的位置i = self.dic[node]# 构造左子树# 左子树根节点为前序遍历序列根节点右边第一个元素# 右边界为中序遍历根节点位置左边的一个位置root.left = self.recur(pre_root+1, in_left, i-1)# 构造右子树# (i-in_left) 表示左子树节点的个数# 右子树根节点为前序遍历序列根节点右边第(i-in_left)+1个元素# 左边界为中序遍历根节点位置右边的一个位置root.right = self.recur(pre_root+(i-in_left)+1, i+1, in_right)return roott = Solution().buildTree([3,9,20,15,7], [9,3,15,20,7])
print(t.val, t.left.val, t.right.val)

时间复杂度 O(N): N 为树的节点数量。初始化 HashMap 需遍历 inorder ,占用 O(N) ;递归共建立 N 个节点,每层递归中的节点建立、搜索操作占用 O(1),因此递归占用 O(N)。(最差情况为所有子树只有左节点,树退化为链表,此时递归深度 O(N) ;平均情况下递归深度 O(log2Nlog_2 Nlog2​N)。

空间复杂度 O(N): HashMap 使用 O(N) 额外空间;递归操作中系统需使用 O(N) 额外空间。

基于递归的思想,还有一种写法:

class Solution:# 递归(2) 192 msdef buildTree(self, preorder, inorder):if not preorder:return Noneloc = inorder.index(preorder[0])root = TreeNode(preorder[0])root.left = self.buildTree(preorder[1 : loc + 1], inorder[ : loc])root.right = self.buildTree(preorder[loc+1 : ], inorder[loc+1: ])return root

这样的写法更多的使用了python内置的方法,看起来十分简洁,就是执行时间长了一些。

迭代方法

例如要重建的是如下二叉树:

        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) 压入栈内,接下来准备一路向左构建左子树。
  • 初始化中序遍历下标为 0。
  • 遍历 preorder 中还未访问过的每个元素(例如 [9,8,5,4,10,20,15,7]),判断栈顶元素是否等于中序遍历下标指向的元素 (例如 4) 。
  • 若栈顶元素不等于中序遍历下标指向的元素 (例如 4) ,说明当前的 preorder 元素还没到达树的最左端,则将当前元素 (例如 9) 作为其上一个元素(例如栈顶元素 3)的左子节点,并将当前元素 (例如 9) 压入栈内,继续对 preorder 中还未访问过的每个元素(例如 [8,5,4,10,20,15,7]) 重复此过程,一直向左构建子树 (例如可以得到 3->9->8->5->4),并将构建的子树入栈 (例如得到栈 [3,9,8,5,4])。
  • 若栈顶元素(即上一次遍历的元素,例如 4)等于中序遍历下标指向的元素 (例如 4) ,则从栈内弹出一个元素 (例如 弹出 4),同时令中序遍历下标指向下一个元素 (例如 5) ,之后继续判断栈顶元素 (例如 5) 是否等于中序遍历下标指向的元素,若相等则重复该操作,直至栈为空或者元素不相等(例如 8 出栈以后,栈顶元素 9 与中序遍历元素 10 不同),这说明在栈顶元素(例如 8) 产生了分支,因此令当前元素 (例如 10) 作为最后一个相等元素(即刚刚出栈的元素 8 )的右节点。
  • 遍历结束,返回根节点。

代码如下:

class TreeNode:def __init__(self, x):self.val = xself.left = Noneself.right = Noneclass Solution:# 迭代     56 msdef buildTree(self, preorder, inorder):if not preorder:return None    root = TreeNode(preorder[0])stack = []L = len(preorder)stack.append(root)index = 0               for i in range(1, L):preorderval = preorder[i]# 中序的当前 i 不等于栈顶时表示还没搜索到根节点,# 每次append的的preorder[i]其实都是其左子树,左子树的左子树,...if stack[-1].val != inorder[index]:     #进左子树node = stack[-1]node.left = TreeNode(preorderval)stack.append(node.left)#一旦中序inorder里的元素与栈顶相等时,表示左子树已经走完了else:# 如果栈弹空了,表示根节点root出栈了# == 时表示当前出栈元素没有右孩子while stack and stack[-1].val == inorder[index]:node = stack[-1]stack.pop()index += 1     node.right = TreeNode(preorderval)      stack.append(node.right)return root     t = Solution().buildTree([3,9,20,15,7], [9,3,15,20,7])
print(t.val, t.left.val, t.right.val)

时间复杂度:O(n) ,前序遍历和后序遍历都被遍历。

空间复杂度:O(n) ,额外使用栈存储已经遍历过的节点。

参考文章:

面试题07. 重建二叉树

迭代-栈图解:剑指07. (前序中序)重建二叉树:递归

【剑指offer 07】用迭代和递归两种方法重构二叉树(python实现)相关推荐

  1. java输出链表的值_[剑指offer] 从尾到头打印链表(三种方法) java

    一.每次把新遍历的链表值放到list索引为0的位置,实现逆序. public class Solution { public ArrayList printListFromTailToHead(Lis ...

  2. 【LeetCode】剑指 Offer 07. 重建二叉树

    [LeetCode]剑指 Offer 07. 重建二叉树 文章目录 [LeetCode]剑指 Offer 07. 重建二叉树 package offer;import java.util.ArrayD ...

  3. 剑指offer——面试题61:按之字形顺序打印二叉树

    剑指offer--面试题61:按之字形顺序打印二叉树 Solution1: 基于上一题的解法,缺点:效率低下! /* struct TreeNode {int val;struct TreeNode ...

  4. 剑指offer——面试题23:从上往下打印二叉树

    剑指offer--面试题23:从上往下打印二叉树 Solution1: 典型的BFS算法! 思路一开始没想到,按照书上的思路写的答案... 注意:deque是双向队列,在头尾插入都很快! /* str ...

  5. 剑指offer——面试题17:合并两个排序的链表

    剑指offer--面试题17:合并两个排序的链表 Solution1: 不要犯低级错误... /* struct ListNode {int val;struct ListNode *next;Lis ...

  6. 剑指offer——面试题7:用两个栈实现队列

    剑指offer--面试题7:用两个栈实现队列 Solution1: 注意栈的基本操作与vector略有不同~ class Solution { public:void push(int node) { ...

  7. 剑指offer编程题(JAVA实现)——第38题:二叉树的深度

    github https://github.com/JasonZhangCauc/JZOffer 剑指offer编程题(JAVA实现)--第38题:二叉树的深度 题目描述 输入一棵二叉树,求该树的深度 ...

  8. LeetCode_剑指 Offer 57. 和为s的两个数字(利用set、双撞指针两种思路 Java实现)

    题目描述:剑指 Offer 57. 和为s的两个数字 输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s.如果有多对数字的和等于s,则输出任意一对即可. 示例 1: 输入:n ...

  9. c语言中fact函数怎么调用,C语言程序题: 1、编写一个求n!的函数fact(n),要求fact函数分别用递归和非递归两种方法实现...

    点击查看C语言程序题: 1.编写一个求n!的函数fact(n),要求fact函数分别用递归和非递归两种方法实现具体信息 答:int fac(int n) //非递归{int f=1; for(;n;) ...

最新文章

  1. 冒泡排序_python实现冒泡排序
  2. IntelliJ IDEA 2021.3.2 发布:告别不断建议安装xx插件的提示!
  3. ATMEGA8 DIP-28面包板实验
  4. mysql--字段--索引的增删改查
  5. 详解@EnableEurekaServer和@EnableDiscoveryClient 或 @EnableEurekaClient注解
  6. CruiseControl.NET与TFS结合的配置文件
  7. 单片机定时器_单片机定时器/计数器基本原理
  8. SQLSERVER事务日志已满 the transaction log for database 'xx' is full
  9. STM32驱动SPI LCD屏幕
  10. 电瓶车.20180809
  11. 用树莓派做linux电视盒子,用树莓派制造一台“口袋电视”
  12. 微信多订单合并付款_微信小商店订单合并打单,操作分享请收藏!
  13. DNS域名服务之:排查DNS的故障
  14. 分享 stormzhang的Andoid学习之路
  15. 荣联科技再出发,奏响集成商转型最强音
  16. iscroll.js的使用
  17. 一个小需求引发的思考
  18. 今天是大四的第一天,感觉自己特别的慌,在秋招的路上我一个人单枪匹马,在这里我将记录我的历程。
  19. (30)虚拟时钟create_virtual_clock
  20. PyQt5技术分享:制作一个美观的Dock栏

热门文章

  1. sublime安装Codecs33
  2. Spark-shell进行粘贴模式
  3. 各种编码范围总结以及linux下面的编码批量转化
  4. 分别用matlab和python计算物品相似度矩阵(Jaccard系数
  5. 测试php是否连接mysql_如何测试php是否连接mysql成功
  6. linux实验五编程淮海工学院,实验一-LinuxC编程工具GCC和GDB.doc
  7. 机器学习入门-文本数据-使用聚类增加文本的标签属性
  8. python 【第一篇】基础数据类型
  9. Linux环境下服务器 Tomcat war包部署步骤
  10. 实验二 初始化阶段-source.c