11.27自顶向下 DFS
199. 二叉树的右视图
思路:BFS层序遍历或DFS深搜先右后左
BFS层序遍历,每次可以先遍历右儿子再遍历左儿子,每个循环存储第一个节点
nodei==0的值。DFS深搜时,先递归右子树,再递归左子树,当某个深度首次到达时,对应的节点就在右视图中。
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
queue<TreeNode*> que;
vector<int> res;
if(root) que.push(root);
while(!que.empty()){
int n = que.size();
for (int i = 0; i < n; i++) {
TreeNode* node = que.front();
que.pop();
if(i == 0) res.push_back(node->val);
if(node->right) que.push(node->right);
if(node->left) que.push(node->left);
}
}
return res;
}
};
class Solution {
vector<int> res;//直接定义全局变量数组,减少dfs里的参数
void dfs(TreeNode* node , int depth ){
if(!node) return;
if(depth == res.size()) res.push_back(node->val);
//当深度首次到达时,记录节点值
dfs(node->right , depth + 1 );//先递归右子树,保证首次遇到的一定是最右边的节点
dfs(node->left , depth + 1 );
}
public:
vector<int> rightSideView(TreeNode* root) {
dfs(root , 0 );
return res;
}
};
1448. 统计二叉树中好节点的数目
思路:搜索所有路径,记录路径上的最大值mx,定义全局变量res,每次节点值大于mx , res++。
有个坑是以为需要剪枝,遇到node->val < mx就返回,这是错的。样例1告诉我们需要遍历所有节点。所以本题返回条件只有遇到空节点。
class Solution {
int res;
void dfs(TreeNode* node , int mx){
if(!node) return;
if(node->val >= mx){
res ++;
mx = node->val;
}
if(node->left) dfs(node->left , mx);
if(node->right) dfs(node->right , mx);
}
public:
int goodNodes(TreeNode* root) {
dfs(root , INT_MIN);
return res;
}
};
灵神版:
我们可以在向下递归的同时,额外维护一个参数
mx表示从根节点到当前节点之前,路径上的最大节点值。
- 如果当前节点为空,到达递归边界,返回 0。
- 递归左子树
goodNodes(root.left, max(mx, root.val)),获取左子树的好节点个数 left。- 递归右子树
goodNodes(root.right, max(mx, root.val)),获取右子树的好节点个数 right。- 如果当前节点是好节点,即
mx <= root.val,那么返回left+right+1。否则返回left+right。
class Solution {
public:
int goodNodes(TreeNode *root, int mx = INT_MIN) {
if (root == nullptr)
return 0;
int left = goodNodes(root->left, max(mx, root->val));
int right = goodNodes(root->right, max(mx, root->val));
return left + right + (root->val >= mx);
}
};
1457. 二叉树中的伪回文路径
思路:
- 题目允许把路径上的节点值重新排列。如果能排列成回文序列,按照长度的奇偶性分类讨论:
偶回文序列:左半边和右半边是一一对应的。比如 11222211,左半边有两个 1,右半边肯定也有两个 1。所以每个数字的出现次数均为偶数。对于长为偶数的路径,只要路径上的每个数字的出现次数均为偶数,就一定可以重新排列成回文序列。
奇回文序列:把正中间的那个数拿出来,就变成偶回文序列了。比如 112232211,拿出正中间的 3 就和上面一样。所以恰好有一个数字的出现次数为奇数,其余数字的出现次数均为偶数。对于长为奇数的路径,只要路径上数字的出现次数满足该要求,就一定可以重新排列成回文序列。
综合起来就是
奇数个数的数的个数<=1
- 如何维护出现次数的奇偶性?
有两种写法:
数组 / 位运算。用一个数组 p 维护路径上 1 到 9 每个数字出现次数的奇偶性。如果一个数出现偶数次,则记录为 0;如果出现奇数次,则记录为 1。递归之前,所有数都出现 0 次,所以数组元素都初始化成 0。
从根节点开始递归,假设我们遇到了 5,如果 p[5]=0,那么把 p[5] 从 0 改成 1,否则从 1 改成 0,这可以用「异或 1」实现,即
p[5] ^= 1。(p[5] = p[5] ^ 1)当递归到叶子时,如果发现
数组 p 中的 1 的个数是 0 个或者 1 个,那么说明我们可以得到一个回文序列,返回 1,否则返回 0。
//自己的写法:搜索所有路径,用一个哈希表统计路径中数字的个数,最后遍历个数为奇数的数如果<=1则ans++,结果好像溢出了,暂时没找到原因。
class Solution {
vector<unordered_map<int , int>> res;
void dfs(TreeNode* node , unordered_map<int , int>& mp){
if(!node) return;
mp[node->val] ++;
if(!node->left && !node->right) res.push_back(mp);
dfs(node->left , mp);
dfs(node->right , mp);
}
public:
int pseudoPalindromicPaths (TreeNode* root) {
int ans , odd = 0;
unordered_map<int , int> mp;
dfs(root , mp);
for (auto mp : res){
for (int i = 0 ; i < mp.size() ; i++){
if(mp[i] % 2 == 1) odd += 1;
}
if(odd <= 1) ans ++;
}
return ans;
}
};
展开:
C++中
unorder_map的三种插入方式:emplace、赋值和insert,并通过示例展示了它们的区别。其中,emplace在遇到相同key时拒绝插入,赋值会更新已有key的value,而insert的行为与emplace相同。在需要更新value时,应使用赋值方式而非emplace。
灵神的题解
用数组维护:
class Solution {
int dfs(TreeNode *node, array<int, 10> &p) {
if (node == nullptr) {
return 0;
}
p[node->val] ^= 1; // 修改 node->val 出现次数的奇偶性
int res;
if (node->left == node->right) { // node 是叶子节点
res = accumulate(p.begin(), p.end(), 0) <= 1;
} else {
res = dfs(node->left, p) + dfs(node->right, p);
}
// 恢复到递归 node 之前的状态(不做这一步就把 node->val 算到其它路径中了),体现回溯的思路。
p[node->val] ^= 1;
return res;
}
public:
int pseudoPalindromicPaths(TreeNode *root) {
array<int, 10> p{};
return dfs(root, p);
}
};
注释:
accumulate是numeric库中的一个函数,主要用来对指定范围内元素求和,但也自行指定一些其他操作,如范围内所有元素相乘、相除等。使用前需要引入相应的头文件。
#include <numeric>函数共有四个参数,其中前三个为必须,第四个为非必需。
若不指定第四个参数,则默认对范围内的元素进行累加操作。
accumulate(起始迭代器, 结束迭代器, 初始值, 自定义操作函数)
accumulate函数将它的一个内部变量设置为指定的初始值,然后在此初值上累加输入范围内所有元素的值。accumulate算法返回累加的结果,其返回类型就是其第三个实参的类型。可以使用accumulate把string型的vector容器中的元素连接起来:
string sum = accumulate(v.begin() , v.end() , string(" "));这个函数调用的效果是:从空字符串开始,把vec里的每个元素连接成一个字符串。
它擅长于将一个范围汇总为单一值,但不擅长于对每个元素进行函数应用并收集结果。使用时要时刻谨记这一原则,以确保代码清晰易懂,体现算法的本意。
res = accumulate(p.begin(), p.end(), 0) <= 1;额外对这行解释,因为p数组元素值只有0/1,因此对其和进行累加的值,就是奇数的个数,因此与1比较,如果大于1,res = 0 ; <=1 , res = 1。
写法二:用位运算维护
由于 p 中只有 0 和 1,所以可以把 p 压缩成一个二进制数 mask,它的每个比特记录着 0 和 1。
如果 mask 中只有一个 1,那么去掉这个 1 之后,mask 就变成 0 了,所以可以用上面这篇文章中的「删除最小元素」的方法,判断
mask&(mask−1)=0是否成立,如果成立则说明 mask 中要么只有一个 1,要么全为 0。
class Solution {
public:
int pseudoPalindromicPaths(TreeNode *root, int mask = 0) {
if (root == nullptr) {
return 0;
}
mask ^= 1 << root->val; // 修改 root->val 出现次数的奇偶性
if (root->left == root->right) { // root 是叶子节点
return (mask & (mask - 1)) == 0;
}
return pseudoPalindromicPaths(root->left, mask) +
pseudoPalindromicPaths(root->right, mask);
}
};
复杂度分析
- 时间复杂度:O(n),其中 n 为二叉树的节点个数。
- 空间复杂度:O(n)。最坏情况下,二叉树退化成一条链,递归需要 O(n) 的栈空间。
位运算
分享|从集合论到位运算,常见位运算技巧分类总结! - 力扣(LeetCode)
1315. 祖父节点值为偶数的节点和
思路:深搜递归,遇到偶数节点就判断孙子节点是否存在,是就加上值。
class Solution {
int res;
void dfs(TreeNode* node){
if(!node) return;
if(node->val % 2 == 0){
if(node->left){
if(node->left->left) res += node->left->left->val;
if(node->left->right) res += node->left->right->val;
}
if(node->right){
if(node->right->left) res += node->right->left->val;
if(node->right->right) res += node->right->right->val;
}
}
dfs(node->left);
dfs(node->right);
}
public:
int sumEvenGrandparent(TreeNode* root) {
dfs(root);
return res;
}
};



浙公网安备 33010602011771号