羊羊刷题笔记Day21/60 | 第六章 二叉树P7 | 530. 二叉搜索树的最小绝对差、501. 二叉搜索树中的众数、236 二叉树的最近公共祖先

141 阅读11分钟

530 二叉搜索树的最小绝对差

继续玩二叉搜索树

思路

题目中要求在二叉搜索树上任意两节点的差的绝对值的最小值。 注意是二叉搜索树,二叉搜索树可是有序的。 参照98 验证二叉搜索树,遇到在二叉搜索树上求最值、差值之类的,就把它想成在一个有序数组上求最值,求差值,这样就简单多了。

递归

那么二叉搜索树采用中序遍历,其实就是一个有序数组。

⭕如果本题是在一个有序数组上求两个数最小差值,这是不是就是一道送分题了?

最直观暴力的想法,就是把二叉搜索树转换成有序数组,然后遍历一遍数组,就统计出来最小差值了。 代码较简单,不展示.

但其实与98 验证二叉搜索树一样,在二叉搜素树中序遍历的过程中,我们就可以直接计算了。 只需要用一个pre节点记录一下cur节点的前一个节点。 如图: image.png 一些同学不知道在递归中如何记录前一个节点的指针,其实实现起来是很简单的,大家只要看过一次,写过一次,就掌握了。

思路:深度遍历。pre记录上一个节点,root记录当前节点。每次取最小值(与 root.val - pre.val )比较 代码如下:

class Solution {
    private int result = Integer.MAX_VALUE;
    private TreeNode pre;
    public int getMinimumDifference(TreeNode root) {

        if (root == null) return 0;

        traversal(root);

        return result;
    }

    private void traversal(TreeNode root) {
        // 终止条件
        if (root == null) return;

        // 单层递归逻辑 - 中序左右中(由于二叉搜素树 左中右依次递增)
        traversal(root.left); // 左
        if (pre != null) result = Math.min(result,root.val - pre.val); // 中
        pre = root;
        traversal(root.right); // 右
    }
}

迭代法请看这里👈

总结

规律:遇到在二叉搜索树上求什么最值,求差值之类的,都要思考一下二叉搜索树可是有序的,要利用好这一特点。 同时要学会在递归遍历的过程中如何记录前后两个指针,这也是一个小技巧,学会了还是很受用的。

501 二叉搜索树中的众数

继续用上一题pre cur指针的思想

思路

这道题目呢,递归法我从两个维度来讲。 首先如果是普通二叉树的话,应该怎么解题,是二叉搜索树,又应该如何解题,两种方式做一个比较,可以加深大家对二叉树的理解。

递归法

普通二叉树

如果不是二叉搜索树,最直观的方法一定是把这个树都遍历了,用map统计频率,把频率排个序,最后取前面高频的元素的集合。代码思路较清晰,不再展示。

二叉搜索树

既然是搜索树,它中序遍历就是有序的,是递增的数组 如图: image.png 中序遍历代码如下:

void searchBST(TreeNode cur) {
    if (cur == null) return ;
    searchBST(cur.left);       // 左
    (处理中间节点逻辑)                // 中
    searchBST(cur.right);      // 右
    return ;
}

遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。

两数对比操作问题

关键是在有序数组上的话,好搞,在树上怎么搞呢

这就考察对树的操作了。

在上一题 530. 二叉搜索树的最小绝对差 中我们就使用了pre指针和cur指针的技巧,这次又用上了。 而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素。

代码如下:

// 更新频数
if (pre == null || rootValue != pre.val) count = 1;
else count++;
// 更新pre节点
pre = root;

频数统计问题

此时又有问题了,因为要求最大频率的元素集合(注意是集合,不是一个元素,可以有多个众数),如果是数组上大家一般怎么办?

如果是普通二叉树:应该是先遍历一遍数组,找出最大频率(maxCount),然后再重新遍历一遍数组把出现频率为maxCount的元素放进集合(因为众数有多个)。这种方式遍历了两遍数组。

但这里其实只需要遍历一次就可以找到所有的众数。

如果 频率count 等于 maxCount(最大频率),我们就清空原来的list,把这个元素加入到结果集中(以下代码为result数组),并更新频数MaxCount代码如下:

// 更新结果
if (count > maxCount) {
    // 有更大频次的众数
    maxCount = count; // 更新频数
    list.clear(); // 清空集合list
    list.add(rootValue);
} else if (count == maxCount) {
    // 记录相同频次
    list.add(rootValue);
}

