106m. 从中序与后序遍历序列构造二叉树
方法一:递归
用时:1h33m8s
思路
首先需要知道中序遍历和后序遍历的一些特性。
- 中序遍历:若能找到根节点在中序遍历中的位置,则根节点左边是左子树的中序遍历,根节点右边是右子树的中序遍历。
例如:
中序遍历为:[9, 3, 15, 20, 7]。
3为根节点,3的左边[9]是左子树的中序遍历,3的右边[15, 20, 7]是右子树的中序遍历。 - 后序遍历:最后一个元素为根节点,且若知道左子树的节点数
L
L
L,则后序遍历前
L
L
L个元素就是左子树的后序遍历,第
L
+
1
L+1
L+1至倒数第二个元素为右子树的后序遍历。
例如:上图的后序遍历为:[9, 15, 7, 20, 3]。
最后一个元素3为根节点,左子树有1个结点,则前1个元素[9]为左子树的后序遍历,第2至倒数第二个元素[15, 7, 20]为右子树的后序遍历。
根据上述两点性质,我们就可以根据中序遍历和后序遍历的结果重建二叉树。
递归逻辑:
- 后续遍历数组最后一个元素为根节点。
- 在中序遍历数组中找到根节点(查找的时间复杂度为 O ( n ) O(n) O(n),可以先用哈希表记录中序遍历数组,这样查找的时间复杂度就为 O ( 1 ) O(1) O(1)),并以根节点为边界将中序遍历数组分成左右两边,左边为左子树的中序遍历数组,右边为右子树的中序遍历数组。
- 统计左子树的数量 L L L,后序遍历数组的前 L L L个元素就是左子树的后序遍历数组,第 L + 1 L+1 L+1至倒数第二个元素为右子树的后序遍历数组。
- 分别知道左子树以及右子树的中后序遍历数组后,可以继续递归。
- 递归结束的条件:中后序遍历数组为空,则返回空结点;中后序遍历数组只有一个元素,则返回叶子结点。
如果递归传递的参数是每次更新后的中后序遍历数组的话,每次递归都需要新建一个数组,新建数组的时间复杂度和空间复杂度为 O ( n ) O(n) O(n),在力扣上会超出时间限制。此处可以进行优化,每次传递的数组引用传递,再传入新的中后序遍历数组在原先的中后序遍历数组的起止位置,每次递归只需更新起止位置,更新起止位置的时间复杂度和空间复杂度都为 O ( 1 ) O(1) O(1)。
起止位置的更新:
- 左子树的中序遍历数组:因为是从中间的根节点位置划分,所以起始位置与当前的起始位置一致,结束位置是根节点位置-1。
- 右子树的中序遍历数组:起始位置是根节点位置+1,结束位置与当前的结束位置一致。
- 左子树的后序遍历数组:首先要计算左子树的节点数,在中序遍历数组中,根节点往左是左子树,所以左子树的节点数为根节点位置减去中序遍历数组的当前起始位置。那么左子树的后序遍历数组的起始与当前的起始位置一致,结束位置是起始位置+节点数-1。
- 右子树的后序遍历数组:起始位置紧接着左子树的后序遍历数组的,即起始位置+节点数,结束位置是当前结束位置-1。
起止位置的更新这里比较绕,需要好好理清楚逻辑。
- 时间复杂度: O ( n ) O(n) O(n)。
- 空间复杂度: O ( n ) O(n) O(n)。
C++代码
class Solution {
private:
unordered_map<int, int> inorderMap;
TreeNode* _buildTree(vector<int>& inorder, int inBegin, int inEnd, vector<int>& postorder, int postBegin, int postEnd) {
if (inEnd - inBegin < 0) return nullptr;
TreeNode* root = new TreeNode();
int idx = inorderMap[postorder[postEnd]];
root->val = inorder[idx];
root->left = _buildTree(inorder, inBegin, idx - 1, postorder, postBegin, postBegin + idx - inBegin - 1);
root->right = _buildTree(inorder, idx + 1, inEnd, postorder, postBegin + idx - inBegin, postEnd - 1);
return root;
}
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
int size = inorder.size();
for (int i = 0; i < size; ++i) inorderMap[inorder[i]] = i;
return _buildTree(inorder, 0, inorder.size() - 1, postorder, 0, postorder.size() - 1);
}
};
方法二:迭代法
思路
- 时间复杂度: O ( ) O() O()
- 空间复杂度: O ( ) O() O()
C++代码
看完讲解的思考
迭代法看不懂,二刷时候再看看吧…
代码实现遇到的问题
一开始做不出来,稍微偷瞄了一下题解有了思路,但是实现起来磕磕绊绊,对于递归的边界一直搞错,那一块地方有点绕,需要好好理清楚,理清楚之后做下一题就秒接了。
105m. 从前序与中序遍历序列构造二叉树
方法一:递归法
思路
与上一题106m思路类似。
- 时间复杂度: O ( n ) O(n) O(n)。
- 空间复杂度: O ( n ) O(n) O(n)。
C++代码
class Solution {
private:
unordered_map<int, int> inorderMap;
TreeNode* _buildTree(vector<int>& preorder, int preBegin, int preEnd, vector<int>& inorder, int inBegin, int inEnd) {
if (preEnd < preBegin) return nullptr;
TreeNode* root = new TreeNode();
int idx = inorderMap[preorder[preBegin]];
root->val = inorder[idx];
root->left = _buildTree(preorder, preBegin + 1, preBegin + idx - inBegin, inorder, inBegin, idx - 1);
root->right = _buildTree(preorder, preBegin + 1 + idx - inBegin, preEnd, inorder, idx + 1, inEnd);
return root;
}
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int size = inorder.size();
for (int i = 0; i < size; ++i) inorderMap[inorder[i]] = i;
return _buildTree(preorder, 0, preorder.size() - 1, inorder, 0, inorder.size());
}
};
方法二:迭代法
用时:
思路
- 时间复杂度: O ( ) O() O()
- 空间复杂度: O ( ) O() O()
C++代码
看完讲解的思考
无。
代码实现遇到的问题
无。
654m. 最大二叉树
方法一:递归法
思路
递归逻辑:
- 根节点的值为当前数组最大值。
- 左节点为当前数组最大值左边的数组的最大值;右节点为当前数组最大值右边的数组的最大值,递归调用。
- 当数组为空时,返回空结点。
由于如果每次都新建数组传递参数,时间复杂度和空间复杂度都为 O ( n ) O(n) O(n),故数组采用引用传递,并且传递起止位置作为参数,时间空间复杂度将为 O ( 1 ) O(1) O(1)。
- 时间复杂度: O ( n 2 ) O(n^2) O(n2),递归的时间复杂度是 O ( n ) O(n) O(n),每次递归中查找最大值的时间复杂度也是 O ( n ) O(n) O(n),故时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
- 空间复杂度: O ( n ) O(n) O(n)。
C++代码
class Solution {
private:
TreeNode* _constructMaximumBinaryTree(vector<int>& nums, int begin, int end) {
if (begin > end) return nullptr;
int maxNum = -1;
int idx = -1;
for (int i = begin; i <= end; ++i) {
if (nums[i] > maxNum) {
maxNum = nums[i];
idx = i;
}
}
TreeNode* root = new TreeNode(maxNum);
root->left = _constructMaximumBinaryTree(nums, begin, idx - 1);
root->right = _constructMaximumBinaryTree(nums, idx + 1, end);
return root;
}
public:
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
return _constructMaximumBinaryTree(nums, 0, nums.size() - 1);
}
};
方法二:
用时:
思路
由于每次选取的父节点都是当前剩余数组中的最大值,且在左边的数组选择左节点,右边的数组选择右节点。
性质:
- 父节点一定大于子节点。
- 某个元素的右节点一定位于该元素的右边或者为空。
- 某个元素的左节点一定位于该元素的左边或者为空。
可以得出以下推论:
- 对于任意位置的一个元素x,x从右向左第一个比x大的元素记为l,x的左节点为l与x之间最大的元素,如果l与x之间没有其他元素,那么x的左节点为空。
- 对于任意位置的一个元素x,x从左向右第一个比x大的元素记为r,x的右节点为x与r之间最大的元素,如果x与r之间没有其他元素,那么x的右节点为空。
例如:3,2,1,6,0,5
任意选一个位置的数如2,2左边第一个比它大的数为3,3和2之间没有其他元素,则2的左节点为空,2右边第一个比它大的数为6,2和6之间最大的元素为1,那么2的右节点为1。
搞清楚上述这些我们就能发现,从根节点向下,一直是递减的,我们就可以用单调栈来实现。
=============================================
我们维护一个单调栈,里面的元素单调递减,那么遍历完整个数组后,单调栈中栈底部的元素肯定就是数组中最大的元素,即二叉树的根节点,且栈自底向上的结点会是下面结点的右节点。
自左向右遍历数组,维护单调栈的逻辑如下:
- 如果栈是空的,则直接入栈。
- 如果新增元素大于栈顶元素,那么新增元素结点的左节点更新为栈顶元素(推论1),然后弹出栈顶元素。重复此操作直到栈顶元素大于新增元素或者栈为空。通过这个操作,新增元素的左节点一定能找到推论1中所需要的元素或者空结点。
- 如果新增元素小于栈顶元素,那么栈顶元素结点的右节点更新为新增元素(推论2),然后将新增元素入栈。通过这个操作,每个结点的右节点一定是推论2中所述的元素或者空结点。
在遍历过程,栈内的元素一定是通过右节点逐个相连的,这点一定要搞清楚。
- 时间复杂度: O ( n ) O(n) O(n)。
- 空间复杂度: O ( n ) O(n) O(n)。
C++代码
class Solution {
public:
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
int size = nums.size();
vector<int> st; // 单调栈,单调递减,存放的是元素的索引
vector<TreeNode*> tree(size);
for (int i = 0; i < size; ++i) {
tree[i] = new TreeNode(nums[i]);
// 如果新来的大于栈顶元素
while (!st.empty() && nums[i] > nums[st.back()]) {
tree[i]->left = tree[st.back()]; // 新来的结点的左结点更新为栈顶元素
st.pop_back(); // 弹出
}
// 栈顶元素的右节点为当前节点
if (!st.empty()) tree[st.back()]->right = tree[i];
st.push_back(i);
}
return tree[st.front()];
}
};
看完讲解的思考
单调栈太巧妙了,也好难理解,搞了老半天QAQ。之后得多敲几遍,多手推几遍,多草稿纸模拟几遍,这样才能理解其中的奥妙。
代码实现遇到的问题
无。
最后的碎碎念
今天有点折磨…构造二叉树还有最后一题的单调栈…这真的是中等题吗QAQ,我感觉比我之前做的困难题还难,人麻了,搞到快12点,心态小崩。今天的题均题解字数写的是有史以来最多。