算法学习记录~2023.5.9~二叉树Day7~501.二叉搜索树中的众数 & 236. 二叉树的最近公共祖先 & 235. 二叉搜索树的最近公共祖先


501.二叉搜索树中的众数

题目链接

力扣题目链接

思路1:按照普通二叉树直接做

如果不是二叉搜索树,那第一反应就是遍历整个树,然后用map统计频率,接着把频率排序后取最高频元素集合。
需要注意的点是,map可以对key排序但不能对value排序,因此需要把map转化为数组vector在进行排序,当然vector也需要存放类型为pair<int, int>的数据,同样是第一个为元素、第二个为频率

代码

class Solution {
public:
    void dfs(TreeNode* cur, unordered_map<int, int>& map){
        if(cur == NULL)
            return;
        map[cur -> val]++;  //中。用于统计元素频率
        dfs(cur->left, map);
        dfs(cur->right, map);
        return ;
    }

    bool static compare(const pair<int,int>& a, const pair<int,int>& b){
        return a.second > b.second;
    }

    vector<int> findMode(TreeNode* root) {
        unordered_map<int, int> map;    //key:元素 value:元素出现频率
        vector<int> result;
        if (root == NULL)
            return result;
        dfs(root, map);
        vector<pair<int, int>> vec(map.begin(),map.end());  //需要将map转为pair类型的vector方便排序
        sort(vec.begin(), vec.end(), compare);
        result.push_back(vec[0].first);         //先将频率最高的元素放入result
        for( int i = 1; i < vec.size(); i++){
            if(vec[i].second == vec[0].second)  //可能存在频率相同的好几个众数
                result.push_back(vec[i].first);
            else
                break;
        }
        return result;
    }
};

思路2:利用二叉搜索树性质

二叉搜索树的中序遍历为有序数组,因此可以通过比较相邻两个元素来获得元素的频率,比较相邻元素同样需要一个pre节点和cur节点作比较,pre初始化为NULL就可以知道什么时候是第一个元素

if (pre == NULL) { // 第一个节点
    count = 1; // 频率为1
} else if (pre->val == cur->val) { // 与前一个节点数值相同
    count++;
} else { // 与前一个节点数值不同
    count = 1;
}
pre = cur; // 更新上一个节点

因为众数可能会有多个,所以可以第一遍遍历找出最大频率maxCount,再遍历第二次找到所有频率为maxCount的元素放进集合,这个就不具体实现了。
其实可以遍历一次就找到所有的,具体步骤如下

  1. 如果 频率count 等于 maxCount(最大频率),要把这个元素加入到结果集中
if (count == maxCount) { // 如果和最大值相同,放进result中
    result.push_back(cur->val);
}
  1. 第一步可能会觉得有问题,因为result怎么能轻易就把元素放进去,万一这个maxCount此时还不是真正最大频率呢。因此需要在count大于maxCount时,不光更新maxCount,还需要清空result,因为之前的元素都失效了
if (count > maxCount) { // 如果计数大于最大值
    maxCount = count;   // 更新最大频率
    result.clear();     // 很关键的一步,不要忘记清空result,之前result里的元素都失效了
    result.push_back(cur->val);
}

代码

class Solution {
public:
    int maxCount = 0;           //最大频率
    int count = 0;              //统计每一个遍历到的当前数的频率
    TreeNode* pre = NULL;       //定义pre节点用于取频率,定义为NULL方便开始
    vector<int> result;         //结果可能有多个所以用vector

    void dfs(TreeNode* cur){
        if (cur == NULL)
            return;

        dfs(cur -> left);       //左

        //接下来都是中节点的处理逻辑
        if (pre == NULL){       //第一个节点
            count = 1;
            maxCount = 1;
        }
        else if (pre -> val == cur -> val){     //如果和前一个节点相同
            count++;
        }
        else{                   //如果和前一个节点不同
            count = 1;
        }
        pre = cur;              //cur就是下一个的cur的pre
        if (count == maxCount)  //如果和最大值相同,放进result中
            result.push_back(cur -> val);
        if(count > maxCount){   //频率最大值被更新
            maxCount = count;   //更新
            result.clear();     //清空result,非常关键,因为之前result里的元素都失效了
            result.push_back(cur -> val);
        }

        dfs(cur -> right);      //右
        return ;
    }

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

思路3:迭代法

和思路2一样,只不过是通过迭代实现中序遍历,中间节点的处理逻辑完全一致

代码

class Solution {
public:
    vector<int> findMode(TreeNode* root) {
        stack<TreeNode*> st;
        TreeNode* cur = root;
        TreeNode* pre = NULL;
        int maxCount = 0;
        int count = 0;
        vector<int> result;
        while (cur != NULL || !st.empty()){
            if (cur != NULL){   //指针来访问节点,访问到最底层
                st.push(cur);   //将访问的节点放进栈
                cur = cur -> left;  //左
            }
            else{
                cur = st.top(); //中
                st.pop();
                if (pre == NULL) { // 第一个节点
                    count = 1;
                } else if (pre->val == cur->val) { // 与前一个节点数值相同
                    count++;
                } else { // 与前一个节点数值不同
                    count = 1;
                }
                if (count == maxCount) { // 如果和最大值相同,放进result中
                    result.push_back(cur->val);
                }

                if (count > maxCount) { // 如果计数大于最大值频率
                    maxCount = count;   // 更新最大频率
                    result.clear();     // 很关键的一步,不要忘记清空result,之前result里的元素都失效了
                    result.push_back(cur->val);
                }
                pre = cur;

                cur = cur->right;               // 右
            }
        }
        return result;
    }
};

总结


236. 二叉树的最近公共祖先

题目链接

力扣题目链接

思路

因为是要找祖先,所以自然想到希望能从下往上找,因为这样下面找到了就能直接返回到上面的节点也就是祖先。

二叉树怎样才能自底向上查找?那就是回溯,二叉树回溯的过程就是从底向上,后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值来处理中节点的逻辑。

有两种情况:

