前四天都在深度优先、广度优先遍历以及各种扩展以及花活遍历二叉树。今天开始构造二叉树了hhh
513 找树左下角的值
递归
咋眼一看,这道题目用递归的话就就一直向左遍历,最后一个就是答案呗? 我们来分析一下题目:在树的最后一行找到最左边的值。
❗❗❗注意:没有这么简单,一直向左遍历到最后一个,它未必是最后一行。
首先要是最后一行,然后是最左边的值。
如果使用递归法,如何判断是最后一行呢,其实就是深度最大的叶子节点一定是最后一行。
如果对二叉树深度和高度还有点疑惑的话,请看:这篇文章的110.平衡二叉树。
所以要找深度最大的叶子节点。
那么如何找最左边的呢?可以使用前序遍历(当然中序,后序都可以,因为本题没有 中间节点的处理逻辑,只要左优先就行),保证优先左边搜索,然后记录深度最大的叶子节点,此时就是树的最后一行最左边的值。 递归三部曲:
- 确定递归函数的参数和返回值
参数必须有要遍历的树的根节点,还有就是一个int型的变量用来记录最长深度。 这里就不需要返回值了,所以递归函数的返回类型为void。 本题还需要类里的两个全局变量,maxLen用来记录最大深度,result记录最大深度最左节点的数值。 代码如下:
private int maxDepth = Integer.MIN_VALUE;
private int value = 0;
private void findLeftValue(TreeNode root,int deep){}
- 确定终止条件
当遇到叶子节点的时候,就需要统计一下最大的深度,所以需要遇到叶子节点来更新最大深度。 代码如下:
if (root == null) return;
// 终止条件:遇到空节点 - 记录深度
if (root.left == null && root.right == null){
if (deep > maxDepth){
value = root.val;
maxDepth = deep;
}
}
- 确定单层递归的逻辑
在找最大深度的时候,递归的过程中依然要使用回溯,代码如下:
// 单层遍历逻辑 - 左右(中不需要处理)遍历,左遍历完需要回溯,继续遍历右节点
if (root.left != null) {
deep++;
findLeftValue(root.left,deep);
deep--;
}
if (root.right != null){
deep++;
findLeftValue(root.right,deep);
deep--;
}
完整代码如下:
class Solution {
private int maxDepth = Integer.MIN_VALUE;
private int value = 0;
public int findBottomLeftValue(TreeNode root) {
value = root.val;
findLeftValue(root,0);
return value;
}
private void findLeftValue(TreeNode root,int deep){
if (root == null) return;
// 终止条件:遇到空节点 - 记录深度
if (root.left == null && root.right == null){
if (deep > maxDepth){
value = root.val;
maxDepth = deep;
}
}
// 单层遍历逻辑 - 左右(中不需要处理)遍历,左遍历完需要回溯,继续遍历右节点
if (root.left != null) {
deep++;
findLeftValue(root.left,deep);
deep--;
}
if (root.right != null){
deep++;
findLeftValue(root.right,deep);
deep--;
}
}
}
112 路径总和
递归
由于涉及到路径,因此可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树
- 确定递归函数的参数和返回类型
参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。 返回值:**递归函数什么时候需要返回值?什么时候不需要返回值?**这里总结如下三点:
- 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
- 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先中)
- 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)
而本题符合第三种情况,我们要找一条符合条件的路径,所以递归函数需要返回值,及时返回,那么返回类型是什么呢?
如图所示:
图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用Boolean类型表示。
所以代码如下:
private boolean traversal(TreeNode root, Integer count){}
- 确定终止条件
首先计数器如何统计这一条路径的和呢? 可以用累加也可以用递减,让计数器count初始为0 / 目标和,然后每次加上 / 减去遍历路径节点上的数值。 如果最后count == targetSum / 0,同时到了叶子节点的话,说明找到了目标和。 如果遍历到了叶子节点,count不为targetSum / 0,就是没找到。 递归终止条件代码如下:
// 终止条件 - 遇到根节点 - 判断count是否为零
if (root.left == null && root.right == null && count == 0) return true;
- 确定单层递归的逻辑
因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。 递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。 同时递归出来后要进行回溯,以便遍历右节点。 代码如下:
// 单层循环逻辑 - 左右
if (root.left != null) {
// 向下遍历左节点,并减去当前节点的值
count -= root.left.val;
// 如果子节点返回true 则一层层向上反馈
if (traversal(root.left, count)) return true;
// 回溯 - 遍历右节点
count += root.left.val;
}
if (root.right != null) {
// 向下遍历左节点,并减去当前节点的值
count -= root.right.val;
// 如果子节点返回true 则一层层向上反馈
if (traversal(root.right, count)) return true;
// 回溯 - 这里可选择性写,这里不需要回溯
count += root.right.val;
}
// 如果到了子节点 count还是没减完,返回false
return false;
整体代码如下:
累加判断是否为目标和:
private int count;
private int sum;
public boolean hasPathSum(TreeNode root, int targetSum) {
count = 0;
sum = targetSum;
if (root == null) return false;
// 先预先加上根节点的值
return traversal(root, root.val);
}
private boolean traversal(TreeNode root, Integer count){
// 终止条件 - 遇到根节点 - 判断count是否为targetSum
if (root.left == null && root.right == null && count == sum) return true;
// 单层循环逻辑 - 左右
if (root.left != null) {
// 向下遍历左节点,并加去当前节点的值
count += root.left.val;
// 如果子节点返回true 则一层层向上反馈
if (traversal(root.left, count)) return true;
// 回溯 - 遍历右节点
count -= root.left.val;
}
if (root.right != null) {
// 向下遍历左节点,并加去当前节点的值
count += root.right.val;
// 如果子节点返回true 则一层层向上反馈
if (traversal(root.right, count)) return true;
// 回溯 - 这里可选择性写,这里不需要回溯
count += root.right.val;
}
// 如果到了子节点 count还是不等于targetSum,返回false
return false;
}
递减判断是否为目标和(与递增差不多):
class Solution {
private int count;
public boolean hasPathSum(TreeNode root, int targetSum) {
count = targetSum;
if (root == null) return false;
// 先减去根节点的值
return traversal(root, count - root.val);
}
private boolean traversal(TreeNode root, Integer count){
// 终止条件 - 遇到根节点 - 判断count是否为零
if (root.left == null && root.right == null && count == 0) return true;
// 单层循环逻辑 - 左右
if (root.left != null) {
// 向下遍历左节点,并减去当前节点的值
count -= root.left.val;
// 如果子节点返回true 则一层层向上反馈
if (traversal(root.left, count)) return true;
// 回溯 - 遍历右节点
count += root.left.val;
}
if (root.right != null) {
// 向下遍历左节点,并减去当前节点的值
count -= root.right.val;
// 如果子节点返回true 则一层层向上反馈
if (traversal(root.right, count)) return true;
// 回溯 - 这里可选择性写,这里不需要回溯
count += root.right.val;
}
// 如果到了子节点 count还是没减完,返回false
return false;
}
}
106 从中序与后序遍历序列构造二叉树
思路
首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点(该点为根节点),先切中序数组,分为了左右子树,根据这左右子树,反过来再切后序数组。切完后,后序左右子树部分最后一个元素也是左右节点........以此一层一层切下去,每次后序数组最后一个元素就是节点元素。
如果让我们肉眼看两个序列,画一棵二叉树的话,应该分分钟都可以画出来。
流程如图:
那么代码应该怎么写呢?
说到一层一层切割,就应该想到了递归。
来看一下一共分几步:
- 第一步:如果数组大小为零的话,说明是空节点了。
- 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
- 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
- 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
- 第五步:切割后序数组,切成后序左数组和后序右数组
- 第六步:递归处理左区间和右区间
不难写出如下代码:
class Solution {
private Map<Integer,Integer> map;
public TreeNode buildTree(int[] inorder, int[] postorder) {
map = new HashMap<>();
// 用map保存中序序列的数值对应位置
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i],i);
}
// 返回值
return findNode(inorder,0,inorder.length,postorder,0,postorder.length);
}
/**
* 参数1-3:前序数组,前序首坐标,尾坐标
* 参数4-6:后序数组,后序首坐标,尾坐标
*/
public TreeNode findNode(int[] inorder, int inBegin, int inEnd, int[] postorder, int postBegin, int postEnd) {
// 参数里的范围都是前闭后开
// 不满足左闭右开,说明没有元素,返回空树
if (inBegin >= inEnd || postBegin >= postEnd) return null;
// 找到后序遍历的最后一个元素(根节点)在中序遍历中的位置
Integer rootIndex = map.get(postorder[postEnd - 1]);
// 构建树 - 构造结点
TreeNode root = new TreeNode(inorder[rootIndex]);
int leftEnd = rootIndex - inBegin;
// 有了根节点在中序中已知左右子树,记录中序左子树个数,用来确定后序数列的个数
// 将左右子树当成一棵树继续递归
root.left = findNode(inorder, inBegin, rootIndex, postorder, postBegin, postBegin + leftEnd);
// 这里后序遍历最后节点 - 1,由于最后节点已被提出来是根节点
root.right = findNode(inorder,rootIndex + 1,inEnd,postorder,postBegin + leftEnd,postEnd - 1);
// 返回根节点
return root;
}
}
🔴本题难点:
**❗❗❗注意:本题难点难点,就是如何切割,以及边界值。**找不好很容易乱套。
- 切割标准以及切割中序数组
此时应该注意确定切割的标准,是左闭右开,还有左开右闭,还是左闭右闭,这个就是不变量,要在递归中保持这个不变量。 在切割的过程中会产生四个区间,把握不好不变量的话,一会左闭右开,一会左闭右闭,必然乱套! 在第一天的二分查找中领略了循环不变量的重要性,在二分查找以及之后的螺旋矩阵的求解中,坚持循环不变量非常重要,本题也是。 首先要切割中序数组,为什么先切割中序数组呢? 切割点在后序数组的最后一个元素,就是用这个元素来切割中序数组分成左右子树的,所以必要先切割中序数组。
root.left = findNode(inorder, inBegin, rootIndex, postorder, postBegin, postBegin + leftEnd);
- 切割后序数组
首先后序数组的最后一个元素指定不能要了,这是切割点 也是 当前二叉树中间节点的元素,已经用了。
后序数组的切割点怎么找?
此时有一个很重的点,就是中序数组中左子树大小一定是和后序数组的左子树大小相同的(这是必然)。
因此后序的左子树长度等于中序左子树长度,而后序右子树起点为postBegin + leftEnd,终点就是postEnd - 1(注意此处不计最后一个节点了,因为他是根节点)
root.right = findNode(inorder,rootIndex + 1,inEnd,postorder,postBegin + leftEnd,postEnd - 1);
总结
之前的二叉树题目都是各种遍历二叉树,这次开始构造二叉树了,加上回溯的思想。思路其实比较简单,但是真正代码实现出来并不容易。 今天的三题自己运行也需要通过多次打印,在关键点中输出变量,不断调试。(依旧清晰记得,112 路径之和由于add(list) 以及 add(new Arraylist(list))的区别调试了近一个小时,不过最后搞懂之后豁然开朗!) 最后一题根据算法原理,真正了解了为什么后序和中序以及中序和前序可以唯一确定一棵二叉树,而前序和后序却不行。(前序后序都只给了根节点信息,没有中序左右子树的信息)
学习资料: