算法学习记录| 2023.X.XX| 章节DayX| 题目号.题目标题 & 题目号.题目标题
106. 从中序与后序遍历序列构造二叉树
题目链接
思路1:详细思路
主体思路是分治法
解决此问题的关键在于要很熟悉树的各种遍历次序代表的什么,最好能够将图画出来。通过不断切割,一层一层处理即可找到所有节点,直接根据如下图的思路即可。重点就是搞清顺序后如何切割的问题。
切割思路大概分为这几步:
- 如果数组大小为零的话,说明是空节点了
- 如果不为空,那么取后序数组最后一个元素作为节点元素
- 找到后序数组最后一个元素在中序数组的位置,作为切割点
- 切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
- 切割后序数组,切成后序左数组和后序右数组(按照中序切割后的数组长度来作为切割标准)
- 递归处理左区间和右区间
代码
注意看注释,尤其是分割时的区间那一块,涉及到很多思考时的注意点和易错点
class Solution {
TreeNode* traversal (vector<int>& inorder, vector<int>& postorder){
//第一步:如果数组大小为零的话,说明是空节点了
if (postorder.size() == 0)
return NULL;
//第二步:如果不为空,那么取后序数组最后一个元素作为节点元素
int rootValue = postorder[postorder.size() - 1];
TreeNode* root = new TreeNode(rootValue);
//如果是叶子节点的话就不需要继续切割,把这个点存入树即可,返回当前根结点继续
if (postorder.size() == 1)
return root;
//第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
int delimiterIndex; //切割点坐标
for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++){
if (inorder[delimiterIndex] == rootValue)
break;
}
//第四步:切割中序数组,切成中序左数组和中序右数组(注意区间)(vector左闭右开)
//实际的右边界应该为切割点左一个,也就是角标为delimiterIndex-1
//结合vector语法,左闭右开,begin加的数其实就是角标,直接加切割点坐标则实际区间正好为左一个
//[0, delimiterIndex)
vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
//左边界直接是第一个坐标即可,根据vector语法直接begin加坐标
//[delimiterIndex + 1, end)
vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end());
//第五步:切割后序数组,切成后序左数组和后序右数组(注意区间)(vector左闭右开)
postorder.pop_back(); //后序数组抛弃末尾,因为已经用过了,也就是中间节点
//[0, leftInorder.size)
vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
//leftInorder角标正好是长度为leftInorder.size()的下一个的角标
//[leftInorder.size(), end)
vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());
//第六步:递归处理左区间和右区间
root -> left = traversal(leftInorder, leftPostorder);
root -> right = traversal(rightInorder, rightPostorder);
return root;
}
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (inorder.size() == 0 || postorder.size() == 0)
return NULL;
return traversal(inorder, postorder);
}
};
思路2:使用下标为参数的优化版本
写在前面:和思路1不同的是这次使用左闭右闭,同时创建一个哈希表pos来记录每个值在中序遍历中的位置。
先创建根节点,然后递归创建左右子树,并让指针指向两棵子树
A. 关于在中序遍历中对根节点进行快速定位:
一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,但这样做的时间复杂度较高。我们可以考虑使用哈希表来帮助我们快速地定位根节点。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的出现位置。这样在中序遍历中查找根节点位置的操作,只需要 O(1) 的时间
- 创建一个哈希表pos记录记录每个值在中序遍历中的位置。
- 先利用后序遍历找根节点:后序遍历的最后一个数,就是根节点的值;
- 确定左右子树的后序遍历和中序遍历,先递归创建出左右子树,然后创建根节点;
- 最后将根节点的左右指针指向两棵子树;
时间复杂度分析: 查找根节点的位置需要O(1) 的时间,创建每个节点需要的时间是 O(1),因此总的时间复杂度是 O(n)。
B. 关于子树的左右边界:
- 先利用后序遍历找根节点:后序遍历的最后一个数,就是根节点的值;
- 在中序遍历中找到根节点的位置 k,则 k 左边是左子树的中序遍历,右边是右子树的中序遍历;
- 假设il,ir对应子树中序遍历区间的左右端点, pl,pr对应子树后序遍历区间的左右端点。那么左子树的中序遍历的区间为 [il, k - 1],右子树的中序遍历的区间为[k + 1, ir];
- 由步骤3可知左子树中序遍历的长度为k - 1 - il + 1,由于一棵树的中序遍历和后序遍历的长度相等,因此后序遍历的长度也为k - 1 - il + 1。这样,根据后序遍历的长度,我们可以推导出左子树后序遍历的区间为[pl, pl + k - 1 - il],右子树的后序遍历的区间为[pl + k - 1 - il + 1, pr - 1];
代码
class Solution {
public:
unordered_map<int, int> pos;
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
int n = inorder.size();
for(int i = 0; i < n; i++){
pos[inorder[i]] = i; //记录中序遍历的根节点位置
}
return dfs(inorder, postorder, 0, n - 1, 0, n - 1);
}
TreeNode* dfs(vector<int>& inorder, vector<int>& postorder,int il, int ir, int pl, int pr){
if(il > ir) return nullptr;
int k = pos[postorder[pr]]; //中序遍历根节点位置
TreeNode* root = new TreeNode(postorder[pr]); //创建根节点
root->left = dfs(inorder, postorder, il, k - 1, pl, pl + k - 1 - il);
root->right = dfs(inorder, postorder, k + 1, ir, pl + k - 1 - il + 1, pr - 1);
return root;
}
};
总结
分割时的区间问题是最大的难点,首先是一定要保持一致性,另外就是必须得熟悉使用的计算机语言的基础,了解诸如vector(begin(),begin()+ x)的x到底指什么(其实就正好相当于角标,从0开始),以及end()并不指向最后一个元素
105. 从前序与中序遍历序列构造二叉树
题目链接
思路
和106一致,只要想清楚遍历顺序节点有什么特殊之处,就能不断找到根结点,先创建根节点,然后递归创建左右子树,并让指针指向两棵子树
代码
class Solution {
public:
unordered_map<int,int> pos;
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int n = preorder.size();
for (int i = 0; i < n; i++)
pos[inorder[i]] = i; //记录中序遍历的根结点位置
return dfs(preorder, 0, n - 1, inorder, 0, n - 1);
}
TreeNode* dfs(vector<int>& pre, int pl, int pr, vector<int>& in, int il, int ir){
if ( pl > pr )
return NULL;
int k = pos[pre[pl]] - il; //左子树长度;pos[pre[pl]]为中序遍历中根结点位置
TreeNode* root = new TreeNode(pre[pl]);
root -> left = dfs(pre, pl + 1, pl + k, in, il, il + k - 1);
root -> right = dfs(pre,pl + k + 1, pr, in, il + k + 1, ir);
return root;
}
};
总结
654. 最大二叉树
题目链接
思路
其实和上面两道题类似,同样是找到根节点,然后不断递归左右子树。
因此直接看代码就可以了
代码
class Solution {
public:
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
return dfs(nums, 0, nums.size() - 1); //传的是下标,所以右下标要-1
}
TreeNode* dfs(vector<int>& nums, int left, int right){
if (left > right)
return NULL;
int maxIndex = left; //初始化最大值的坐标
for (int i = left + 1; i <= right; i++){ //因为right是能到达的最大坐标,所以条件要有=
if (nums[i] > nums[maxIndex])
maxIndex = i;
}
TreeNode* root = new TreeNode(nums[maxIndex]);
root -> left = dfs(nums, left, maxIndex - 1);
root -> right = dfs(nums, maxIndex + 1, right);
return root;
}
};
总结
难点还是在于区间的限制。
本次解题采用左闭右闭,传入参数为实际的下标,因此在最开始传值时不能直接传整个数组的size,而应该减1。
另外在循环条件的设置中也要考虑到right是可以达到的,所以不能用小于,而应该用小于等于。