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;实时判断cnt和maxcnt的关系来更新结果集。
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. 解题思路:后序遍历
- 为什么是后序?
- 我们需要知道左子树和右子树是否包含
p或q,才能判断当前节点是不是最近公共祖先。这本质上是"自底向上"的处理逻辑:先处理孩子,再处理自己。
- 我们需要知道左子树和右子树是否包含
2. 四种情况分析
递归逻辑的核心在于对 left 和 right 返回值的判断:
| 情况 | left (左子树结果) | right (右子树结果) | 结论与操作 |
|---|---|---|---|
| 1 | 非空 (找到了) | 非空 (找到了) | p 和 q 分别位于左右子树。当前 root 就是 LCA。 |
| 2 | 空 (没找到) | 非空 (找到了) | p 和 q 都在右子树。返回 right (LCA在右边)。 |
| 3 | 非空 (找到了) | 空 (没找到) | p 和 q 都在左子树。返回 left (LCA在左边)。 |
| 4 | 空 | 空 | 左右都没找到。返回 NULL。 |
3. 一个容易被忽略的细节
当 root == p || root == q 时,直接返回 root。这看起来像是找到了就停,但实际上不会漏掉另一种情况:
- 如果另一个目标节点在它的子树里,根据情况1,该节点会作为返回值传上去,最终被识别为 LCA。
- 如果另一个目标节点不在它的子树里,该节点的返回值会作为有效信号传给父节点,参与父节点的判断。
4. 复杂度分析
- 时间复杂度:O(N)
- 最坏情况下需要遍历整棵树的所有节点。
- 空间复杂度:O(N)
- 取决于递归栈的深度。最坏情况(树退化为链表)为 O(N)。