【代码随想录训练营第42期 Day18打卡 二叉树Part6-LeetCode 530.二叉搜索树的最小绝对差 501.二叉搜索树中的众数 236. 二叉树的最近公共祖先

目录

一、做题心得

二、题目与题解

题目一:530.二叉搜索树的最小绝对差

题目链接

题解:中序遍历(DFS)

题目二:501.二叉搜索树中的众数

题目链接

题解1:层序遍历(BFS) + 哈希

题解2:中序遍历(DFS)+ 哈希

题目三:236. 二叉树的最近公共祖先

题目链接

题解:递归(后序遍历思想)

三、小结


一、做题心得

今天是代码随想录打卡的第18天,来到了二叉树章节的part6。今天的题目,依旧是同递归练习很大。果然,递归作为通往二叉树世界的钥匙,当真作用巨大。说说几天做题的感受吧,前两道和之前打卡的题有些许关联,再次应用到了二叉搜索树的性质--中序遍历的有序性,难度整体来说不大,第三题的话,个人感觉还是比较新的,没做出来,但看了题解之后还是蛮清晰的,运用到的思想我们其实也知道,但是不太容易用上。

好了,话不多说,直接开始今天的题目。

二、题目与题解

题目一:530.二叉搜索树的最小绝对差

题目链接

530. 二叉搜索树的最小绝对差 - 力扣(LeetCode)

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。

差值是一个正数,其数值等于两值之差的绝对值。

示例 1:

输入:root = [4,2,6,1,3]
输出:1

示例 2:

输入:root = [1,0,48,null,null,12,49]
输出:1

提示:

  • 树中节点的数目范围是 [2, 104]
  • 0 <= Node.val <= 105
题解:中序遍历(DFS)

这个题和昨天打卡的 98. 验证二叉搜索树 - 力扣(LeetCode)有一些相似,都是考察二叉搜索树的性质--通过中序遍历实现从小到大的排序,这个昨天已经有提到了,因为中序遍历按照左根右的顺序对节点进行遍历,而二叉搜索树节点的值恰好满足左节点<根节点<右节点。

这个题要求我们得到最小的绝对差,而绝对差的最小值肯定是在排完序后的相邻节点中出现,这时我们只需要不断比较记录下最小的值即可。实现比较相邻节点的关键就是先驱节点pre的设置。

代码如下:

class Solution {
public:
    int ans = INT_MAX;                //初始化结果为最大int型值(尽可能大)
    TreeNode* pre = nullptr;            //初始化pre节点为空,存储当前节点的前一个节点

    void dfs(TreeNode* cur) {           //中序遍历(递归实现),cur为当前节点
    if (cur == nullptr)     return;
    dfs(cur->left);                   //左
    if (pre != nullptr) {            //中
        ans = min(ans, cur->val - pre->val);        //更新最小差值
    }
    pre = cur;             //更新pre位置,往后移动一位(即移到了当前位置)
    dfs(cur->right);            //右
}

    int getMinimumDifference(TreeNode* root) {
        dfs(root);
        return ans;
    }
};

题目二:501.二叉搜索树中的众数

题目链接

501. 二叉搜索树中的众数 - 力扣(LeetCode)

给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。

如果树中有不止一个众数,可以按 任意顺序 返回。

假定 BST 满足如下定义:

  • 结点左子树中所含节点的值 小于等于 当前节点的值
  • 结点右子树中所含节点的值 大于等于 当前节点的值
  • 左子树和右子树都是二叉搜索树

示例 1:

输入:root = [1,null,2,2]
输出:[2]

示例 2:

输入:root = [0]
输出:[0]

提示:

  • 树中节点的数目在范围 [1, 104] 内
  • -105 <= Node.val <= 105
题解1:层序遍历(BFS) + 哈希

这里是作用于所有二叉树的通解--其实哪种遍历方式都行,只是我更习惯层序遍历。

思路很简单,就是挨个遍历每个节点,然后用哈希表存储每个节点的值以及该值出现的次数。出现次数最多的值自然就是题目要求的众数,这时候只需要用一个数组存储结果即可。

代码如下:

