1. 算法训练营第17天:二叉树专题精讲
今天我们来深入探讨LeetCode中关于二叉树的四个经典题目:654最大二叉树、617合并二叉树、700二叉搜索树中的搜索和98验证二叉搜索树。这些题目涵盖了二叉树构建、遍历、搜索和验证等核心操作,是算法面试中的高频考点。
1.1 为什么选择这四道题目
这四道题目看似独立,实则内在联系紧密。它们从不同角度考察了二叉树的特性和操作:
- 654题展示了如何根据特定规则构建二叉树
- 617题训练了对两棵树的同步遍历能力
- 700题利用了二叉搜索树的特性进行高效搜索
- 98题则是对二叉搜索树性质的验证
掌握这四道题,你就能建立起对二叉树操作的完整认知框架。下面我将逐一解析每道题的核心思路和实现细节。
2. 654最大二叉树:构建的艺术
2.1 问题理解与思路分析
给定一个不含重复元素的整数数组,构建一棵最大二叉树:
- 根节点是数组中的最大值
- 左子树是通过最大值左侧子数组构建的最大二叉树
- 右子树是通过最大值右侧子数组构建的最大二叉树
这与通过中序+后序遍历序列构建二叉树的思路类似,都是通过确定根节点后递归构建左右子树。
2.2 递归解法实现
cpp复制TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
return build(nums, 0, nums.size() - 1);
}
TreeNode* build(vector<int>& nums, int l, int r) {
if (l > r) return nullptr;
// 找到最大值及其索引
int maxVal = INT_MIN, index = l;
for (int i = l; i <= r; i++) {
if (nums[i] > maxVal) {
maxVal = nums[i];
index = i;
}
}
TreeNode* root = new TreeNode(maxVal);
root->left = build(nums, l, index - 1);
root->right = build(nums, index + 1, r);
return root;
}
2.3 时间复杂度分析
每次递归都需要遍历当前区间寻找最大值:
- 最坏情况下(数组完全逆序),时间复杂度为O(n²)
- 平均情况下时间复杂度为O(nlogn),类似于快速排序
2.4 注意事项
- 边界条件处理:当l > r时返回nullptr
- 最大值初始化:使用INT_MIN确保能找到数组中的最大值
- 递归终止条件:区间长度为1时自动终止
提示:在实际面试中,可以先用一个简单例子(如[3,2,1])手动演示构建过程,再写代码。
3. 617合并二叉树:双树同步遍历
3.1 问题描述与解题思路
给定两棵二叉树,将它们合并为一棵新二叉树:
- 对应节点都存在:新节点值为两者之和
- 只有一个节点存在:新节点取存在的节点值
- 都不存在:对应位置为nullptr
这需要同时对两棵树进行遍历,可以采用前序遍历的方式。
3.2 递归实现
cpp复制TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
if (!t1) return t2;
if (!t2) return t1;
TreeNode* root = new TreeNode(t1->val + t2->val);
root->left = mergeTrees(t1->left, t2->left);
root->right = mergeTrees(t1->right, t2->right);
return root;
}
3.3 迭代解法
也可以使用层序遍历(BFS)的方式实现:
cpp复制TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
if (!t1) return t2;
if (!t2) return t1;
queue<TreeNode*> q;
q.push(t1);
q.push(t2);
while (!q.empty()) {
TreeNode* node1 = q.front(); q.pop();
TreeNode* node2 = q.front(); q.pop();
node1->val += node2->val;
if (node1->left && node2->left) {
q.push(node1->left);
q.push(node2->left);
} else if (node2->left) {
node1->left = node2->left;
}
// 右子树处理同理
if (node1->right && node2->right) {
q.push(node1->right);
q.push(node2->right);
} else if (node2->right) {
node1->right = node2->right;
}
}
return t1;
}
3.4 复杂度分析
两种方法的时间复杂度都是O(n),n是两棵树中节点数的较小值。递归方法的空间复杂度取决于树高,而迭代方法的空间复杂度取决于树的宽度。
4. 700二叉搜索树中的搜索:利用特性
4.1 二叉搜索树特性回顾
二叉搜索树(BST)具有以下性质:
- 左子树所有节点值 < 根节点值
- 右子树所有节点值 > 根节点值
- 左右子树也都是BST
这个性质使得在BST中搜索可以像二分查找一样高效。
4.2 递归搜索实现
cpp复制TreeNode* searchBST(TreeNode* root, int val) {
if (!root || root->val == val) return root;
return val < root->val ? searchBST(root->left, val) : searchBST(root->right, val);
}
4.3 迭代实现
cpp复制TreeNode* searchBST(TreeNode* root, int val) {
while (root && root->val != val) {
root = val < root->val ? root->left : root->right;
}
return root;
}
4.4 复杂度分析
时间复杂度:O(h),h是树高。对于平衡BST,h=log(n);最坏情况下(树退化为链表)h=n。
空间复杂度:
- 递归:O(h)(调用栈)
- 迭代:O(1)
5. 98验证二叉搜索树:陷阱与技巧
5.1 常见错误思路
很多初学者会这样判断:
cpp复制bool isValidBST(TreeNode* root) {
if (!root) return true;
if (root->left && root->left->val >= root->val) return false;
if (root->right && root->right->val <= root->val) return false;
return isValidBST(root->left) && isValidBST(root->right);
}
这种写法是错误的,因为它只检查了当前节点与直接子节点的关系,而没有确保整个左子树的所有节点都小于根节点。
5.2 正确解法:中序遍历
利用BST中序遍历结果为升序的特性:
cpp复制TreeNode* prev = nullptr;
bool isValidBST(TreeNode* root) {
if (!root) return true;
if (!isValidBST(root->left)) return false;
if (prev && prev->val >= root->val) return false;
prev = root;
return isValidBST(root->right);
}
5.3 区间限定法
另一种思路是在遍历时传递当前节点的值范围:
cpp复制bool isValidBST(TreeNode* root) {
return helper(root, LONG_MIN, LONG_MAX);
}
bool helper(TreeNode* root, long minVal, long maxVal) {
if (!root) return true;
if (root->val <= minVal || root->val >= maxVal) return false;
return helper(root->left, minVal, root->val) &&
helper(root->right, root->val, maxVal);
}
5.4 复杂度分析
两种方法的时间复杂度都是O(n),需要访问所有节点。空间复杂度都是O(h),取决于树高。
6. 二叉树问题解题方法论
通过这四道题目,我们可以总结出解决二叉树问题的通用方法:
- 明确遍历顺序:前序、中序、后序还是层序?不同顺序适用于不同场景
- 递归三要素:
- 终止条件
- 当前层处理逻辑
- 递归调用左右子树
- 利用树的性质:如BST的排序性质可以优化搜索效率
- 边界条件处理:空节点、单节点等特殊情况
- 空间优化:递归转迭代减少空间消耗
在实际面试中,建议:
- 先明确问题要求
- 用简单例子手动模拟
- 确定遍历顺序和递归逻辑
- 编写代码并测试边界条件
- 分析复杂度并提出优化方向
7. 常见错误与调试技巧
在解决二叉树问题时,容易犯以下错误:
-
无限递归:忘记写终止条件或条件不正确
- 调试:打印递归深度和参数值
-
指针操作错误:对空指针进行操作
- 防御性编程:每次访问节点前检查是否为nullptr
-
错误利用性质:如BST问题中只检查直接子节点
- 验证:用多个测试用例验证,包括极端情况
-
变量覆盖:如98题中忘记维护prev指针
- 技巧:使用全局变量或引用参数
调试时可以:
- 用小规模树手动模拟
- 打印中间结果
- 使用可视化工具观察树结构
8. 扩展练习与进阶题目
为了巩固二叉树相关算法,推荐以下LeetCode题目:
| 题目编号 | 名称 | 难度 | 关键点 |
|---|---|---|---|
| 226 | 翻转二叉树 | 简单 | 递归交换左右子树 |
| 101 | 对称二叉树 | 简单 | 双树同步遍历 |
| 104 | 二叉树的最大深度 | 简单 | 递归计算深度 |
| 105 | 从前序与中序遍历序列构造二叉树 | 中等 | 利用遍历特性 |
| 114 | 二叉树展开为链表 | 中等 | 后序遍历处理 |
对于想进一步挑战的学员,可以尝试:
-
- 二叉树的序列化与反序列化
-
- 二叉树中的最大路径和
-
- 恢复二叉搜索树
记住,掌握二叉树的关键在于多练习、多思考、多总结。每做完一道题,问问自己:
- 这道题的套路是什么?
- 有哪些易错点?
- 能否用不同的方法解决?
- 时间/空间复杂度如何?
通过这样的刻意练习,你很快就能对二叉树问题游刃有余。