根据中序、前序遍历重建二叉树

文章目录

  • 题目
  • 递归
    • 思路
    • 细节
    • 易错
    • 代码
    • 复杂度分析
  • 迭代
    • 思路
    • 细节
    • 易错
    • 代码
    • 复杂度分析


题目

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

例如,给出

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

返回如下的二叉树:

在这里插入图片描述

限制:

0 <= 节点个数 <= 5000


递归

思路

首先要明确最重要的一个知识:

对于任意一颗树而言,前序遍历的形式总是

[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]

即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是

[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]

显然:

  • 对前序遍历来讲,找到左右子树的遍历结果分界线是困难的,找到根节点是简单的
  • 对中序遍历来讲,找到根节点是困难的,但找到根节点之后,左右两侧自然分成左右两棵子树

根据上面的特性,我们可以做出互补:

  1. 通过前序遍历的结果数组的首元素确定根节点
  2. 根据找到的根节点结合中序遍历数组确定左右子树的节点数目

重复上述过程,我们也就可以通过将每个节点视作根节点,不断递归生成左右子树,无法再生成左右子树。很显然生成左右子树的过程可以用递归思想来实现。


细节

思路有了,仍需解决几个问题:

  1. 即使通过前序遍历找到根节点,怎样确定根节点在中序遍历中的位置?
  2. 递归生成左右子树的细节操作是什么?

先解决第一个问题:

普通的方法当然是拿着根节点的值,从中序遍历结果数组inorder [0]开始遍历,但是每次在生成根节点时都进行遍历的话,时间复杂度较高(O(N))。因此可以使用哈希表来建设中序遍历数组值到下标键值对映射

在构造二叉树的过程之前,我们可以对中序遍历的列表进行一遍扫描(O(N)),就可以构造出这个哈希映射。

在此后构造二叉树的过程中,我们就只需要 O(1)的时间对根节点进行定位了。(一次O(N),N次O(1));否则我们必须每次都遍历一遍中序遍历结果数组定位根节点(N次O(N))。

再来说第二个问题:

递归生成左右子树这种说法听起来还是太“模糊”了,其实我们实际做的操作是不停的生成根节点,再进入这个根节点的左右子树,在每个子树中生成当前子树的根节点,直到这个”根节点“没有子树为止。

易错

写代码的时候没有子树应该返回空指针——return nullptr;,粗心大意写成了return null;,null和nullptr是有区别的。

代码

