1. 二叉树算法精讲:LeetCode 530-538题解
作为一名算法工程师,我深知二叉树在面试和实际开发中的重要性。今天我将分享LeetCode上编号530到538的9道二叉树经典题目,这些题目涵盖了二叉搜索树的各种操作和常见问题。通过这组题目,我们可以系统掌握二叉树的遍历、修改和特性应用。
1.1 二叉搜索树的最小绝对差(LeetCode 530)
这道题要求我们找出二叉搜索树中相邻节点的最小绝对差。二叉搜索树的中序遍历结果是有序数组,这是解题的关键。
核心思路:
- 中序遍历二叉搜索树,得到有序数组
- 比较数组中相邻元素的差值,找出最小值
优化方案:
使用双指针法可以在遍历过程中直接计算差值,无需额外空间存储整个中序序列。我们维护一个pre指针指向当前节点的前驱节点:
cpp复制class Solution {
public:
int ans = INT_MAX;
TreeNode* pre = nullptr;
int getMinimumDifference(TreeNode* root) {
traversal(root);
return ans;
}
void traversal(TreeNode* cur) {
if (!cur) return;
traversal(cur->left);
if (pre) {
ans = min(ans, abs(cur->val - pre->val));
}
pre = cur;
traversal(cur->right);
}
};
注意事项:
- pre指针必须定义为全局变量或通过引用传递
- 初始值ans应设为足够大的数(如INT_MAX)
- 绝对差计算要考虑负数情况,使用abs函数
1.2 二叉搜索树中的众数(LeetCode 501)
这道题要求找出二叉搜索树中出现频率最高的元素。普通二叉树的做法是用哈希表统计频率,但二叉搜索树可以利用其有序特性优化。
高效解法:
- 中序遍历过程中统计当前数字的出现次数
- 维护最大出现次数maxCount
- 当当前计数等于maxCount时加入结果集
- 当当前计数超过maxCount时清空结果集并更新maxCount
cpp复制class Solution {
public:
vector<int> findMode(TreeNode* root) {
vector<int> result;
int count = 0, maxCount = 0;
TreeNode* pre = nullptr;
stack<TreeNode*> st;
TreeNode* cur = root;
while (cur || !st.empty()) {
if (cur) {
st.push(cur);
cur = cur->left;
} else {
cur = st.top();
st.pop();
// 统计频率
if (!pre || cur->val != pre->val) {
count = 1;
} else {
count++;
}
// 更新结果
if (count > maxCount) {
maxCount = count;
result.clear();
result.push_back(cur->val);
} else if (count == maxCount) {
result.push_back(cur->val);
}
pre = cur;
cur = cur->right;
}
}
return result;
}
};
关键点:
- 迭代法中pre指针的处理要小心
- 结果集的更新逻辑要放在统计频率之后
- 最后一个节点的处理不能遗漏
1.3 二叉树的最近公共祖先(LeetCode 236)
最近公共祖先(LCA)问题是二叉树中的经典问题。给定二叉树和两个节点,找到它们深度最大的公共祖先。
递归解法:
cpp复制class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (!root || root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left && right) return root;
return left ? left : right;
}
};
算法分析:
- 时间复杂度:O(n),每个节点最多访问一次
- 空间复杂度:O(h),递归栈深度为树高
注意事项:
- 该解法假设p和q一定存在于树中
- 如果可能不存在,需要额外判断
1.4 二叉搜索树的最近公共祖先(LeetCode 235)
二叉搜索树的LCA可以利用其有序特性简化:
cpp复制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 nullptr;
}
};
优化点:
- 无需递归,迭代即可解决
- 利用二叉搜索树的有序性快速定位
1.5 二叉搜索树的插入操作(LeetCode 701)
二叉搜索树的插入操作相对简单,新节点总是可以插入到某个叶子节点位置:
cpp复制class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (!root) return new TreeNode(val);
TreeNode* cur = root;
while (true) {
if (val < cur->val) {
if (!cur->left) {
cur->left = new TreeNode(val);
break;
}
cur = cur->left;
} else {
if (!cur->right) {
cur->right = new TreeNode(val);
break;
}
cur = cur->right;
}
}
return root;
}
};
注意事项:
- 处理空树的特殊情况
- 插入位置一定是某个叶子节点
- 不需要重新平衡树结构
1.6 删除二叉搜索树中的节点(LeetCode 450)
删除操作是二叉搜索树中最复杂的操作,需要考虑多种情况:
cpp复制class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (!root) return nullptr;
if (key < root->val) {
root->left = deleteNode(root->left, key);
} else if (key > root->val) {
root->right = deleteNode(root->right, key);
} else {
if (!root->left) return root->right;
if (!root->right) return root->left;
TreeNode* minNode = getMin(root->right);
root->val = minNode->val;
root->right = deleteNode(root->right, minNode->val);
}
return root;
}
TreeNode* getMin(TreeNode* node) {
while (node->left) node = node->left;
return node;
}
};
五种情况处理:
- 节点不存在
- 是叶子节点
- 只有左子树
- 只有右子树
- 左右子树都存在
关键点:
- 对于左右子树都存在的情况,可以用右子树的最小节点或左子树的最大节点替代
- 注意内存管理,特别是C++中要手动释放节点
1.7 修剪二叉搜索树(LeetCode 669)
修剪二叉搜索树是指保留值在[L,R]范围内的节点:
cpp复制class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
if (!root) return nullptr;
if (root->val < low) return trimBST(root->right, low, high);
if (root->val > high) return trimBST(root->left, low, high);
root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);
return root;
}
};
常见错误:
- 直接删除整个子树而不仅是不符合条件的节点
- 忘记递归处理修剪后的子树
1.8 将有序数组转换为平衡二叉搜索树(LeetCode 108)
将有序数组转换为高度平衡的二叉搜索树:
cpp复制class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
return helper(nums, 0, nums.size() - 1);
}
TreeNode* helper(vector<int>& nums, int left, int right) {
if (left > right) return nullptr;
int mid = left + (right - left) / 2;
TreeNode* root = new TreeNode(nums[mid]);
root->left = helper(nums, left, mid - 1);
root->right = helper(nums, mid + 1, right);
return root;
}
};
算法分析:
- 时间复杂度:O(n),每个元素处理一次
- 空间复杂度:O(logn),递归栈深度
关键点:
- 选择中间元素作为根节点保证平衡
- 递归构建左右子树
1.9 把二叉搜索树转换为累加树(LeetCode 538)
累加树是指每个节点的值变为原树中所有大于等于它的节点值之和:
cpp复制class Solution {
public:
TreeNode* convertBST(TreeNode* root) {
int sum = 0;
stack<TreeNode*> st;
TreeNode* cur = root;
while (cur || !st.empty()) {
if (cur) {
st.push(cur);
cur = cur->right;
} else {
cur = st.top();
st.pop();
sum += cur->val;
cur->val = sum;
cur = cur->left;
}
}
return root;
}
};
解题思路:
- 反序中序遍历(右-根-左)
- 维护一个累加和变量
- 遍历过程中更新节点值
2. 二叉树算法总结与心得
通过这组题目,我们可以总结出二叉树算法的一些通用技巧:
- 遍历是基础:前序、中序、后序和层次遍历要熟练掌握,很多问题都是遍历的变种
- 递归与迭代:递归写法简洁但可能有栈溢出风险,迭代写法效率更高但代码复杂
- 指针技巧:如pre指针记录前驱节点,在许多问题中非常有用
- 二叉搜索树特性:中序遍历有序性可以简化很多问题
- 边界条件:空树、单节点等特殊情况要特别注意
在实际面试中,二叉树问题出现的频率非常高。建议初学者按照以下步骤练习:
- 先理解基本遍历方式
- 练习简单修改操作(插入、删除)
- 解决属性判断问题(对称、平衡等)
- 最后攻克复杂问题(LCA、序列化等)
我在实际工作中发现,很多树相关问题都可以分解为遍历+局部处理。掌握这个思路后,解决树问题会变得更有章法。