class Solution {
public:
    vector<int> findMode(TreeNode* root) {
        unordered_map<int,int> hash;        //哈希表:key存储节点的值(first),value存储为该节点值的节点的个数(second)
        vector<int> ans;        //存储结果
        if (root == nullptr)    return ans;
        queue<TreeNode*> q;
        q.push(root);
        while (!q.empty()) {                //层序遍历(BFS)
            TreeNode* node = q.front();
            q.pop();
            hash[node->val]++;
            if (node->left)     q.push(node->left);
            if (node->right)    q.push(node->right);
        }
        int Max = 0;        //记录众数(出现频率最高的节点值)出现的次数,初始化为0
        for (auto i : hash) {           //注意是这里用auto,因为hash的元素的特殊性
            Max = max(Max,i.second);        
        }
        for (auto i : hash) {
            if(i.second == Max)           //和众数出现的次数相同
                ans.push_back(i.first);        //添加众数到结果
        }
        return ans;
    }
};
题解2:中序遍历(DFS)+ 哈希

由于这个题已经要求是二叉搜索树了,所以可以用上述中序遍历的解法。这样排序之后,重复的数字就会集中在一起,就不需要用哈希表来单独存储,效率更高。每次出现更大众数的时候,之前的众数清空。

代码如下:

class Solution {
public:
    int max_cnt = 0;            // 最大频次
    int cnt = 0;                // 当前元素出现频次
    TreeNode* pre = nullptr;    // 前驱节点,初始化为空(前驱节点是后续进行相邻元素大小判断的关键)
    vector<int> ans;            // 保存众数

    void dfs(TreeNode* root) {              // 中序遍历--作用相当于排序:从小到大排序
        if (root == nullptr) return;
        dfs(root->left);                    // 左
        if (pre && pre->val == root->val)
            cnt++;      // 与前驱相同,频次++  
        else
            cnt = 1;    // 是没前驱的首个节点,或非重复元素,频次初始化
        if (cnt == max_cnt)     // 成为了众数之一               // 中
            ans.push_back(root->val);
        if (cnt > max_cnt)          // 频次突破新高,成为众数
        {
            ans.clear();            // 关键:之前的众数无效
            max_cnt = cnt;
            ans.push_back(root->val);
        }        
        pre = root;     // 更新前驱
        dfs(root->right);           // 右
    }

    vector<int> findMode(TreeNode* root) 
    {
        dfs(root);
        return ans;
    }
};

题目三:236. 二叉树的最近公共祖先

题目链接

236. 二叉树的最近公共祖先 - 力扣(LeetCode)

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

示例 1:

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。

示例 2:

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。

示例 3:

输入:root = [1,2], p = 1, q = 2
输出:1

提示:

  • 树中节点数目在范围 [2, 105] 内。
  • -109 <= Node.val <= 109
  • 所有 Node.val 互不相同 。
  • p != q
  • p 和 q 均存在于给定的二叉树中。
题解:递归(后序遍历思想)

这道题其实不太好想到,由于我们想要找到最近公共祖先,自然就希望能够自底向上查找,这里我们就可以想到后序遍历的思想--以左右根的遍历顺序进行遍历。这样,我们就可以通过递归,实现子节点向父节点传递信息。

具体分三步走:

1.基本情况处理:

如果当前节点root是p或q,那么直接返回root,因为root本身就是最近公共祖先(或者就是目标节点之一)。

如果root是nullptr,说明已经遍历到了叶子节点的下一层,没有找到p和q,按照二叉树的性质,此时应该返回nullptr。

2.递归调用:

递归地在左子树中查找p和q的最近公共祖先,将结果保存在left中。

递归地在右子树中查找p和q的最近公共祖先,将结果保存在right中。

3.结果判断:

如果left和right都不为空,说明p和q分别位于当前节点root的左右两侧,因此root就是它们的最近公共祖先,返回root。

如果left为空而right不为空,说明p和q都位于右子树中(或者p/q之一就是right),因此整个树的最近公共祖先就是right,返回right。

如果left不为空而right为空,说明p和q都位于左子树中(或者p/q之一就是left),因此整个树的最近公共祖先就是left,返回left。

代码如下:

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == p || root == q || root == nullptr)      return root;        //p,q有一个为根节点或者根节点为空时,最近公共祖先就是root
        //递归:分别在左右子树中查找p和q的最近公共祖先,并将结果存放在left,right中
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);

        if (left != nullptr && right != nullptr)    return root;        //p,q一个在左子树,一个在右子树,此时最近公共祖先就是root
        if (left == nullptr)    return right;           //p,q都在右子树中,这时最近公共祖先就是右子树的最近公共祖先
        return left;            //p,q都在左子树中
    }
};

三、小结

今天的打卡到此也就结束了,又是不断递归的一天。后边还得多花点时间总结一下。最后,我是算法小白,但也希望终有所获。

  • 26
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值