代码随想录算法训练营 Day16 | 二叉树 part06

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

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

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

cpp 复制代码
// 递归法
class Solution {
public:
    int m_min = 1e5; // 记录最小差值,初始化为一个较大值
    TreeNode* pre = NULL; // 指向前驱节点
    int getMinimumDifference(TreeNode* root) {
        if (root == NULL) return 0;
        // 1. 递归左子树
        getMinimumDifference(root->left);
        // 2. 中序处理(中间节点)
        // 如果前驱节点存在,计算当前节点与前驱节点的差值
        if (pre && root->val - pre->val < m_min) {
            m_min = root->val - pre->val; // 更新最小差值
        }
        pre = root; // 更新前驱节点为当前节点
        // 3. 递归右子树
        getMinimumDifference(root->right);
        return m_min;
    }
};

// 迭代法
class Solution {
public:
    int getMinimumDifference(TreeNode* root) {
        if (root == NULL) return 0;
        stack<TreeNode*> st; // 栈,用于模拟递归
        TreeNode* pre = NULL; // 指向前驱节点
        TreeNode* cur = root; // 工作指针
        int m_min = 1e5; // 记录最小差值
        while (cur || !st.empty()) {
            // 1. 模拟递归深入左子树
            if (cur) {
                st.push(cur);
                cur = cur->left;
            }
            // 2. 左边走到底,开始处理节点
            else {
                cur = st.top(); // 弹出栈顶元素
                st.pop();
                // 【核心逻辑】计算当前节点与前驱节点的差值
                if (pre && cur->val - pre->val < m_min) {
                    m_min = cur->val - pre->val;
                }
                pre = cur; // 更新前驱指针
                cur = cur->right; // 转向右子树
            }
        }
        return m_min;
    }
};

总结

1. 解题原理:BST 的单调性
  • 在有序序列中,求最小差值一定是在相邻的两个元素之间产生。
  • 因此,我们只需要在中序遍历的过程中,计算当前节点与前驱节点的差值即可。
2. 为什么初始化 m_min = 1e5

题目给定的节点值范围是 [0, 10^5]

  • 最大可能的差值是 10^5 - 0 = 100000
  • m_min 初始化为 100000 (1e5),可以保证第一次计算出的有效差值一定能更新 m_min,同时不需要引入 INT_MAX
3. 复杂度分析
  • 时间复杂度:O(N)
    • 两种方法都需要遍历整棵树一次,N 为节点个数。
  • 空间复杂度:
    • 递归法:O(N)。取决于递归栈的深度。
    • 迭代法:O(N)。取决于显式栈 st 的大小。
    • 注:若是平衡二叉树,空间复杂度为 O(log N)。

501. 二叉搜索树中的众数

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

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

假定 BST 满足如下定义:

  • 结点左子树中所含节点的值 小于等于 当前节点的值
  • 结点右子树中所含节点的值 大于等于 当前节点的值
  • 左子树和右子树都是二叉搜索树
cpp 复制代码
// 递归法(哈希表 + 优先队列)
class Solution {
public:
    // 1. 自定义比较器,用于构建大顶堆
    // 优先队列默认是大顶堆,但pair默认先比first,我们需要按second(频率)从大到小排
    class cmp {
    public:
        bool operator() (const pair<int, int>& a, const pair<int, int>& b) {
            return a.second < b.second; // 左边小于右边,大的往上冒(大顶堆)
        }
    };
    unordered_map<int, int> mp; // 记录元素值 -> 出现频率
    priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> que; // 按频率排序的堆
    // 简单的中序遍历,统计频率
    void inOrder(TreeNode* root) {
        if (root == NULL) return;
        inOrder(root->left);
        mp[root->val]++; // 统计次数
        inOrder(root->right);
    }
    vector<int> findMode(TreeNode* root) {
        if (root == NULL) return {};
        vector<int> ans;
        inOrder(root); // 遍历统计
        // 将统计结果放入优先队列
        for (auto i : mp) {
            que.push({i.first, i.second});
        }
        // 取出频率最高的元素
        int m_max = que.top().second; // 最高频率
        ans.push_back(que.top().first); // 加入结果
        que.pop();
        // 检查是否有并列第一(频率相同)
        while (!que.empty() && que.top().second == m_max) {
            ans.push_back(que.top().first);
            que.pop();
        }
        return ans;
    }
};

// 递归法(双指针)
class Solution {
public:
    TreeNode* pre = NULL; // 指向前驱节点
    int cnt = 0;          // 当前元素出现的次数
    int maxcnt = 0;       // 历史最大出现次数
    vector<int> ans;      // 结果集
    void find(TreeNode* root) {
        if (root == NULL) return;
        // 1. 左
        find(root->left);
        // 2. 中(处理逻辑)
        if (pre == NULL) {
            // 第一个节点,计数初始化为1
            cnt = 1;
        } 
        else if (pre->val == root->val) {
            // 遇到相同的值,计数+1
            cnt++;
        } 
        else {
            // 遇到不同的值,计数重置为1
            cnt = 1;
        }
        // 更新前驱指针
        pre = root;
        // 更新结果集
        if (cnt == maxcnt) {
            // 如果当前计数等于最大计数,加入结果集(并列第一)
            ans.push_back(root->val);
        } 
        else if (cnt > maxcnt) {
            // 如果当前计数大于最大计数,说明发现了新的众数
            maxcnt = cnt;        // 更新最大计数
            ans.clear();         // 之前的结果作废
            ans.push_back(root->val); // 加入新的众数
        }
        // 3. 右
        find(root->right);
    }
    vector<int> findMode(TreeNode* root) {
        if (root == NULL) return {};
        find(root);
        return ans;
    }
};

