提示:DDU,供自己复习使用。欢迎大家前来讨论~
二叉树 Part04
二、题目
题目一:530.二叉搜索树的最小绝对差
解题思路:
-
BST的特点:二叉搜索树是一种特殊的二叉树,其中每个节点的值都大于或等于其左子树上所有节点的值,并且小于或等于其右子树上所有节点的值。
-
类比有序数组:由于BST的有序性,我们可以将其视为一个有序排列的数组,其中元素按照BST的遍历顺序排列。
-
求解最值和差值:在有序数组中求解最值(最大值或最小值)或差值(两个元素之间的差)通常涉及到找到数组中相邻的元素,因为它们之间的差值最小。
-
简化问题:将BST问题转化为有序数组问题可以简化思考过程,因为数组操作通常更直观。
在BST上求解最小差值,可以考虑到树的根节点开始,然后沿着树的遍历顺序(通常是中序遍历)找到相邻的节点对,计算它们的差值,并更新最小差值。
递归法
那么二叉搜索树采用中序遍历,其实就是一个有序数组。在一个有序数组上求两个数最小差值,这是不是就是一道送分题了。
把二叉搜索树转化为有序数组了,其实在二叉搜素树中序遍历的过程中,我们就可以直接计算了。
完整代码如下:
class Solution {
private:
vector<int> vec;
void traversal(TreeNode* root) {
if (root == NULL) return;
traversal(root->left);
vec.push_back(root->val); // 将二叉搜索树转换为有序数组
traversal(root->right);
}
public:
int getMinimumDifference(TreeNode* root) {
vec.clear();
traversal(root);
if (vec.size() < 2) return 0;
int result = INT_MAX;
for (int i = 1; i < vec.size(); i++) { // 统计有序数组的最小差值
result = min(result, vec[i] - vec[i-1]);
}
return result;
}
};
需要用一个pre节点记录一下cur节点的前一个节点。
![530.二叉搜索树的最小绝对差](https://img-blog.csdnimg.cn/img_convert/d2eb9bb66ee6af4587e53c1cee396913.png)
不太知道在递归中如何记录前一个节点的指针,其实实现起来是很简单的。
代码如下:(中序遍历,左中右)
双指针,前后两个,掌握一下
class Solution {
private:
int result = INT_MAX;
TreeNode* pre = NULL;
void traversal(TreeNode* cur) {
if (cur == NULL) return;
traversal(cur->left); // 左
if (pre != NULL){ // 中
result = min(result, cur->val - pre->val);
}
pre = cur; // 记录前一个
traversal(cur->right); // 右
}
public:
int getMinimumDifference(TreeNode* root) {
traversal(root);
return result;
}
};
迭代法:
(了解一下)也是中序遍历
class Solution {
public:
int getMinimumDifference(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* cur = root;
TreeNode* pre = NULL;
int result = INT_MAX;
while (cur != NULL || !st.empty()) {
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
cur = cur->left; // 左
} else {
cur = st.top();
st.pop();
if (pre != NULL) { // 中
result = min(result, cur->val - pre->val);
}
pre = cur;
cur = cur->right; // 右
}
}
return result;
}
};
小结:
遇到在二叉搜索树上求什么最值,求差值之类的,都要思考一下二叉搜索树可是有序的,要利用好这一特点。
同时要学会在递归遍历的过程中如何记录==前后两个指针==,这也是一个小技巧,学会了还是很受用的。
题目二: 501.二叉搜索树中的众数
解题思路
递归法我从两个维度来讲。两种方式做一个比较,可以加深大家对二叉树的理解
- 如果不是二叉搜索树的话,应该怎么解题。
- 是二叉搜索树,又应该如何解题。
递归法:
如果不是二叉搜索树
如果不是二叉搜索树,最直观的方法一定是把这个树都遍历了,用map统计频率,把频率排个序,最后取前面高频的元素的集合。
-
遍历
至于用前中后序哪种遍历也不重要,因为就是要全遍历一遍,怎么个遍历法都行,层序遍历都没毛病!
// map<int, int> key:元素,value:出现频率 void searchBST(TreeNode* cur, unordered_map<int, int>& map) { // 前序遍历 if (cur == NULL) return ; map[cur->val]++; // 统计元素频率 searchBST(cur->left, map); searchBST(cur->right, map); return ; }
-
把统计的出来的出现频率(即map中的value)排个序
在C++中,
std::map
和std::multimap
默认按键(key)排序,而不是按值(value)。如果需要根据值来排序,首先需要将
map
中的元素转移到一个vector
中,这个vector
存储的类型是pair<int, int>
,其中第一个int
代表元素,第二个int
代表该元素的出现频率。之后,可以对这个vector
进行排序,以实现按值排序的目的。简单来说,就是先将
map
转换成vector
,然后对vector
里的元素按值排序。bool static cmp (const pair<int, int>& a, const pair<int, int>& b) { return a.second > b.second; // 按照频率从大到小排序 } vector<pair<int, int>> vec(map.begin(), map.end()); sort(vec.begin(), vec.end(), cmp); // 给频率排个序
-
取前面高频的元素
此时数组vector中已经是存放着按照频率排好序的pair,那么把前面高频的元素取出来就可以了。
-
result.push_back(vec[0].first); for (int i = 1; i < vec.size(); i++) { // 取最高的放到result数组中 if (vec[i].second == vec[0].second) result.push_back(vec[i].first); else break; } return result;
整体C++代码如下:
class Solution {
private:
void searchBST(TreeNode* cur, unordered_map<int, int>& map) { // 前序遍历
if (cur == NULL) return ;
map[cur->val]++; // 统计元素频率
searchBST(cur->left, map);
searchBST(cur->right, map);
return ;
}
bool static cmp (const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second;
}
public:
vector<int> findMode(TreeNode* root) {
unordered_map<int, int> map; // key:元素,value:出现频率
vector<int> result;
if (root == NULL) return result;
searchBST(root, map);
vector<pair<int, int>> vec(map.begin(), map.end());
sort(vec.begin(), vec.end(), cmp); // 给频率排个序
result.push_back(vec[0].first);
for (int i = 1; i < vec.size(); i++) {
// 取最高的放到result数组中
if (vec[i].second == vec[0].second) result.push_back(vec[i].first);
else break;
}
return result;
}
};
是二叉搜索树
既然是搜索树,它中序遍历就是有序的。
如图:
中序遍历代码如下:
void searchBST(TreeNode* cur) {
if (cur == NULL) return ;
searchBST(cur->left); // 左
(处理节点) // 中
searchBST(cur->right); // 右
return ;
}
遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。
使用了pre指针和cur指针的技巧,这次又用上了。
弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。
而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素。
if (pre == NULL) { // 第一个节点
count = 1; // 频率为1
} else if (pre->val == cur->val) { // 与前一个节点数值相同
count++;
} else { // 与前一个节点数值不同
count = 1;
}
pre = cur; // 更新上一个节点
-
数组上找众数的常规方法:
- 首先遍历数组,统计每个元素的出现次数,并记录最大频率。
- 再次遍历数组,将出现频率等于最大频率的元素添加到结果集合中。
-
二叉搜索树上的应用:
- 类似地,在二叉搜索树上也可以通过两次遍历来找到众数集合。
- 一次遍历的优化,实际上,在某些情况下,只需要遍历一次数据结构就可以找到所有的众数。
-
如何实现单次遍历:
- 由于二叉搜索树的有序性,可以在遍历过程中更高效地识别众数。
- 需要一个方法来在单次遍历过程中识别并收集所有具有最大频率的元素。
-
遍历方法:
- 使用中序遍历(对于BST)可以按照值的顺序访问所有节点,有助于在单次遍历中收集众数。
-
统计与收集:
-
在遍历过程中,同时统计每个元素的出现频率,并与当前已知的最大频率进行比较,以此来识别众数。
-
可能需要额外的数据结构(如哈希表)来记录每个元素的出现次数,以便在单次遍历中做出判断。
如果 频率count 等于 maxCount(最大频率),当然要把这个元素加入到结果集中(以下代码为result数组),代码如下:
if (count == maxCount) { // 如果和最大值相同,放进result中 result.push_back(cur->val); } //频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集(以下代码为result数组),因为结果集之前的元素都失效了。 if (count > maxCount) { // 如果计数大于最大值 maxCount = count; // 更新最大频率 result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 result.push_back(cur->val); }
关键代码都讲完了,完整代码如下:(只需要遍历一遍二叉搜索树,就求出了众数的集合)
class Solution { private: int maxCount = 0; // 最大频率 int count = 0; // 统计频率 TreeNode* pre = NULL; vector<int> result; void searchBST(TreeNode* cur) { if (cur == NULL) return ; searchBST(cur->left); // 左 // 中 if (pre == NULL) { // 第一个节点 count = 1; } else if (pre->val == cur->val) { // 与前一个节点数值相同 count++; } else { // 与前一个节点数值不同 count = 1; } pre = cur; // 更新上一个节点 if (count == maxCount) { // 如果和最大值相同,放进result中 result.push_back(cur->val); } if (count > maxCount) { // 如果计数大于最大值频率 maxCount = count; // 更新最大频率 result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 result.push_back(cur->val); } searchBST(cur->right); // 右 return ; } public: vector<int> findMode(TreeNode* root) { count = 0; maxCount = 0; pre = NULL; // 记录前一个节点 result.clear(); searchBST(root); return result; } };
-
迭代法
只要把中序遍历转成迭代,中间节点的处理逻辑完全一样。
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. 二叉树的最近公共祖先
解题思路:
从下往上,利用回溯的过程,后序遍历(左右中)。中的处理过程,要根据左和右的结果进行判断 =》后序遍历
遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。
那么二叉树如何可以自底向上查找呢? 回溯啊,二叉树回溯的过程就是从底向上。
后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。
接下来就看如何判断一个节点是节点q和节点p的公共祖先呢。
首先最容易想到的一个情况(同级节点):
如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。 即情况一:
![img](https://img-blog.csdnimg.cn/img_convert/71bb3daf3ad02527506acae6a070fb4b.png)
判断逻辑是 如果递归遍历遇到q,就将q返回,遇到p 就将p返回,那么如果 左右子树的返回值都不为空,说明此时的中节点,一定是q 和p 的最近祖先。
容易有疑惑的点:会不会左子树遇到q 返回,右子树也遇到q返回,这样并没有找到 q 和p的最近祖先。
这里需要审题,题目强调:二叉树节点数值是不重复的,而且一定存在 q 和 p。
但是很多人容易忽略一个情况,就是节点本身p(q),它拥有一个子孙节点q§。(不同级节点) 情况二:
![img](https://img-blog.csdnimg.cn/img_convert/c6fd6cf00ae02bf9ebb63c5083a46ae5.png)
其实情况一 和 情况二 代码实现过程都是一样的,也可以说,实现情况一的逻辑,顺便包含了情况二。
因为遇到 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;
-
- 确定单层递归逻辑
值得注意的是 本题函数有返回值,是因为回溯的过程需要递归函数的返回值做判断,但本题我们依然要遍历树的所有节点。
**Q:**如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢?
搜索一条边的写法:
-
if (递归函数(root->left)) return ; if (递归函数(root->right)) return ;
搜索整个树写法:
-
left = 递归函数(root->left); // 左 right = 递归函数(root->right); // 右 left与right的逻辑处理; // 中
区别:
在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)。
Q:那么为什么要遍历整棵树呢?
直观上来看,找到最近公共祖先,直接一路返回就可以了。
![236.二叉树的最近公共祖先](https://img-blog.csdnimg.cn/img_convert/65af362bc69d2060b41c76b106683aab.png)
就像图中一样直接返回7。
但事实上还要遍历根节点右子树(即使此时已经找到了目标节点了),也就是图中的节点4、15、20。
因为在如下代码的后序遍历中,如果想利用left和right做逻辑处理, 不能立刻返回,而是要等left与right逻辑处理完之后才能返回。
left = 递归函数(root->left); // 左
right = 递归函数(root->right); // 右
left与right的逻辑处理; // 中
所以此时大家要知道我们要遍历整棵树。知道这一点,对本题就有一定深度的理解了。
那么先用left和right接住左子树和右子树的返回值,代码如下:
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
如果left 和 right都不为空,说明此时root就是最近公共节点。这个比较好理解
如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然。
这里有的同学就理解不了了,为什么left为空,right不为空,目标节点通过right返回呢?(返回的是遍历到的节点的值)
如图:
![236.二叉树的最近公共祖先1](https://img-blog.csdnimg.cn/img_convert/33bcb2b4298df4db75899cd69e2d6e78.png)
图中节点10的左子树返回null,右子树返回目标值7,那么此时节点10的处理逻辑就是把右子树的返回值(最近公共祖先7)返回上去!
这里也很重要,可能刷过这道题目的同学,都不清楚结果究竟是如何从底层一层一层传到头结点的。
那么如果left和right都为空,则返回left或者right都是可以的,也就是返回空。
代码如下:
if (left == NULL && right != NULL) return right;
else if (left != NULL && right == NULL) return left;
else { // (left == NULL && right == NULL)
return NULL;
}
那么寻找最小公共祖先,完整流程图如下:
![236.二叉树的最近公共祖先2](https://img-blog.csdnimg.cn/img_convert/49ca7d06b50efbfa584964c2a1b3c9d1.png)
从图中,可以看到,我们是如何回溯遍历整棵二叉树,将结果返回给头结点的!
完整的代码:
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == q || root == p || root == NULL) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
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;
}
}
};
小结:
未必真正了解这里面回溯的过程,以及结果是如何一层一层传上去的。
归纳如下三点:
- 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从底向上的遍历方式。
- 在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。
- 要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。
可以说这里每一步,都是有难度的,都需要对二叉树,递归和回溯有一定的理解。
总结
- 回溯的重要性
- 二叉树的构造
坚持坚持,写不出来,但一定要理解思路。