1. 二叉树深度与高度的本质区别
在二叉树算法中,深度(Depth)和高度(Height)这两个概念经常被初学者混淆。让我用实际场景来解释它们的区别:
- 深度:就像测量一口井的深度要从井口开始算起,二叉树的深度是从根节点出发,沿着路径向下计算到目标节点的边数。例如根节点的深度为0,其直接子节点深度为1。这种计算天然适合前序遍历(根-左-右),因为我们在第一次遇到节点时就记录其深度。
cpp复制// 前序遍历计算节点深度示例
void preorderDepth(TreeNode* node, int currentDepth) {
if (!node) return;
cout << "Node " << node->val << " depth: " << currentDepth << endl;
preorderDepth(node->left, currentDepth + 1);
preorderDepth(node->right, currentDepth + 1);
}
- 高度:更像测量一棵树从地面到树顶的高度,二叉树的高度是从目标节点出发向下到最远叶子节点的边数。叶子节点的高度为0,空节点高度为-1。计算高度需要后序遍历(左-右-根),因为必须知道子树的高度才能计算当前节点高度。
cpp复制// 后序遍历计算节点高度示例
int postorderHeight(TreeNode* node) {
if (!node) return -1;
int leftHeight = postorderHeight(node->left);
int rightHeight = postorderHeight(node->right);
return max(leftHeight, rightHeight) + 1;
}
关键理解:根节点的高度就是整棵树的深度。这解释了为什么计算二叉树深度时,可以采用计算根节点高度的后序遍历方法。
2. 平衡二叉树的递归解法精讲
2.1 问题核心分析
平衡二叉树(110题)的定义是:每个节点的左右子树高度差不超过1。这个定义本身就暗示了递归解法的可能性:
- 需要比较左右子树的高度差
- 需要递归检查每个子树是否平衡
2.2 递归实现细节
标准解法采用后序遍历,时间复杂度O(n):
cpp复制class Solution {
public:
bool isBalanced(TreeNode* root) {
return getHeight(root) != -1;
}
int getHeight(TreeNode* node) {
if (!node) return 0;
int leftHeight = getHeight(node->left);
if (leftHeight == -1) return -1;
int rightHeight = getHeight(node->right);
if (rightHeight == -1) return -1;
if (abs(leftHeight - rightHeight) > 1) return -1;
return max(leftHeight, rightHeight) + 1;
}
};
2.3 关键优化点
- 提前终止:一旦发现某子树不平衡,立即返回-1,避免不必要的计算
- 高度复用:在计算高度差的同时完成平衡性检查,避免重复遍历
- 空节点处理:空节点高度为0,保证叶子节点高度计算正确
常见错误:混淆高度和平衡状态的返回值,导致逻辑错误。建议用-1表示不平衡状态,非负数表示实际高度。
3. 二叉树所有路径的回溯解法
3.1 问题理解与递归思路
257题要求找出从根到所有叶子的路径,例如:
code复制 1
/ \
2 3
\
5
输出:["1->2->5", "1->3"]
这类"收集所有路径"的问题通常需要:
- 前序遍历记录路径
- 到达叶子节点时保存路径
- 回溯时移除当前节点
3.2 完整实现代码
cpp复制class Solution {
public:
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> result;
vector<int> path;
backtrack(root, path, result);
return result;
}
void backtrack(TreeNode* node, vector<int>& path, vector<string>& result) {
if (!node) return;
path.push_back(node->val);
if (!node->left && !node->right) { // 叶子节点
string pathStr;
for (int i = 0; i < path.size(); ++i) {
pathStr += to_string(path[i]);
if (i != path.size() - 1) pathStr += "->";
}
result.push_back(pathStr);
}
backtrack(node->left, path, result);
backtrack(node->right, path, result);
path.pop_back(); // 回溯
}
};
3.3 回溯法的核心要点
- 递归前添加:进入节点时将其加入路径
- 递归后移除:处理完子树后从路径中移除(这就是回溯)
- 叶子节点判断:左右子节点均为空时才记录完整路径
- 路径构建技巧:使用vector动态存储,最后转换为字符串
调试技巧:在递归调用前后打印path内容,观察回溯过程。例如在push_back和pop_back前后添加日志输出。
4. 指针操作与二叉树遍历
4.1 C++中的指针访问语法
在二叉树实现中,我们频繁使用node->left这样的指针访问。需要理解:
node是指向TreeNode结构体的指针->运算符相当于先解引用再访问成员,等价于(*node).left- 必须确保node不为nullptr,否则会引发段错误
cpp复制// 安全的指针访问示例
void traverse(TreeNode* node) {
if (!node) return; // 必须的null检查
// 访问左右子节点
if (node->left) traverse(node->left);
if (node->right) traverse(node->right);
}
4.2 指针与递归的关系
递归处理二叉树时,指针传递有以下特点:
- 值传递:指针本身是按值传递的,函数内修改指针不会影响外部
- 共享对象:指针指向的TreeNode对象是共享的,修改对象内容会影响所有引用
cpp复制void modifyTree(TreeNode* node) {
node->val = 100; // 会影响原始树
node = node->left; // 不会影响外部指针
}
5. 二叉树问题的通用解题框架
5.1 递归三要素
解决二叉树问题的递归方法通常包含:
- 终止条件:处理空节点或叶子节点的基本情况
- 递归处理:处理左子树和右子树
- 当前逻辑:根据子树结果处理当前节点
5.2 四种基本遍历方式
- 前序遍历:适合"自上而下"处理,如计算深度
cpp复制void preorder(TreeNode* node) {
if (!node) return;
// 处理当前节点
preorder(node->left);
preorder(node->right);
}
- 中序遍历:BST会得到有序序列
cpp复制void inorder(TreeNode* node) {
if (!node) return;
inorder(node->left);
// 处理当前节点
inorder(node->right);
}
- 后序遍历:适合"自下而上"处理,如计算高度
cpp复制void postorder(TreeNode* node) {
if (!node) return;
postorder(node->left);
postorder(node->right);
// 处理当前节点
}
- 层序遍历:使用队列进行广度优先搜索
cpp复制void levelOrder(TreeNode* root) {
queue<TreeNode*> q;
if (root) q.push(root);
while (!q.empty()) {
TreeNode* node = q.front();
q.pop();
// 处理当前节点
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
}
6. 常见问题与调试技巧
6.1 递归栈溢出
当二叉树极度不平衡时(如退化成链表),递归可能导致栈溢出。解决方法:
- 改用迭代法实现
- 使用尾递归优化(如果编译器支持)
- 增加递归深度限制(不推荐)
6.2 指针相关错误
- 空指针解引用:总是检查
if (!node)条件 - 内存泄漏:如果手动管理内存,记得删除节点
- 野指针:删除节点后将其指针置为nullptr
6.3 调试日志技巧
在递归函数中添加日志输出,帮助理解执行流程:
cpp复制void traverse(TreeNode* node, int depth = 0) {
cout << string(depth, ' ') << "Entering: ";
cout << (node ? to_string(node->val) : "null") << endl;
if (!node) return;
traverse(node->left, depth + 2);
traverse(node->right, depth + 2);
cout << string(depth, ' ') << "Leaving: ";
cout << (node ? to_string(node->val) : "null") << endl;
}
7. 算法优化思路进阶
7.1 记忆化递归
对于需要重复计算的子树问题(如二叉树的直径),可以使用哈希表存储已计算结果:
cpp复制unordered_map<TreeNode*, int> memo;
int getHeightMemo(TreeNode* node) {
if (!node) return 0;
if (memo.count(node)) return memo[node];
int left = getHeightMemo(node->left);
int right = getHeightMemo(node->right);
return memo[node] = max(left, right) + 1;
}
7.2 迭代法实现
所有递归算法都可以改为迭代实现,通常使用栈模拟递归:
cpp复制vector<string> binaryTreePathsIterative(TreeNode* root) {
vector<string> result;
if (!root) return result;
stack<pair<TreeNode*, string>> s;
s.push({root, to_string(root->val)});
while (!s.empty()) {
auto [node, path] = s.top();
s.pop();
if (!node->left && !node->right) {
result.push_back(path);
continue;
}
if (node->right) {
s.push({node->right, path + "->" + to_string(node->right->val)});
}
if (node->left) {
s.push({node->left, path + "->" + to_string(node->left->val)});
}
}
return result;
}
在实际工程中,递归代码通常更简洁,但迭代法可以避免栈溢出风险,各有优劣。