java算法day17
今天的内容基本上涉及:二叉树的构建。
- 106 从中序与后序序列构造二叉树
- 105 从前序与中序序列构造二叉树
- 654 最大二叉树
106从中序与后序序列构造二叉树
这个题如果你会手算,那么你就很容易理解下面这个过程。
例子:
根据中序和后序遍历,可以得到:
在进行下一步之前。
先知道中序遍历和后序遍历的性质。这也是手算的核心。
由上图可以得到两个性质。
1.在后序遍历的中,最后一个元素就是树的根节点。
2.在中序遍历中,根节点的左边为左子树,根节点的右边为右子树。
可以记,但是要理解本质
先从中序遍历:
这就涉及到中序遍历的过程,由于中序是一直向底层,扫完根节点的左子树后,然后才到根节点,然后才是右子树,那么就得到,在中序序列中,根节点,划分开了左右子树的区间。而且这个性质是递归满足的,因为按根节点的左子树的根节点来想想,仍然是满足这个过程。因此在左区间,仍然可以根据子树的根节点,再次划分出子树的左右子树。
后序遍历:
流程基本一样,按照后序遍历的性质,那么区间的最后一个节点就是根节点,因为根节点是最后遍历到的。由于是先遍历左子树,然后右子树,然后才是根节点。所以根节点左边的序列也是可以划出左右区间。
根据这两个性质,就可以做题了。
我们手算的思路:
后序找最后一个节点,确定根节点,这个点就作为我们构建的目标树的根节点,在中序中找到根节点,通过中序中的该根节点,划分两个区间,就是代表了左右子树。后序序列也要划分区间。
这里我当时就没想到怎么划分区间,看了题解才知道的。
由于中序已经确定了两个区间,那么左右区间的大小是可以得出来得。因为有中序序列中这个根节点的位置。那么通过区间大小,后序序列中的区间划分就很容易得到了。
这样就完成了一轮要干的事情。那么后面的过程就是对左右区间依次这么干。
所以,这就看到了递归的思想,原问题和子问题模型。那么就自然而然的想到了用递归来做。
所以得出做法思路:
1.首先在后序遍历序列中找到根节点(最后一个元素),这个节点就用来用来构造的树节点。
2.根据根节点在中序遍历序列中找到根节点的位置
3.根据根节点的位置将中序遍历序列分为左子树和右子树
4.根据根节点的位置确定左子树和右子树在中序数组和后续数组中的左右边界位置
5.递归构造左子树和右子树
6.返回根节点结束
过程中的注意点
1.变量定义
因为在递归的过程中,要传递区间。所以需要定义几个变量:
(1)HashMap inorderIndexMap用来记录中序遍历中每个值的下标索引,因为每次拿到后序中的最后一个节点,还要去确定该点在中序中的位置,正常来说我们的方法一般都是遍历去找。所以对这个查找过程我们就可以想到用hashMap来进行查找优化。该HashMap,key是节点的值,value是该节点在中序序列中的下标索引。为什么value是下标索引,因为找在中序中的位置的主要用途是用来划分左右子树的区间。
(2)中序序列根节点的索引位置rootIndexInorder
(3)中序遍历数组的区间始末位置inStart和inEnd
(4)后序遍历数组的区间始末位置postStart和postEnd
2.位置关系的计算
由于对于中序和后序序列而言,都要递归其左右子树,那么传递给下一层的时候,区间的位置就要算给清楚。所以这就涉及边界位置的计算。
看这个图很快就能懂,不要有划分区间了就将区间又分割成两个从0开始的。
从这个图计算下一层要传递的边界。
左子树-中序数组:inStart = inStart, inEnd = rootIndex-1
左子树-后序数组:postStart = postStart,postEnd = postStart+ (rootIndex-inStart)-1
这里我说明一下,(rootIndex-inStart)就是在计算左区间长度,在后序序列和中序序列,他们所代表的树的区间长度是一样的。所以通过起点+区间长度-1这种能确定右边界。
右子树-中序数组:inStart = rootIndex+1,inEnd = inEnd。
右子树-后序数组:postStart = postStart+(rootIndex-inStart),postEnd = postEnd-1。
下面是一个完整的过程
class Solution {
Map<Integer,Integer> inorderIndexMap = new HashMap<>();
//这个因为在递归中要频繁的用到,所以设置为全局变量。这样就没必要在递归的时候每次都传他。
int[] postorder;
public TreeNode buildTree(int[] inorder, int[] postorder) {
for(int i = 0;i<inorder.length;i++){
inorderIndexMap.put(inorder[i],i);
}
this.postorder = postorder;
//开始依据左右区间进行构建
return buildTreeHelper(0,inorder.length-1,0,postorder.length-1);
}
TreeNode buildTreeHelper(int inStart,int inEnd,int postStart,int postEnd){
//到最后肯定是递归到单个节点,但是在最后一个节点再来一次流程,必会导致end<start,对两个序列都是如此
if(inEnd < inStart || postEnd < postStart){
return null;
}
int rootValue = postorder[postEnd];
TreeNode root = new TreeNode(rootValue);
int rootIndexInorder = inorderIndexMap.get(rootValue);
int leftSubtreeSize = rootIndexInorder-inStart;
//递归构建左子树和右子树,注意是传区间,还是那句话,递归不要想复杂,尤其是往下递归这里。
//这里由于要构建左子树,所以传的就是中序和后序中代表左区间的边界
root.left = buildTreeHelper(inStart,rootIndexInorder-1,postStart,postStart+leftSubtreeSize-1);
//这里由于要构建右子树,所以传的就是中序和后序中代表右区间的边界
root.right = buildTreeHelper(rootIndexInorder+1,inEnd,postStart+leftSubtreeSize,postEnd-1);
//构造完了进行返回。
return root;
}
}
再次强调,我感觉这个区间很容易让人晕,一定要紧扣住原问题和子问题
想想往下的过程中,什么是原问题,什么是子问题。
个人总结:
做这个题的时候,感觉这种递归和平常的递归很不一样。在递下去的时候只是对树的节点值进行了定义,并没有直接把树构建起来。而是在归的过程中构建起来的。因此根据这个差异做个总结。
1.传统递归 vs. 这种方法:
传统递归:通常从根节点开始,然后递归地修改或构建子树。
这种方法:从底层开始构建,然后在回溯过程中将节点连接起来。
构建过程:
2.每次递归调用都创建一个新的节点。
这个节点的左右子树通过递归调用来构建。
节点之间的连接是在递归返回时完成的。
3.连接过程:
当一个递归调用返回时,它返回一个完整的子树(可能只是一个叶节点)。
父节点通过将这个返回的子树赋值给其 left 或 right 属性来建立连接。
4.自底向上的构建:
虽然代码看起来像是自顶向下的,但实际的构建过程是自底向上的。
最深层的叶节点首先被创建和返回。
然后,这些叶节点被用来构建它们的父节点,以此类推。
5.优点:
这种方法非常适合从遍历序列构建树,因为它允许我们在不需要额外数据结构的情况下,直接从给定的序列构建树。
它也很自然地符合递归的思想,每个子问题(子树的构建)都是原问题的一个较小版本。
105从前序和中序序列构造二叉树
思想与上面完全相同。
就是换成了前序,前序的特点就是根节点换到第一个来了。
所以上面总结性质那个图,前序的分布就是根,左子树,右子树。
所以进一步可以知道,题目的变化点主要在于,根节点的确定,从最后一个节点,变成了第一个节点。
然后区间的计算方式也变了
前序+中序的区间边界计算:
tips:中序的计算没变化
左子树:
中序数组:inStart = inStart, inEnd = rootIndex - 1
前序数组:preStart = preStart + 1, preEnd = preStart + (rootIndex - inStart)
说明:(rootIndex - inStart) 同样是计算左子树的长度。前序遍历中,根节点后紧跟左子树,所以左子树起始位置是 preStart + 1。(+1是因为第一个元素被取了,还有打括号的就是区间长度)
右子树:
中序数组:inStart = rootIndex + 1, inEnd = inEnd
前序数组:preStart = preStart + (rootIndex - inStart) + 1, preEnd = preEnd
要算是从总区间长度来考虑的。
class Solution {
private Map<Integer, Integer> inorderIndexMap;
private int[] preorder;
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
inorderIndexMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inorderIndexMap.put(inorder[i], i);
}
return buildTreeHelper(0, 0, inorder.length - 1);
}
private TreeNode buildTreeHelper(int preStart, int inStart, int inEnd) {
if (inStart > inEnd) return null;
int rootVal = preorder[preStart];
TreeNode root = new TreeNode(rootVal);
int rootIndex = inorderIndexMap.get(rootVal);
int leftSubtreeSize = rootIndex - inStart;
root.left = buildTreeHelper(preStart + 1, inStart, rootIndex - 1);
root.right = buildTreeHelper(preStart + leftSubtreeSize + 1, rootIndex + 1, inEnd);
return root;
}
}
654 最大二叉树
思路比较清晰。不断扫描区间最大值,然后递归左右子树,注意递归左右子树时取最大值的左右索引即可。
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
return build(nums,0,nums.length-1);
}
TreeNode build(int[] nums,int l,int r){
if(l>r){
return null;
}
//从区间起点开始扫描,找最大值
int idx = l;
//这样能防止第一个就是最大值而取不到的情况
for(int i = l;i<=r;i++){
if(nums[i]>nums[idx]){
idx = i;
}
}
TreeNode ans = new TreeNode(nums[idx]);
//递归左右子树。
ans.left = build(nums,l,idx-1);
ans.right = build(nums,idx+1,r);
return ans;
}