以上,两个较为困难以及关键问题解决了,完整代码如下:(只需要遍历一遍二叉搜索树,就求出了众数的集合

class Solution {
    private ArrayList<Integer> list;
    private TreeNode pre;
    private int count;
    private int maxCount;

    public int[] findMode(TreeNode root) {
        // 初始化值
        count = 0;
        pre = null;
        list = new ArrayList<>();
        maxCount = 0;

        findNum(root);

        // 转换
        int[] arr = new int[list.size()];
        for (int i = 0; i < list.size(); i++) {
            arr[i] = list.get(i);
        }

        return arr;
    }

    private void findNum(TreeNode root) {
        // 终止条件
        if (root == null) return;

        // 单层递归逻辑 中序
        findNum(root.left);// 左

        // 中
        int rootValue = root.val;
        // 更新频数
        if (pre == null || rootValue != pre.val) count = 1;
        else count++;
        // 更新结果
        if (count > maxCount) {
            // 有更大频次的众数
            maxCount = count;
            list.clear();
            list.add(rootValue);
        } else if (count == maxCount) {
            // 记录相同频次
            list.add(rootValue);
        }
        // 更新pre节点
        pre = root;

        findNum(root.right); // 右
    }
}

迭代法看这里👈,(大概是中序遍历的模板加上递归法中间节点的处理逻辑)

总结

本题在递归法中,我给出了如果是普通二叉树,应该怎么求众数。 知道了普通二叉树的做法时候,我再进一步给出二叉搜索树又应该怎么求众数,这样鲜明的对比,相信会对二叉树又有更深层次的理解了。 在递归遍历二叉搜索树的过程中,我还介绍了一个统计最高出现频率元素集合的技巧, 要不然就要遍历两次二叉搜索树才能把这个最高出现频率元素的集合求出来。 我们需要尽量从各个角度对本题进剖析,更深入理解二叉树

236 二叉树的最近公共祖先

本题对理解回溯以及递归返回值特别有帮助!

思路

遇到这个题目首先想的是要是能自底向上查找就好了,因此选用后序遍历。(原因详见104 二叉树的最大深度) 接下来就看如何判断一个节点是节点q和节点p的公共祖先呢。

情况1:

首先最容易想到的一个情况:如果找到一个节点,[发现左子树出现结点p,右子树出现节点q],或者反过来 [左子树出现结点q,右子树出现节点p]那么该节点就是节点p和q的最近公共祖先。 即情况一: image.png 判断逻辑是 如果递归遍历遇到q,就将q返回,遇到p 就将p返回,那么如果 左右子树的返回值都不为空,说明此时的中节点,一定是q 和p 的最近祖先。

情况2:(大多数会忽略)

节点本身p(q),它拥有一个子孙节点q(p)。 情况二: image.png 其实情况一 和 情况二 代码实现过程都是一样的,也可以说,实现情况一的逻辑,顺便包含了情况二。 因为遇到 q 或者 p 就返回,这样也包含了 q 或者 p 本身就是 公共祖先的情况。 以上字面意思可能比较抽象,晦涩难懂,结合下面的代码理解会更加清晰

递归法

递归三部曲:

  • 确定递归函数返回值以及参数

可以利用上题目中返回值是TreeNode ,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。 代码如下:

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {}
  • 确定终止条件

遇到空的话,因为树都是空了,所以返回空。

那么我们来说一说,如果 root == q,或者 root == p,说明找到 q p ,则将其返回,这个返回值,后面在中节点的处理过程中会用到,那么中节点的处理逻辑

代码如下:

// 终止条件 - 空节点 以及 找到p q
if (root == null) return null;
if (root == p || root == q) return root;
  • 确定单层递归逻辑

值得注意的是 本题函数有返回值,回溯的就是返回值,回溯的过程需要递归函数的返回值做判断,但本题我们依然要遍历树的所有节点。

⭕而在112 路径总和中说了 递归函数有返回值就是要遍历某一条边,但有返回值也要看如何处理返回值

如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢? 搜索一条边的写法:

if (递归函数(root.left)) return ;

if (递归函数(root.right)) return ;

搜索整个树写法:

left = 递归函数(root.left);  // 左
right = 递归函数(root.right); // 右
left与right的逻辑处理;         // 中

⭕在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)简单来说,如果一条边满足条件则马上返回。而需要结合left 和 right 返回结果进行进一步返回判断的则是搜索整个书的。 那么为什么要遍历整棵树呢?直观上来看,找到最近公共祖先,直接一路返回就可以了。 如图: image.png 就像图中一样直接返回7,多美滋滋。 但事实上还要遍历根节点右子树(即使此时已经找到了目标节点了),也就是图中的节点4、15、20。 因为在如下代码的后序遍历中,如果想利用left和right做逻辑处理, 不能立刻返回,而是要等left与right逻辑处理完之后才能返回

left = 递归函数(root.left);  // 左
right = 递归函数(root.right); // 右
left与right的逻辑处理;         // 中

所以此时大家要知道我们要遍历整棵树。知道这一点,对本题就有一定深度的理解了。

进一步理解:既然还会访问 4、15、20等节点,那么这个算法还具有扩展性,还可以处理除了p q外多个节点的公共祖先

那么先用left和right接住左子树和右子树的返回值,代码如下:

TreeNode left = lowestCommonAncestor(root->left, p, q);
TreeNode right = lowestCommonAncestor(root->right, p, q);

如果left 和 right都不为空,说明此时root就是最近公共节点。这个比较好理解 如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然。 这里有的同学就理解不了了,为什么left为空,right不为空,目标节点通过right返回呢? 如图: image.png 图中节点10的左子树返回null,右子树返回目标值7,那么此时节点10的处理逻辑就是把右子树的返回值(最近公共祖先7)返回上去!

这里需要搞清楚结果究竟是如何从底层一层一层传到头结点的。

那么如果left和right都为空,则返回left或者right都是可以的,也就是返回空。

代码如下:

// 左右子节点都有返回(遇见了p q)
if (left != null && right != null) return root;
    // 左子树为空 - 返回右子树的值
else if (left == null && right != null) return right;
    // 右子树为空 - 返回左子树的值
else if (left != null && right == null) return left;
    // 左右子树为空 - 没有遇见p q - 返回null
else return null;

那么寻找最小公共祖先,完整流程图如下: image.png 从图中,大家可以看到,我们是如何回溯遍历整棵二叉树,将结果返回给头结点的! 整体代码如下:

🔴总结

这道题目需要真正了解这里面回溯的过程,以及结果是如何一层一层传上去的那么我给大家归纳如下三点

  1. 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从底向上的遍历方式。
  2. 在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。
  3. 要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。

这里每一步都是有难度的,都需要对二叉树,递归和回溯有一定的理解。 本题没有迭代法,因为迭代法不适合模拟回溯的过程。理解递归的解法就够了。

学习资料:

530 二叉搜索树的最小绝对差

510 二叉搜索树中的众数

236 二叉树的最近公共祖先