222.完全二叉树的节点个数
完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。
普通二叉树
// 版本一
class Solution {
private:
int getNodesNum(TreeNode* cur) {
if (cur == NULL) return 0;
int leftNum = getNodesNum(cur->left); // 左
int rightNum = getNodesNum(cur->right); // 右
int treeNum = leftNum + rightNum + 1; // 中
return treeNum;
}
public:
int countNodes(TreeNode* root) {
return getNodesNum(root);
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(log n),算上了递归系统栈占用的空间
完全二叉树
class Solution {
public:
int countNodes(TreeNode* root) {
if (root == nullptr) return 0;
TreeNode* left = root->left;
TreeNode* right = root->right;
int leftDepth = 0, rightDepth = 0; // 这里初始为0是有目的的,为了下面求指数方便
while (left) { // 求左子树深度
left = left->left;
leftDepth++;
}
while (right) { // 求右子树深度
right = right->right;
rightDepth++;
}
if (leftDepth == rightDepth) {
return (2 << leftDepth) - 1; // 注意(2<<1) 相当于2^2,所以leftDepth初始为0
}
return countNodes(root->left) + countNodes(root->right) + 1;
}
};
- 时间复杂度:O(log n × log n)
- 空间复杂度:O(log n)
左移<< 右移>>
110.平衡二叉树
本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
求深度可以从上到下去查 所以需要前序遍历(中左右),而高度只能从下到上去查,所以只能后序遍历(左右中)
class Solution {
public:
// 返回以该节点为根节点的二叉树的高度,如果不是平衡二叉树了则返回-1
int getHeight(TreeNode* node) {
if (node == NULL) {
return 0;
}
int leftHeight = getHeight(node->left); // 左
if (leftHeight == -1)
return -1;
int rightHeight = getHeight(node->right); // 右
if (rightHeight == -1)
return -1;
int result;
if (abs(leftHeight - rightHeight) > 1) { // 中
result = -1;
} else {
result = 1 + max(leftHeight,
rightHeight); // 以当前节点为根节点的树的最大高度
}
return result;
}
bool isBalanced(TreeNode* root) {
return getHeight(root) == -1 ? false : true;
}
};
257. 二叉树的所有路径
class Solution {
private:
void traversal(TreeNode* cur, vector<int>& path, vector<string>& result) {
path.push_back(cur->val); // 中,中为什么写在这里,因为最后一个节点也要加入到path中
// 这才到了叶子节点
if (cur->left == NULL && cur->right == NULL) {
string sPath;
for (int i = 0; i < path.size() - 1; i++) {
sPath += to_string(path[i]);
sPath += "->";
}
sPath += to_string(path[path.size() - 1]);
result.push_back(sPath);
return;
}
if (cur->left) { // 左
traversal(cur->left, path, result);
path.pop_back(); // 回溯
}
if (cur->right) { // 右
traversal(cur->right, path, result);
path.pop_back(); // 回溯
}
}
public:
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> result;
vector<int> path;
if (root == NULL) return result;
traversal(root, path, result);
return result;
}
};
为什么不需要显式判断 cur
是否为空
-
函数入口已经做了非空检查:
binaryTreePaths
函数在调用traversal
之前,已经对根节点root
进行了非空检查:if (root == NULL) return result;
这保证了递归的初始调用时,cur
不会为空。
-
递归只在节点存在的情况下继续:
- 在
traversal
函数内部,只有在cur->left
或cur->right
不为空的情况下,才会进行递归调用。 - 因此,每次进入
traversal
函数时,cur
必然是一个有效的非空节点。
- 在
-
叶子节点处理:
- 当
cur
为叶子节点(即cur->left == NULL && cur->right == NULL
)时,函数会直接处理路径并返回,不会继续递归。
- 当
to_string
是 C++ 标准库中的一个函数,它用于将数值类型(如整数、浮点数等)转换为字符串(std::string
)形式。
class Solution {
private:
void traversal(TreeNode* cur, string path, vector<string>& result) {
path += to_string(cur->val); // 中
if (cur->left == NULL && cur->right == NULL) {
result.push_back(path);
return;
}
if (cur->left) traversal(cur->left, path + "->", result); // 左
if (cur->right) traversal(cur->right, path + "->", result); // 右
}
public:
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> result;
string path;
if (root == NULL) return result;
traversal(root, path, result);
return result;
}
};
显式判断 cur
是否为空是不必要的,因为:
- 你已经在
binaryTreePaths
函数中对根节点进行了非空检查。 - 递归逻辑本身在处理空节点时已经得到处理(通过检查
cur->left
和cur->right
是否存在)。 - 叶子节点的判断和处理也确保了
cur
是有效的节点。
在这个版本的代码中,你不需要显式地进行回溯,这是因为路径的构建是通过字符串拼接完成的,而字符串在 C++ 中是不可变的(即每次拼接都会生成一个新的字符串)。这使得每次递归调用中的 path
都是一个独立的副本,而不会影响其他递归调用中的 path
值。
回溯的概念
回溯通常用于撤销某个操作以恢复到之前的状态,从而在递归或迭代的过程中可以探索不同的选择。在之前的代码版本中,由于路径是通过 vector<int>
记录的,递归回溯时需要显式地将最后一个节点从路径中移除,以便回溯到上一个状态。
为什么不需要回溯
在这个版本的代码中,路径 path
是以字符串的形式进行构建的,每次递归调用时,你将当前节点的值拼接到 path
中,并传递一个新的字符串 path + "->"
到下一层递归。这有几个关键点:
-
字符串不可变:
- 每次
path + "->"
会生成一个新的字符串,而不会改变原有的path
,这意味着你不需要显式地进行回溯操作来恢复path
的原始状态。
- 每次
-
递归过程中路径独立:
- 在每个递归调用中,
path
是一个独立的字符串副本,修改它不会影响其他递归路径中的path
。因此,即使递归到某个叶子节点并返回上层调用,之前构建的path
依然是原来的样子,不需要通过回溯来恢复。
- 在每个递归调用中,
-
递归的自包含性:
- 递归函数调用时,每一层都能独立处理自己的
path
,生成各自的路径字符串并传递给下一层,而不需要担心其他路径的干扰。
- 递归函数调用时,每一层都能独立处理自己的
总结
在这种情况下,不需要回溯是因为你通过不可变的字符串拼接方式实现了路径的构建,每次递归都生成了一个新的字符串对象,递归过程本身就确保了路径的独立性。相比之下,使用回溯的情况通常发生在路径或状态是可变对象(如数组或 vector
)时,需要在递归返回后恢复状态以继续其他路径的探索。
这个方法的优点是代码更加简洁,避免了不必要的回溯操作,缺点可能是在涉及大量路径时,由于字符串拼接操作可能会带来一些性能开销。
注意在函数定义的时候void traversal(TreeNode* cur, string path, vector<string>& result)
,定义的是string path
,每次都是复制赋值,不用使用引用,否则就无法做到回溯的效果。(这里涉及到C++语法知识)
那么在如上代码中,貌似没有看到回溯的逻辑,其实不然,回溯就隐藏在traversal(cur->left, path + "->", result);
中的 path + "->"
。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。
404.左叶子之和
class Solution {
public:
int sumOfLeftLeaves(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right== NULL) return 0;
int leftValue = sumOfLeftLeaves(root->left); // 左
if (root->left && !root->left->left && !root->left->right) { // 左子树就是一个左叶子的情况
leftValue = root->left->val;
}
int rightValue = sumOfLeftLeaves(root->right); // 右
int sum = leftValue + rightValue; // 中
return sum;
}
};
确定单层递归的逻辑,不要想的太复杂