羊羊刷题笔记Day20/60 | 第六章 二叉树P6 | 654. 最大二叉树、617. 合并二叉树、700. 二叉搜索树中的搜索、98. 验证二叉搜索树

236 阅读4分钟

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;
  • 确定单层递归的逻辑

这里有三步工作

  1. 先要找到数组中最大的值对应的下标, 最大的值构造根节点,下标用来下一步分割数组。

代码如下:

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);
  1. 最大值所在的下标左区间 构造左子树

根据计算出来的maxIndex,确定左子树区间为[start,maxIndex) 代码如下:

root.left = getMax(nums, start, maxIndex);
  1. 最大值所在的下标右区间 构造右子树

根据计算出来的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 合并二叉树

递归

二叉树使用递归,就要想使用前中后哪种遍历方式? 本题使用哪种遍历都是可以的! 我们下面以前序遍历为例。动画如下: 那么我们来按照递归三部曲来解决:

  1. 确定递归函数的参数和返回值:

首先要合入两个二叉树,那么参数至少是要传入两个二叉树的根节点,返回值就是合并之后二叉树的根节点。 因此原方法即可,代码如下:

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;
  1. 确定单层递归的逻辑:

单层递归的逻辑就比较好写了,这里我们重复利用一下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;*/

递归法

  1. 确定递归函数的参数和返回值

递归函数的参数传入的就是根节点和要搜索的数值,返回的就是以这个搜索数值所在的节点。 因此使用原方法即可,代码如下:

public TreeNode searchBST(TreeNode root, int val) {}
  1. 确定终止条件

如果root为空,或者找到这个数值了,就返回root节点。

if (root == null || root.val == val){
    return root;
}
  1. 确定单层递归的逻辑

看看二叉搜索树的单层递归逻辑有何不同。 因为二叉搜索树的节点是有序的,所以可以有方向的去搜索。 如果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] image.png 节点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;
    }
}

最后这份代码看上去整洁一些,思路也清晰

迭代法看这里👈

学习资料:

654. 最大二叉树

617. 合并二叉树

700. 二叉搜索树中的搜索

98. 验证二叉搜索树