  1. 左右子树各有一个题目要求的值(p、q),那该节点就是两个节点的最近公共祖先:
    判断逻辑是 如果递归遍历遇到q,就将q返回,遇到p就将p返回,那么如果 左右子树的返回值都不为空,说明此时的中节点,一定是q和p的最近祖先
  2. 当前节点本身就拥有一个目标值,另一个在左右子树上

事实上这两种情况的代码实现过程是一样的,因为遇到q或者p就返回,这样也包含了q或者p本身就是公共祖先的情况。

递归三部曲:

  • 确定递归函数返回值以及参数:
    需要递归函数返回值,来帮助判断是否找到节点q或者p,那么返回值为bool类型就可以了。
    但我们还要返回最近公共节点,可以利用上题目中返回值是TreeNode * ,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
  • 确定终止条件
    遇到空的话,因为树都是空了,所以返回空。
    如果 root == q,或者 root == p,说明找到 q / p ,则将其返回,这个返回值,后面在中节点的处理过程中会用到
if (root == q || root == p || root == NULL) return root;
  • 确定单层递归逻辑
    如果left 和 right都不为空,说明此时root就是最近公共节点。
    如果left和right都为空,则返回left或者right都是可以的,也就是返回空。
    如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然。这一条可以通过下图协助理解。
    在这里插入图片描述
    图中节点10的左子树返回null,右子树返回目标值7,那么此时节点10的处理逻辑就是把右子树的返回值(最近公共祖先7)返回上去。
    这样一层一层向上传,才能传回头结点。
if (left != NULL && right != NULL) return root;

if (left == NULL && right != NULL) return right;
else if (left != NULL && right == NULL) return left;
else  { //  (left == NULL && right == NULL)
    return NULL;
}

整体流程:
在这里插入图片描述

额外知识点

本题函数有返回值,是因为回溯的过程需要递归函数的返回值做判断,但本题我们依然要遍历一整棵树。

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

搜索一条边的写法:

if (递归函数(root->left)) return ;
if (递归函数(root->right)) return ;

搜索整个树写法:

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

在递归函数有返回值的情况下,两者有以下区别:
如果要搜索一条边,递归函数返回值不为空的时候,立刻返回;
如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)

在本题中,想利用left和right做逻辑处理,不能立刻返回,而是要等left与right逻辑处理完之后才能返回,所以要遍历整棵树。

代码

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == p || root == q || root == NULL)             //终止条件
            return root;
        TreeNode* left = lowestCommonAncestor(root->left, p, q);        //左
        TreeNode *right = lowestCommonAncestor(root -> right, p, q);    //右
        //接下来确定中间节点处理
        if (left && right)      //找到了最近公共祖先
            return root;
        if (left == NULL && right != NULL)
            return right;
        else if (left != NULL && right == NULL)
            return left;
        else    //左右都为NULL
            return NULL;
    }
};

总结

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

235. 二叉搜索树的最近公共祖先

题目链接

力扣题目链接

思路

本题和236. 二叉树的最近公共祖先的区别在于需要利用二叉搜索树的性质。

因为是有序树,所以如果中间节点是 q 和 p 的公共祖先,那么中节点的数组一定是在 [p, q]区间的。即 中节点 > p && 中节点 < q 或者 中节点 > q && 中节点 < p。

那么只要从上到下去遍历,遇到 cur节点是数值在[p, q]区间中则一定可以说明该节点cur就是q 和 p的公共祖先。 那问题来了,一定是最近公共祖先吗?

如下图所示,从根节点搜索,第一次遇到 cur节点是数值在[p, q]区间中,即 节点5,此时可以说明 p 和 q 一定分别存在于节点 5的左子树和右子树中。
如果从节点5继续向左遍历,那么将错过成为p的祖先, 如果从节点5继续向右遍历则错过成为q的祖先。
所以当我们从上向下去递归遍历,第一次遇到 cur节点是数值在[p, q]区间中,那么cur就是 p和q的最近公共祖先。
在这里插入图片描述
因此本题就很好解了,按照指定方向找到的第一个符合的节点就是最近公共祖先,不需要遍历整棵树,找到结果就可以直接返回

代码1:递归

class Solution {
public:
    TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q){
        if (cur == NULL)        //并不必要,因为其实题目说了p、q 为不同节点且均存在于给定的二叉搜索树中
            return cur;
        if (cur -> val > p -> val && cur -> val > q -> val){    //cur同时大于p和q,向左
            TreeNode* left = traversal(cur -> left, p, q);
            if(left != NULL)
                return left;
        }
        if (cur -> val < p -> val && cur -> val < q -> val){    //cur同时小于p和q,向右
            TreeNode* right = traversal(cur -> right, p, q);
            if(right != NULL)
                return right;
        }
        //剩下cur节点在区间(p->val <= cur->val && cur->val <= q->val
        //或者 (q->val <= cur->val && cur->val <= p->val)中
        //此时cur就是最近公共祖先了,直接返回
        return cur;
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        return traversal(root, p, q);
    }
};

代码2:迭代

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        while(root){
            if (root->val > p->val && root->val > q->val) {
                root = root->left;
            }
            else if (root->val < p->val && root->val < q->val) {
                root = root->right;
            }
            else
                return root;
        }
        return NULL;    //根据题目要求上面一定会出答案,但是由于必须要返回值所以其实这里随便写个什么符合TreeNode*的都行
    }
};

总结

对于有序树,不需要回溯
直接利用自带的方向性按序寻找即可,也因此本题迭代法显得更简洁。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山药泥拌饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值