算法学习记录| 2023.X.XX| 章节DayX| 题目号.题目标题 & 题目号.题目标题
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的元素放进集合,这个就不具体实现了。
其实可以遍历一次就找到所有的,具体步骤如下
- 如果 频率count 等于 maxCount(最大频率),要把这个元素加入到结果集中
if (count == maxCount) { // 如果和最大值相同,放进result中
result.push_back(cur->val);
}
- 第一步可能会觉得有问题,因为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. 二叉树的最近公共祖先
题目链接
思路
因为是要找祖先,所以自然想到希望能从下往上找,因为这样下面找到了就能直接返回到上面的节点也就是祖先。
二叉树怎样才能自底向上查找?那就是回溯,二叉树回溯的过程就是从底向上,后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值来处理中节点的逻辑。
有两种情况:
- 左右子树各有一个题目要求的值(p、q),那该节点就是两个节点的最近公共祖先:
判断逻辑是 如果递归遍历遇到q,就将q返回,遇到p就将p返回,那么如果 左右子树的返回值都不为空,说明此时的中节点,一定是q和p的最近祖先 - 当前节点本身就拥有一个目标值,另一个在左右子树上
事实上这两种情况的代码实现过程是一样的,因为遇到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;
}
};
总结
- 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从底向上的遍历方式。
- 在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。
- 要理解如果返回值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*的都行
}
};
总结
对于有序树,不需要回溯
直接利用自带的方向性按序寻找即可,也因此本题迭代法显得更简洁。