654 最大二叉树
本题与106. 从中序与后序遍历序列构造二叉树有点相似,都涉及到对树的切割以及左右子树下标判定(此处一定要设定一个区间定义❗ 左闭右开 or 左闭右闭 详见704. 二分查找初次遇到)
最大二叉树的构建过程如下:
思路:找最大值,最大值左边为左子树,右边为右子树。以此思路,找左右子树的最大值继续分左右子树...
构造树一般采用的是前序遍历(从上到下),因为先构造中间节点,然后递归构造左子树和右子树。
- 确定递归函数的参数和返回值
参数传入的是存放元素的数组,返回该数组构造的二叉树的头结点,返回类型是指向节点的指针。 代码如下:
public TreeNode getMax(int[] nums,int start,int end){}
- 确定终止条件
设置约定:遵守左闭右开原则 题目中说了输入的数组大小一定是大于等于1的,所以我们不用考虑小于1的情况,那么当递归遍历的时候,**如果传入的数组大小为1,说明遍历到了叶子节点了。**返回该节点即可 代码如下:
// [1,2) - 还有一个元素1
if (end == start + 1) return new TreeNode(nums[start]);
// [1,1.x] (不可能情况) - 没有元素
if (end - start < 1 ) return null;
- 确定单层递归的逻辑
这里有三步工作
- 先要找到数组中最大的值和对应的下标, 最大的值构造根节点,下标用来下一步分割数组。
代码如下:
int maxIndex = start;
int maxValue = nums[maxIndex];
// 取最大值作根节点
for (int i = start + 1; i < end; i++) {
if (maxValue < nums[i]){
maxIndex = i;
maxValue = nums[i];
}
}
TreeNode root = new TreeNode(maxValue);
- 最大值所在的下标左区间 构造左子树
根据计算出来的maxIndex,确定左子树区间为[start,maxIndex) 代码如下:
root.left = getMax(nums, start, maxIndex);
- 最大值所在的下标右区间 构造右子树
根据计算出来的maxIndex,确定左子树区间为[maxIndex + 1, end) 代码如下:
root.right = getMax(nums, maxIndex + 1, end);
因此整体代码如下:
public TreeNode getMax(int[] nums,int start,int end){
// [1,2) - 还有一个元素1
if (end == start + 1) return new TreeNode(nums[start]);
// [1,1.x] (不可能情况) - 没有元素
if (end - start < 1 ) return null;
int maxIndex = start;
int maxValue = nums[maxIndex];
// 取最大值作根节点
for (int i = start + 1; i < end; i++) {
if (maxValue < nums[i]){
maxIndex = i;
maxValue = nums[i];
}
}
TreeNode root = new TreeNode(maxValue);
// 以此点为分割,分割左右子树
root.left = getMax(nums, start, maxIndex);
root.right = getMax(nums, maxIndex + 1, end);
return root;
}
617 合并二叉树
递归
二叉树使用递归,就要想使用前中后哪种遍历方式?
本题使用哪种遍历都是可以的!
我们下面以前序遍历为例。动画如下:
那么我们来按照递归三部曲来解决:
- 确定递归函数的参数和返回值:
首先要合入两个二叉树,那么参数至少是要传入两个二叉树的根节点,返回值就是合并之后二叉树的根节点。 因此原方法即可,代码如下:
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {}
2. 确定终止条件:
因为是传入了两个树,那么就有两个树遍历的节点t1 和 t2,如果t1 == NULL 了,两个树合并就应该是 t2 了(如果t2也为NULL也无所谓,返回的还是NULL)。 反过来如果t2 == NULL,那么两个数合并就是t1(同理如果t1也为NULL也无所谓,返回还是NULL)。 代码如下:
// 终止条件 - 左空返右 右空返左 (也包含了两个都为空时返回空)
if (root1 == null) return root2;
if (root2 == null) return root1;
- 确定单层递归的逻辑:
单层递归的逻辑就比较好写了,这里我们重复利用一下t1这个树,t1就是合并之后树的根节点(就是修改了原来树的结构)。 那么单层递归中,就要把两棵树的元素加到一起。 代码如下:
// 单层递归逻辑 前中右好理解(在tree1上直接修改)
root1.val += root2.val;
root1.left = mergeTrees(root1.left,root2.left);
root1.right = mergeTrees(root1.right,root2.right);
return root1;
这里用了前序遍历,比较符合常规思维。当然中的处理逻辑在哪都可以,结果是一样的。
此时前序遍历,完整代码就写出来了,如下:
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
// 终止条件 - 左空返右 右空返左 (也包含了两个都为空时返回空)
if (root1 == null) return root2;
if (root2 == null) return root1;
// 单层递归逻辑 前中右好理解(在tree1上直接修改)
root1.val += root2.val;
root1.left = mergeTrees(root1.left,root2.left);
root1.right = mergeTrees(root1.right,root2.right);
return root1;
}
迭代法见这里👈
总结
合并二叉树,也是二叉树操作的经典题目,需要熟练同时操作两个二叉树。 其实之前也有类似两棵树的操作,在101. 对称二叉树中也试过。当时是对比左右子树各种情况,大小是否相等或是否都为空。
700 二叉搜索树中的搜索
接下来不再是普通二叉树了,二叉搜索树登场🎇!
思路
回顾定义:二叉搜索树是一个有序树:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉搜索树
这就决定了,二叉搜索树,递归遍历和迭代遍历和普通二叉树都不一样。 本题,其实就是在二叉搜索树中搜索一个节点。来看看应该如何遍历。
普通二叉树
如果本题是普通二叉树,层序遍历,普通递归都可以完成本题,然而没利用到二叉搜索树的定义,效率较低。思路较简单,不赘述,给出代码: 层序遍历:(广度优先遍历)
// 层序遍历
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()){
int size = queue.size();
while (size-- != 0){
TreeNode node = queue.poll();
// 找到值
if (node.val == val){
return node;
}
if (node.left != null) queue.add(node.left);
if (node.right != null) queue.add(node.right);
}
}
return null;
迭代法:(深度优先遍历 - 中左右)
// 用栈模拟递归 - 中左右
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode pop = stack.pop();// 中
// 增加条件:当节点值相等时返回
if (pop.val == val) {
return pop;
}
// 右左孩子进栈
if (pop.right != null) {
stack.push(pop.right);// 右先进栈
}
if (pop.left != null) {
stack.push(pop.left); // 左
}
}
return null;*/
递归法
- 确定递归函数的参数和返回值
递归函数的参数传入的就是根节点和要搜索的数值,返回的就是以这个搜索数值所在的节点。 因此使用原方法即可,代码如下:
public TreeNode searchBST(TreeNode root, int val) {}
- 确定终止条件
如果root为空,或者找到这个数值了,就返回root节点。
if (root == null || root.val == val){
return root;
}
- 确定单层递归的逻辑
看看二叉搜索树的单层递归逻辑有何不同。 因为二叉搜索树的节点是有序的,所以可以有方向的去搜索。 如果root->val > val,搜索左子树,如果root->val < val,就搜索右子树,最后递归结束如果都没有搜索到,就返回NULL。 代码如下:
// 二叉搜索树性质:左子节点比根节点小,右子节点比根节点大
if (val > root.val) return searchBST(root.right,val);
else return searchBST(root.left,val);*/
由于返回值是本题答案,所以递归时需要接受返回的值然后进行返回 例如:result = searchBST(root->left, val)。(我直接写了return 返回值) 整体代码如下:
// 方法2:利用二叉搜索树的性质
if (root == null || root.val == val){
return root;
}
// 二叉搜索树性质:左子节点比根节点小,右子节点比根节点大
if (val > root.val) return searchBST(root.right,val);
else return searchBST(root.left,val);
迭代法优化:
在普通二叉树中的迭代法基础上,运用二叉搜索树的性质,简化操作以及代码:
// 优化迭代法:利用二叉搜索树的性质 不需要借助栈
while (root != null) {
if (val < root.val) root = root.left;
else if (val > root.val) root = root.right;
else return root;
}
return null;
(由于二叉搜索树的性质特点,层序遍历即广度优先算法不适合用其性质进行优化)
98 验证二叉搜索树
思路
要知道中序遍历(左 中 右 - 以此递增)下,输出的二叉搜索树节点的数值是有序序列。 有了这个特性,验证二叉搜索树,就相当于变成了判断一个序列是不是递增的了。
递归法
- 陷阱1
不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了。 写出了类似这样的代码: (根节点 大于 左节点的值 且 根节点 小于 右节点的值)
if (root.val > root.left.val && root.val < root.right.val) {
return true;
} else {
return false;
}
我们要比较的是 左子树所有节点小于中间节点,右子树所有节点大于中间节点。所以以上代码的判断逻辑是错误的。
举个反例: [10,5,15,null,null,6,20]
节点10大于左节点5,小于右节点15,但右子树里出现了一个6,比10小,这就不符合了!
- 陷阱2
样例中最小节点 可能是int的最小值,如果这样使用最小的int来比较也是不行的。 此时可以初始化比较元素为Long的最小值。 问题可以进一步演进:如果样例中根节点的val 可能是Long的最小值 又要怎么办呢?文中会解答。 了解这些陷阱之后我们来看一下代码应该怎么写: 递归三部曲:
- 确定递归函数,返回值以及参数
要定义一个Long的全局变量,用来比较遍历的节点是否有序,因为后台测试数据中有int最小值,所以定义为Long的类型,初始化为Long最小值。 注意:递归函数要有bool类型的返回值。在112 路径总和中遇见过,只有寻找某一条边(或者一个节点)的时候,递归函数会有bool类型的返回值。 其实本题是同样的道理,我们在寻找一个不符合条件的节点,如果没有找到这个节点就遍历了整个树,如果找到不符合的节点了,立刻返回。 代码如下:
long maxvalue = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
- 确定终止条件
递归到子节点返回true 代码如下:
// 终止条件 空节点
if (root == null) return true;
- 确定单层递归的逻辑
中序遍历,一直更新maxVal,一旦发现maxVal >= root->val,就返回false,注意元素相等时候也要返回false。 代码如下:
// 单层递归逻辑 中序 左中右(深度遍历)
boolean left = isValidBST(root.left); // 左
// 如果是二叉搜索树,数值递增maxvalue < root.val 否则(大于或等于)则不是二叉搜索树
if (maxvalue < root.val) maxvalue = root.val;
else return false;
boolean right = isValidBST(root.right);
return left && right;
整体代码如下:
class Solution {
// 最大值 随着递归增加 因此为成员变量
long maxvalue = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
// 终止条件 空节点
if (root == null) return true;
// 单层递归逻辑 中序 左中右(深度遍历)
boolean left = isValidBST(root.left); // 左
// 如果是二叉搜索树,数值递增maxvalue < root.val 否则则不是二叉搜索树
if (maxvalue < root.val) maxvalue = root.val;
else return false;
boolean right = isValidBST(root.right);
return left && right;
}
}
另外,如果测试数据中有 Long的最小值,怎么办? 不可能在初始化一个更小的值了吧。** 建议避免 初始化最小值**,如下方法取到最左面节点的数值来比较。 代码如下:
class Solution {
// 递归
TreeNode max;
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
// 左
boolean left = isValidBST(root.left);
if (!left) {
return false;
}
// 中 - 替代了之前用最小值比较
if (max != null && root.val <= max.val) {
return false;
}
// 记录此时root 顺序(左 中 右)
max = root;
// 右
boolean right = isValidBST(root.right);
return right;
}
}
最后这份代码看上去整洁一些,思路也清晰
迭代法看这里👈
学习资料: