代码随想录DAY18 - 二叉树 - 08/17

目录

二叉搜索树的最小绝对差

题干

思路和代码

方法一:求中序序列

方法二:递归法+双指针法

方法三:迭代法+双指针法

二叉搜索树中的众数

题干

思路和代码

方法一:求中序序列

方法二:递归法+双指针+中序遍历

​编辑

方法三:迭代法+双指针+中序遍历

二叉树的公共祖先

题干

思路和代码

​编辑

递归法:后序遍历

递归法:优化

总结:二叉搜索树


二叉搜索树的最小绝对差

题干

题目:给你一个二叉搜索树的根节点 root ,返回树中任意两不同节点值之间的最小差值 。差值是一个正数,等于两值之差的绝对值。

注意:0 <= Node.val <= 10^5(节点值非负数)

链接:. - 力扣(LeetCode)

思路和代码

题目是要求 “任意” 两个结点的差值,但问题在于如何遍历任意两个结点?

二叉搜索树的结点是有序的,最小差值只会出现在中序序列的两个相邻元素之间。

方法一:求中序序列

先求二叉搜索树的中序序列数组,再遍历数组从中找差值最小的。而二叉搜索树的结点是有序的,最小差值只会出现在两个相邻元素之间,因此只需要遍历一次数组即可。

class Solution {
public:
    void inorder(TreeNode* root, vector<int> &nums){
        if (root == nullptr) return;
        inorder(root->left,nums);
        nums.push_back(root->val);
        inorder(root->right,nums);
    }
    int getMinimumDifference(TreeNode* root) {
        vector<int> nums;
        inorder(root,nums);
        int minValue = INT_MAX; // 记录最小差值
        // 遍历中序序列数组,只需要比较相邻两个元素即可
        for (int i = 0; i < nums.size()-1; ++i) {
            if (nums[i+1] - nums[i] < minValue){
                minValue = nums[i+1] - nums[i];
            }
        }
        return minValue;
    }
};
方法二:递归法+双指针法

之前的方法是先将中序序列数组求出,再遍历数组,这样相当于遍历了两次二叉树的所有结点。但其实在中序遍历的过程中就可以直接记录最小差值,这样就只需要遍历一次二叉树的所有结点,省时间。

问题是如何在中序遍历的过程中记录最小差值?

我们知道最小差值只会出现在中序序列的两个相邻元素之间,在数组里可以直接使用下标访问相邻元素,而在树中,我们需要设置两个指针,一个指针 cur 指向当前遍历的结点,另一个指针 pre 指向 cur 的上一个结点,这样 pre 和 cur 所指的两个元素就是相邻的。

  • 递归参数和返回值:参数是传入的当前结点,无返回值。

  • 递归结束的条件:若传入的结点为空,说明已经遍历到最底部,则直接返回。

  • 递归顺序:按照 “左中右” 的顺序,我们需要先不断遍历左子树找到最左结点。当 cur 指向最左结点时,pre 暂时为空,则需要让 pre = cur,待 cur 回溯到父节点后,才能比较 pre 和 cur 的差值 与 最小差值,最后再遍历右子树。

class Solution {
public:
    int minValue = INT_MAX; // 记录最小差值
    TreeNode* pre = nullptr; // 记录上一个结点指针
    // 在每一次递归中都要更新,因此要定义为全局变量
    void inorder(TreeNode* cur){
        // 当前结点为空
        if (cur == nullptr) return;
        
        inorder(cur->left); // 遍历左子树
        
        if (pre != nullptr){
            minValue = min(abs(pre->val - cur->val),minValue);
        }
        pre = cur;
        
        inorder(cur->right); // 遍历右子树
    }

    int getMinimumDifference(TreeNode* root) {
        inorder(root);
        return minValue;
    }
};
方法三:迭代法+双指针法

思路和方法二一样,只是将递归中序遍历换成了迭代法。

class Solution {
public:
    int getMinimumDifference(TreeNode* root) {
        stack<TreeNode*> tmpNode; // 存储遍历过的序列
        TreeNode* cur = root;
        TreeNode* pre = nullptr; // 记录上一个结点
        int minValue = INT_MAX;
        while (cur || !tmpNode.empty()){
            if (cur == nullptr){
                // 已经找到最左结点
                cur = tmpNode.top();
                tmpNode.pop();
                // ....计算 pre 和 cur 的差值
                if (pre != nullptr){
                    minValue = min(abs(pre->val - cur->val),minValue);
                }
                pre = cur;
                // ....
                cur = cur->right; // 遍历右子树
            } else{
                tmpNode.push(cur); // 暂存插入的结点
                cur = cur->left; // 不断遍历左子树直到找到最左结点
            }
        }
        return minValue;
    }
};

二叉搜索树中的众数

题干

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

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

假定 BST 满足如下定义:

  • 结点左子树中所含节点的值 小于等于 当前节点的值

  • 结点右子树中所含节点的值 大于等于 当前节点的值

  • 左子树和右子树都是二叉搜索树

链接:. - 力扣(LeetCode)

思路和代码

方法一:求中序序列

由于二叉搜索树的有序性,可以先求出其中序序列数组,再遍历数组统计出现频率最高的元素。

注意,在遍历数组统计元素次数时,我们使用了双指针法,即快慢指针法,用慢指针 slow 记录当前元素出现的第一个位置,快指针 fast 记录当前元素出现的最后一个位置,这样当前元素出现的次数就等于 fast - slow + 1。 每统计完一个元素的次数才更新结果集。

class Solution {
public:
    // 求中序序列
    void inorder(TreeNode* node, vector<int> &nums){
        if (node == nullptr) return;
        inorder(node->left,nums);
        nums.push_back(node->val);
        inorder(node->right,nums);
    }
    // 找众数
    vector<int> findMode(TreeNode* root) {
        vector<int> nums; // 存储中序序列
        inorder(root,nums); // 求中序序列
        int maxCount = 0; // 记录最大出现次数
        vector<int> result; // 存储众数结果集
        // 双指针法遍历数组,统计每个数出现的频率
        int slow = 0; // slow 指向相同元素的起始位置
        int fast; // fast 指向相同元素的末尾位置
        for (fast = 0; fast < nums.size(); ++fast) {
            if (fast == nums.size()-1 || nums[slow] != nums[fast+1]){
                // 当 fast 遍历到数组末尾,或者 fast 的下一个元素已经是不同元素时
                // 说明当前元素已经遍历到末尾
                int count = fast-slow+1; // 统计当前元素的出现次数
                if (maxCount < count){
                    maxCount = count;
                    result.clear(); // 当出现频率更高的元素时,结果集数组要先清空
                    result.push_back(nums[slow]);
                } else if (maxCount == count){
                    result.push_back(nums[slow]);
                }
                slow = fast+1;
            }
        }
        return result;
    }
};
方法二:递归法+双指针+中序遍历

同样是要用双指针法,一个指针 cur 指向当前结点,另一个指针 pre 指向上一个结点,用 curCount 统计当前结点出现的次数,用 maxCount 统计最大出现次数,用 result 数组统计众数结果集。这里的双指针法和上一个方法中在有序数组里的双指针法不同。

在上一个方法有序数组中,每统计完一个元素的次数才更新 result 。但是在递归遍历中,只要当前结点的出现次数大于或等于 maxCount了,就立即更新 result,哪怕此时当前节点的出现次数还没统计完毕。

  • 递归参数和返回值:参数是传入的当前结点 cur 以及结果集数组 result (在递归过程中不断更新 result)。无返回值。

  • 递归结束条件:当 cur 为空,要向上层回溯,递归结束。

  • 递归顺序:根据中序遍历 “左中右” 的顺序,先遍历左子树统计元素次数,再回到中间结点处理,而后遍历右子树统计元素次数。

class Solution {
public:
    int maxCount = 0; // 记录最大出现次数
    int curCount = 0; // 记录当前元素的出现次数
    TreeNode* pre = nullptr; // 记录上一个元素
    void count(TreeNode* cur, vector<int> &result){
        if (cur == nullptr) return;
        
        count(cur->left,result); // 遍历左子树
        
        if (pre == nullptr){ // 如果 pre 为空,说明 cur 为中序序列的第一个结点,此时 count 为 1
            curCount = 1;
        } else if (pre->val == cur->val){
            curCount++;
        } else {
            curCount = 1;
        }

        if (curCount > maxCount){
            maxCount = curCount;
            result.clear();
            result.push_back(cur->val);
        } else if (curCount == maxCount){
            result.push_back(cur->val);
        }
        
        pre = cur;
        count(cur->right,result); // 遍历右子树
    }
    vector<int> findMode(TreeNode* root) {
        vector<int> result;
        count(root,result);
        return result;
    }
};
方法三:迭代法+双指针+中序遍历

思路和上述方法相同,都是在中序遍历过程中记录众数,只不过递归中序遍历改成了迭代中序遍历。

class Solution {
public:
    vector<int> findMode(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> tmpNode;
        TreeNode* pre = nullptr; // 记录 cur 的上一个结点
        TreeNode* cur = root;
        int maxCount = 0; // 记录最大出现次数
        int count = 0; // 记录当前结点的出现次数
        while (cur != nullptr || !tmpNode.empty()){
            if (cur != nullptr){
                tmpNode.push(cur);
                cur = cur->left; // 不断向左子树遍历,找到最左边结点
            } else{
                cur = tmpNode.top();
                tmpNode.pop();
                if (pre == nullptr){
                    count = 1; // 如果 pre 为空,说明当前结点是中序序列的第一个结点,count = 1
                } else if (pre->val == cur->val){
                    count++; // 当前结点和之前的结点相同,则结点出现次数递增
                } else{
                    // 当前结点和之前的结点不同,所以 count 更新为 1
                    count = 1;
                }
                // 每遍历到一个结点就比较一次
                // 比较当前结点的出现次数和最大出现频率
                if (count > maxCount){
                    maxCount = count;
                    result.clear();
                    result.push_back(cur->val);
                } else if (count == maxCount){
                    result.push_back(cur->val);
                }
                
                pre = cur;
                cur = cur->right; // 遍历右子树
            }
        }

        return result;
    }
};

二叉树的公共祖先

题干

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

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

说明:

  • 所有节点的值都是唯一的。

  • p、q 为不同节点且均存在于给定的二叉树中。

链接:. - 力扣(LeetCode)

思路和代码

公共祖先的情况有两种:

  • 情况一:两个结点不在同一棵子树中,则需要回溯找到两个结点的公共祖先。

  • 情况二:一个结点在另一个结点的子树中,则其中一个结点即为最近公共祖先。

递归法:后序遍历

Q:为什么是后序遍历?

先遍历左右子树,看当前子树是否有p或q结点,并把情况返回给父节点(需要收集左右子树的信息)。当左右子树或当前父节点都存在 p、q ,则说明已经找到公共祖先,且由于是后序遍历,此公共祖先肯定是最近的,符合题意。

  • 递归参数和返回值:递归参数是当前结点 node 和要找的 p、q 结点;返回值是 bool 型变量,返回以当前 node 结点为根的子树是否有 p 或 q。

  • 递归结束的条件:当 node 结点为空,说明没有 p、q,直接返回false。

  • 递归顺序:先递归查询左右子树是否有 p、q,此时有多种情况。

    • 左右子树都返回 true,说明左右子树都有 p、q,则当前的父节点即公共祖先(情况一),直接返回 true;

    • 左右子树一个 true、一个 false,说明只有其中一棵子树有p或q,则再判断当前父结点是否为 p 或 q。

      • 若父节点为 p 或 q,则父节点就是公共祖先(情况二),直接返回 true。

      • 若父节点不是 p 或 q,那么仍需要向上层返回,继续寻找公共祖先,不做处理。

    • 左右子树都为 false,再判断当前父节点是否为 p 或 q,若是,直接返回 true。

    • 其余情况均没有找到公共祖先,都需要继续往上返回,返回 leftTree||rightTree(只要有一个子树有 p 或 q,就为 true)。

      (注: leftTree 表示左子树是否有 p 或 q,rightTree 表示右子树是否有 p 或 q)