// 迭代法(栈模拟)
class Solution {
public:
    vector<int> findMode(TreeNode* root) {
        if (root == NULL) return {};
        stack<TreeNode*> st;
        TreeNode* pre = NULL; // 前驱节点
        TreeNode* cur = root; // 当前节点
        int cnt = 0, maxcnt = 0; // 当前计数,最大计数
        vector<int> ans;
        // 迭代版中序遍历
        while (cur || !st.empty()) {
            if (cur) {
                st.push(cur);
                cur = cur->left; // 左
            }
            else {
                cur = st.top(); // 中
                st.pop();
                // 【核心逻辑:实时统计频率】
                if (pre == NULL) cnt = 1;
                else if (pre->val == cur->val) cnt++;
                else cnt = 1;
                pre = cur; // 更新前驱
                // 【核心逻辑:实时更新结果】
                if (cnt == maxcnt) {
                    ans.push_back(cur->val);
                }
                else if (cnt > maxcnt) {
                    maxcnt = cnt;
                    ans.clear();
                    ans.push_back(cur->val);
                }
                cur = cur->right; // 右
            }
        }
        return ans;
    }
};

总结

1. 解题思路对比
  • 方法一(哈希表):
    • 优点:思路最直观,不需要利用 BST 的性质,任何二叉树都适用。
    • 缺点:空间复杂度高 O(N),需要额外存哈希表和优先队列。
  • 方法二/三(双指针/迭代):
    • 优点:充分利用了 BST 中序遍历有序 的性质。相同的元素在中序遍历中一定是连续的。只需要一次遍历,空间复杂度低(不考虑结果集和栈,空间 O(1))。
    • 核心操作:cnt 计数,遇到不同重置为 1;实时判断 cntmaxcnt 的关系来更新结果集。
2. 关键逻辑解析(针对方法二/三)

为什么这里不需要像普通数组那样先统计完频率再找最大值?

  • 因为 BST 中序遍历是有序的,元素是"扎堆"出现的。
  • 我们可以一边遍历一边计数。当 cnt > maxcnt 时,说明之前记录的众数都失效了,直接 ans.clear()
3. 复杂度分析
  • 时间复杂度:均为 O(N),需要遍历每个节点一次。
  • 空间复杂度:
    • 方法一:O(N)(哈希表)。
    • 方法二/三:O(1)(不包括递归栈/迭代栈和返回数组)。栈空间最坏 O(N)。

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

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

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

cpp 复制代码
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 1. 终止条件
        // 如果遍历到空节点,说明没找到,返回空
        if (root == NULL) return NULL;
        // 如果当前节点就是 p 或 q,那它自己就是目标之一,直接返回
        // (如果另一个目标在它的子树里,那它恰好就是LCA;如果不在,那结果也会被上层逻辑处理)
        if (root == p || root == q) return root;
        // 2. 递归左右子树寻找 p 和 q
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        // 3. 处理递归返回的结果
        // 情况A:左子树找到了一个,右子树也找到了一个
        // 说明 p 和 q 分别在当前节点的两侧,当前节点就是最近公共祖先
        if (left && right) return root;
        // 情况B:左边没找到,右边找到了
        // 说明 p 和 q 都在右子树,返回右边找到的节点
        else if (!left) return right;
        // 情况C:左边找到了,右边没找到
        // 说明 p 和 q 都在左子树,返回左边找到的节点
        else return left;
    }
};

总结

1. 解题思路:后序遍历
  • 为什么是后序?
    • 我们需要知道左子树和右子树是否包含 pq,才能判断当前节点是不是最近公共祖先。这本质上是"自底向上"的处理逻辑:先处理孩子,再处理自己。
2. 四种情况分析

递归逻辑的核心在于对 leftright 返回值的判断:

情况 left (左子树结果) right (右子树结果) 结论与操作
1 非空 (找到了) 非空 (找到了) pq 分别位于左右子树。当前 root 就是 LCA。
2 空 (没找到) 非空 (找到了) pq 都在右子树。返回 right (LCA在右边)。
3 非空 (找到了) 空 (没找到) pq 都在左子树。返回 left (LCA在左边)。
4 左右都没找到。返回 NULL。
3. 一个容易被忽略的细节

root == p || root == q 时,直接返回 root。这看起来像是找到了就停,但实际上不会漏掉另一种情况:

  • 如果另一个目标节点在它的子树里,根据情况1,该节点会作为返回值传上去,最终被识别为 LCA。
  • 如果另一个目标节点不在它的子树里,该节点的返回值会作为有效信号传给父节点,参与父节点的判断。
4. 复杂度分析
  • 时间复杂度:O(N)
    • 最坏情况下需要遍历整棵树的所有节点。
  • 空间复杂度:O(N)
    • 取决于递归栈的深度。最坏情况(树退化为链表)为 O(N)。
相关推荐
2401_831824963 小时前
代码性能剖析工具
开发语言·c++·算法
Sunshine for you4 小时前
C++中的职责链模式实战
开发语言·c++·算法
qq_416018724 小时前
C++中的状态模式
开发语言·c++·算法
2401_884563244 小时前
模板代码生成工具
开发语言·c++·算法
2401_831920744 小时前
C++代码国际化支持
开发语言·c++·算法
m0_672703314 小时前
上机练习第51天
数据结构·c++·算法
ArturiaZ5 小时前
【day60】
算法·深度优先·图论
2401_851272995 小时前
自定义内存检测工具
开发语言·c++·算法
☆5665 小时前
C++中的命令模式
开发语言·c++·算法