class Solution {
private:map<int, int> index; // 映射值给定值对应的下标
public:TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {int num = inorder.size();for (int i = 0; i < num; i++){index[inorder[i]] = i; // 建立中序遍历数组 值到下标 的键值对映射,快速定位根节点}return buildRoot(preorder, inorder, 0, num - 1, 0, num - 1);}// 把每个节点都当作它自身的“根节点”,进入到每个节点遍历生成它的左右子树、及根节点本身   TreeNode* buildRoot(vector<int>& pre, vector<int>& in, int pre_left, int pre_right, int in_left, int in_right) {if (pre_left > pre_right) { // 没有子树return nullptr;}int pre_root = pre_left; // 前序遍历的根节点就是左边界int in_root = index[pre[pre_left]]; // 根据映射关系确定中序遍历中的根节点TreeNode* root = new TreeNode(in[in_root]); // 建立根节点// 等价于TreeNode root = TreeNode(in[in_root]);// TreeNode *proot = &root;// 但没必要这样写,可能便于理解但是过于繁琐int lefttree_num = in_root - in_left; // 确定左子树中节点数目// 前序遍历 根节点(左边界)+1 到 根节点+左子树数量 的范围为左子树// 中序遍历根节点左侧为左子树root->left = buildRoot(pre, in, pre_left + 1, pre_left + lefttree_num, in_left, in_root - 1); // 前序遍历 根节点+左子树数量+1 到 右边界 的范围为右子树// 中序遍历根节点右侧为右子树root->right = buildRoot(pre, in, pre_left + 1 + lefttree_num, pre_right, in_root + 1, in_right);return root;}
};

复杂度分析

时间复杂度:O(n),其中 n 是树中的节点个数。

空间复杂度:O(n),除去返回的答案需要的 O(n)空间之外,我们还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h < n,所以总空间复杂度为 O(n)。



迭代

思路

前序遍历的相邻节点 u 和 v 有如下关系:

  1. v 是 u 的左儿子;
  2. u 没有左儿子。则 v 是 u 或者 u 祖先节点的右儿子。

以此树为例:
在这里插入图片描述
它的前序遍历和中序遍历分别为

preorder = [3, 9, 8, 5, 4, 10, 20, 15, 7]
inorder = [4, 5, 8, 10, 9, 3, 15, 20, 7]

可以看到,对于3,9,8,5,4它们之间满足第一种关系(例如:8是9的左儿子),对于4,10它们满足第二种关系,10是4祖父节点的右儿子。

也就是前序遍历会

  1. 从根节点开始,一直遍历左子树
  2. 直到左子树遍历完了,开始遍历右子树
  3. 如果当前的节点没有右子树,则会回溯遍历祖先节点的右子树。

那么我们可以根据这一特性,我们可以用一个栈来存储祖先节点和左子树,直到左子树被遍历完,(本例中,将3,9,8,5,4依次入栈,直到遇到10)此时开始寻找当前节点(10)是谁的右儿子。

细节

思路有了,仍需解决几个问题:

  1. 当开始遍历右子树,怎么确定当前节点是谁的右儿子呢?

这时来看中序遍历,我们可以发现,中序遍历结果数组的首元素是——根节点不断往左走达到的最终节点。 根据这一特性,我们可以创建一个指针 index 指向当前的最左子树

首先我们将根节点 3 入栈,再初始化 index 指向的节点为 4,随后对于前序遍历中的每个节点,我们依次判断它是栈顶节点的左儿子,还是栈中某个节点的右儿子。

  1. 我们遍历 9。9 一定是栈顶节点 3 的左儿子。我们使用反证法,假设 9 是 3 的右儿子,那么 3 没有左儿子,index 应该恰好指向 3,但实际上为 4,因此产生了矛盾。所以我们将 9 作为 3 的左儿子,并将 9 入栈。

stack = [3, 9]
index -> inorder[0] = 4

  1. 我们遍历 8,5 和 4。同理可得它们都是上一个节点(栈顶节点)的左儿子,所以它们会依次入栈。

stack = [3, 9, 8, 5, 4]
index -> inorder[0] = 4

  1. 当我们遍历到 10,这时情况就不一样了。我们发现此时 index 指向的节点和当前的栈顶节点一样,都为 4,也就是说 4 没有左儿子,那么 10 必须为栈中某个节点的右儿子。 那么如何找到这个节点呢?栈中的节点的顺序和它们在前序遍历中出现的顺序是一致的,而且每一个节点的右儿子都还没有被遍历过, 那么这些节点的顺序和它们在中序遍历中出现的顺序一定是相反的(原因如下)。

这是因为栈中的任意两个相邻的节点,前者都是后者的某个祖先。并且我们知道,栈中的任意一个节点的右儿子还没有被遍历过(前序遍历顺序——中左右),说明后者一定是前者左儿子,那么后者就先于前者出现在中序遍历中(中序遍历顺序——左中右)。

因此我们可以先把此时的栈顶元素保存并弹出, 然后把 index 不断向右移动,并与栈顶节点进行比较。如果 index 对应的元素恰好等于栈顶节点,那么说明上一个被弹出的节点没有右子树,且其本身是当前节点的左子树, 所以重复将栈顶节点保存并弹出,然后将 index 增加 1 的过程,直到 index 对应的元素不等于栈顶节点,此时 index 对应的元素就是上一个被保存且弹出的栈顶节点的右子树。按照这样的过程,我们弹出的最后一个节点 x 就是 10 的父节点,这是因为 10 出现在了 xx在栈中的下一个节点的中序遍历之间,因此 10 就是 x 的右儿子(根据中序遍历顺序——左中右,x是中,10是右,x在栈中的下一个节点x的父节点)。

回到我们的例子,我们会依次从栈顶弹出 4,5 和 8,并且将 index 向右移动了三次。我们将 10 作为最后弹出的节点 8 的右儿子,并将 10 入栈。

stack = [3, 9, 10]
index -> inorder[3] = 10

  1. 我们遍历 20时。index 恰好指向当前栈顶节点 10,那么我们会依次从栈顶弹出 10,9 和 3,并且将 index 向右移动了三次。我们将 20 作为最后弹出的节点 3 的右儿子,并将 20 入栈。

stack = [20]
index -> inorder[6] = 15

  1. 我们遍历 15,将 15 作为栈顶节点 20 的左儿子,并将 15 入栈。

stack = [20, 15]
index -> inorder[6] = 15

  1. 我们遍历 7。index 恰好指向当前栈顶节点 15,那么我们会依次从栈顶弹出 15 和 20,并且将 index 向右移动了两次。我们将 7 作为最后弹出的节点 20 的右儿子,并将 7 入栈。

stack = [7]
index -> inorder[8] = 7

此时遍历结束,我们构造出了正确的二叉树。

总结来讲就是,遍历前序遍历结果数组并将其压到栈中:

  1. 栈顶元素不等于index指向的元素时,将当前元素作为栈顶元素左儿子,然后当前元素入栈成为新栈顶
  2. 栈顶元素等于index指向的元素时,弹出并保存栈顶元素,同时将index递增1,再判断栈顶元素和index指向的元素之间的关系,相等则重复上述操作,不相等则将当前元素作为最后一个被弹出的栈顶元素右儿子,然后将当前元素入栈成为新栈顶

易错

  1. 通过判断前序遍历或中序遍历的结果数组是否为空,来确定二叉树是否为空。
  2. 二叉树不为空时,在建立左右子树的循环操作之前,先将根节点入栈。因为根节点的建立操作与其他左右子树不同,放到循环里面要单独处理,反而繁琐。
  3. 注意保存弹出的栈顶元素。
  4. 在生成右子树的时候,栈不为空也应该是重要的循环判定条件之一。

代码

class Solution2 { // 迭代
public:TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {if (!preorder.size()) {return nullptr;}stack<TreeNode*> st;TreeNode* root = new TreeNode(preorder[0]); // 建立根节点st.push(root); // 根节点入栈// 否则无法进行将节点归为左儿子或者右儿子的操作// 因为进行上面的操作需要访问栈顶元素的left或者rightint index = 0;for (size_t i = 1; i < preorder.size(); i++) {int pre = preorder[i];int in = inorder[index];auto node = st.top();if (node->val != in) { // 如果前序遍历i位置的数和中序遍历index位置的数不相等// 说明i位置的数是二叉树的左子树node->left = new TreeNode(pre);st.push(node->left);}else {while (!st.empty() && in == st.top()->val) {in = inorder[++index];node = st.top(); // 保存弹出的节点// 当跳出while时,pre的值即为该节点右子树st.pop();}node->right = new TreeNode(pre);st.push(node->right);}}return root;}
};

复杂度分析

时间复杂度:O(n),其中 n 是树中的节点个数。

空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,我们还需要使用 O(h)(其中 h 是树的高度)的空间存储栈。这里 h < n,所以(在最坏情况下)总空间复杂度为 O(n)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/443803.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深搜+剪枝

文章目录题目思路注意代码复杂度分析题目 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 单词必须按照字母顺序&#xff0c;通过相邻的单元格内的字母构成&#xff0c…

搜索+回溯问题(DFS\BFS详解)

文章目录题目思路DFS思路代码复杂度分析BFS思路代码复杂度分析题目 地上有一个m行n列的方格&#xff0c;从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动&#xff0c;它每次可以向左、右、上、下移动一格&#xff08;不能移动到方格外&#xff09;&am…

快速幂实现pow函数(从二分和二进制两种角度理解快速幂)

文章目录迭代实现快速幂思路int的取值范围快速幂从二进制的角度来理解从二分法的角度来理解代码复杂度分析进阶——超级次方思路倒序快速幂正序快速幂代码复杂度分析迭代实现快速幂 实现 pow(x, n) &#xff0c;即计算 x 的 n 次幂函数&#xff08;即&#xff0c;xn&#xff0…

n位数的全排列(需要考虑大数的情况)

文章目录题目思路代码题目 输入数字 n&#xff0c;按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3&#xff0c;则打印出 1、2、3 一直到最大的 3 位数 999。 示例 1: 输入: n 1 输出: [1,2,3,4,5,6,7,8,9] 说明&#xff1a; 用返回一个整数列表来代替打印 n 为正整数 …

正则表达式匹配(动规)

文章目录题目思路转移方程特征再探 i 和 j代码题目 请实现一个函数用来匹配包含 . 和 * 的正则表达式。模式中的字符 . 表示任意一个字符&#xff0c;而 * 表示它前面的字符可以出现任意次&#xff08;含0次&#xff09;。在本题中&#xff0c;匹配是指字符串的所有字符匹配整…

在循环递增一次的数组中插入元素

文章目录题目思路如何建立左右区间&#xff1f;如何查找最高点&#xff1f;那我们怎么判断 num 到底处于什么样的位置呢&#xff1f;如何确定插入位置&#xff1f;插入元素代码题目 给一个只循环递增一次的数组 res&#xff0c;res 满足首元素大于等于尾元素&#xff0c;形如&…

表示数值的字符串(有限状态自动机与搜索)

文章目录题目思路一代码一思路二代码二题目 思路一 考察有限状态自动机&#xff08;参考jyd&#xff09;&#xff1a; 字符类型&#xff1a; 空格 「 」、数字「 0—9 」 、正负号 「 」 、小数点 「 . 」 、幂符号 「 eE 」 。 状态定义&#xff1a; 按照字符串从左到右的…

树的子结构

文章目录题目深搜深搜代码广搜广搜代码题目 输入两棵二叉树A和B&#xff0c;判断B是不是A的子结构。(约定空树不是任意一个树的子结构) B是A的子结构&#xff0c; 即 A中有出现和B相同的结构和节点值。 例如: 给定的树 A: 给定的树 B&#xff1a; 返回 true&#xff0c;因为…

写题过程中碰见的小问题

文章目录和--vector二维vector的初始化数组中最大的数max_element()数组所有元素之和accumulate()vector数组去重对pair类型的vector排序对元素都为正整数的vector利用sort默认的升序排列进行降序排列一维数组转二维数组size_t和int如何不用临时变量交换两个数?将类函数的形参…

LeetCode——二叉树序列化与反序列化

文章目录题目思路问题一问题二代码实现题目 请实现两个函数&#xff0c;分别用来序列化和反序列化二叉树。 设计一个算法来实现二叉树的序列化与反序列化。不限定序列 / 反序列化算法执行逻辑&#xff0c;你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序…

jsp中生成的验证码和存在session里面的验证码不一致的处理

今天在调试项目的时候发现&#xff0c;在提交表单的时候的验证码有问题&#xff0c;问题是这样的&#xff1a;就是通过debug模式查看得知&#xff1a;jsp页面生成的验证码和表单输入的页面输入的一样&#xff0c;但是到后台执行的时候&#xff0c;你会发现他们是不一样的&#…

求1~n这n个整数十进制表示中1出现的次数

文章目录题目思路代码复杂度分析题目 输入一个整数 n &#xff0c;求1&#xff5e;n这n个整数的十进制表示中1出现的次数。 例如&#xff0c;输入12&#xff0c;那么1&#xff5e;12这些整数中包含1 的数字有1、10、11和12。可得1一共出现了5次。 思路 将个位、十位……每位…

求数字序列中的第n位对应的数字

文章目录题目思路代码复杂度分析致谢题目 数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中&#xff0c;第5位&#xff08;从下标0开始计数&#xff09;是5&#xff0c;第13位是1&#xff0c;第19位是4&#xff0c;等等。 请写一个函数&#xff0c…

一学就废的归并排序

文章目录其他与排序有关的文章原理代码实现复杂度分析其他与排序有关的文章 一学就废的三种简单排序【冒泡、插入、选择】 原理 归并排序&#xff08;Merge sort&#xff09;&#xff1a; 归并排序对元素 递归地 进行 逐层折半分组&#xff0c;然后从最小分组开始&#xff0c…

树状数组的相关知识 及 求逆序对的运用

文章目录树状数组概念前缀和和区间和树状数组原理区间和——单点更新前缀和——区间查询完整代码离散化sort函数unique函数去重erase函数仅保留不重复元素通过树状数组求逆序对树状数组概念 树状数组又名二叉索引树&#xff0c;其查询与插入的复杂度都为 O(logN)&#xff0c;其…

二叉搜索树相关知识及应用操作

文章目录概念查找二叉搜索树的第k大节点概念 二叉查找树&#xff08;Binary Search Tree&#xff09;&#xff0c;&#xff08;又名&#xff1a;二叉搜索树&#xff0c;二叉排序树&#xff09;——它或者是一棵空树&#xff0c;或者是具有下列性质的二叉树&#xff1a; 若它的…

二叉树相关知识及求深度的代码实现

文章目录树二叉树满二叉树和完全二叉树二叉树的性质代码实现求二叉树的深度树 树是一种非线性的数据结构&#xff0c;它是由n个有限结点组成一个具有层次关系的集合。 树的相关名词&#xff1a; 根节点&#xff1a;没有前驱结点的结点。父节点&#xff0c;子节点&#xff1a…

大端小端存储模式详解及判断方法

文章目录大小端模式的概念两种模式出现原因两种模式的优劣大小端的应用情景判断机器的字节序大小端模式的概念 当我们查看数据在内存中的存储情况时&#xff0c;我们经常会发现一个很奇怪的现象&#xff0c;什么现象呢&#xff1f; int main() {int i 12;return 0; }数据在内…

Linux 内存管理 | 物理内存、内存碎片、伙伴系统、SLAB分配器

文章目录物理内存物理内存分配外部碎片内部碎片伙伴系统(buddy system)slab分配器物理内存 在Linux中&#xff0c;内核将物理内存划分为三个区域。 在解释DMA内存区域之前解释一下什么是DMA&#xff1a; DMA&#xff08;直接存储器访问&#xff09; 使用物理地址访问内存&am…

Linux 内存管理 | 虚拟内存管理:虚拟内存空间、虚拟内存分配

文章目录虚拟地址空间用户空间内核空间用户空间内存分配malloc内核空间内存分配kmallocvmalloc虚拟地址空间 在早期的计算机中&#xff0c;程序是直接运行在物理内存上的&#xff0c;而直接使用物理内存&#xff0c;通常都会面临以下几种问题&#xff1a; 内存缺乏访问控制&a…