class Solution {
public:
    TreeNode* ancestor = nullptr; // 记录最近公共祖先,全局变量
    bool findAncestor(TreeNode* node, TreeNode* p, TreeNode* q){
        if (node == nullptr) return false;

        bool leftTree = findAncestor(node->left,p,q);
        if (ancestor != nullptr) return true; // 只要找到了公共祖先,就直接返回 true
        bool rightTree = findAncestor(node->right,p,q);
        if (ancestor != nullptr) return true;

        if (leftTree && rightTree && ancestor == nullptr){
            // 左右子树都为 true,则都有 p、q
            ancestor = node;
            return true;
        } else if ((leftTree && !rightTree) ||
                (!leftTree && rightTree) ){
            // 左子树有,右子树没有;或左子树没有,右子树有
            if (ancestor == nullptr && (node == p || node == q)){
                ancestor = node;
                return true;
            }
        } else{
            // 左右子树都没有,但如果当前结点有,直接返回 true
            if (node == p || node == q) return true;
        }
        return leftTree||rightTree;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        bool result = findAncestor(root,p,q);
        return ancestor;
    }
};
递归法:优化

思路和之前的方法是一样的,只不过我们在递归返回值上做出了调整。由于我们需要记录公共祖先,则可以设定返回值为树的结点。我们在上一个方法中,设定 bool 型变量记录子树中是否有 p、q 结点。在这个方法里,当子树有 p、q 结点时,我们直接返回结点。如果返回结点仍为空,说明还没有找到 p、q。

  • 递归参数和返回值:参数是根节点,要查询的 p、q 结点。返回值是结点。

  • 递归结束条件:当传入的结点为空,说明遍历到最底部,要想上层返回,递归结束。

  • 递归顺序:先遍历左子树查询 p、q,再遍历右子树查询 p、q,并记录左右子树返回的结点情况。

    • 先判断当前结点 root 是否为 p或q,若是,则直接返回 root。(为什么?如下解释)

      • 当结点 root 为 p 或 q,子树若有 p或q,root 是公共祖先,要返回 root。

      • 当结点 root 为 p 或 q,子树没有 p或q,要返回 root,表示找到了 p、q。

    • 若当前结点 root 不是 p 或 q,则有多种情况。

      • 左子树和右子树返回的都不是空指针,说明左右子树都有 p、q,当前结点 node 即为公共祖先,直接返回。

      • 左子树返回空,右子树返回非空,说明右子树有 p 或 q,当前结点 node 又不是 p、q,直接返回右子树的返回结点。

      • 右子树返回空,左子树返回非空,说明左子树有 p 或 q,当前结点 node 又不是 p、q,直接返回左子树的返回结点。

      • 左右子树都返回空,说明一直没有找到 p、q,返回空指针 nullptr。

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == nullptr) return nullptr;
        
        TreeNode* leftTree = lowestCommonAncestor(root->left,p,q);
        TreeNode* rightTree = lowestCommonAncestor(root->right,p,q);
        // 只要当前结点是 p 或 q,无论子树里有没有 p、q,都直接返回 node
        if (root == p || root == q) return root;
        // 如果当前结点不是 p 或 q
        if (leftTree != nullptr && rightTree != nullptr){
            // 左右子树均找到了 p、q,则当前结点 node 就是公共祖先
            return root;
        } else if (leftTree == nullptr && rightTree != nullptr){
            // 左子树没有 p、q,右子树有,直接返回右子树的 p 或 q
            return rightTree;
        } else if (leftTree != nullptr && rightTree == nullptr){
            // 左子树没有 p、q,右子树有,直接返回左子树的 p 或 q
            return leftTree;
        }
        // 以上情况均不满足,说明暂时都没有找到 p、q,返回空指针
        return nullptr;
    }
};

总结:二叉搜索树

遇到在二叉搜索树上求最值、求差值问题,其实都可以先把二叉搜索树转化为一个有序数组,在有序数组上求解问题会变得简单很多。

另一种方法是在中序遍历的过程中使用双指针法,即在递归遍历中用一个指针 cur 指向当前结点,用一个指针 pre 指向上一个遍历过的结点。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值