669. 修剪二叉搜索树
给你二叉搜索树的根节点
root,同时给定最小边界low和最大边界high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案 。所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。
cpp
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
// 1. 终止条件:如果节点为空,直接返回
if (root == NULL) return NULL;
// 2. 当前节点值小于下界
// 说明当前节点及其左子树都需要被剪掉
// 直接去右子树寻找符合范围的节点作为替代
if (root->val < low) {
return trimBST(root->right, low, high);
}
// 3. 当前节点值大于上界
// 说明当前节点及其右子树都需要被剪掉
// 直接去左子树寻找符合范围的节点作为替代
if (root->val > high) {
return trimBST(root->left, low, high);
}
// 4. 当前节点在范围内 [low, high]
// 保留当前节点,但需要递归处理其左右子树,并将处理后的子树接回去
root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);
// 5. 返回当前节点(已经修剪好的子树)
return root;
}
};
总结
1. 解题思路:BST 性质的巧妙运用
- 情况1:
root->val < low- 当前节点太小,且因为 BST 性质,它的左子树里的值只会更小,全部都要删掉。
- 我们不需要删除节点,而是直接"跳过"当前节点,去右子树里找是否有符合要求的大值。这相当于把右子树"提"上来。
- 情况2:
root->val > high- 当前节点太大,且因为 BST 性质,它的右子树里的值只会更大,全部都要删掉。
- 同理,去左子树里找是否有符合要求的小值。
- 情况3:
low <= root->val <= high- 当前节点符合要求,保留它。但是它的子树里可能有不合法的值,所以需要递归修剪左右子树。
2. 为什么不用删除节点?
注意代码中并没有 delete root; 的操作。
- 这里的逻辑是返回新的子树根节点给上层。
- 当
root->val < low时,我们直接返回了trimBST(root->right, ...),这实际上就是让上层节点的指针跳过了当前root,指向了右子树。当前root节点虽然在内存中还在,但在逻辑树中已经被"剪掉"了。
3. 复杂度分析
- 时间复杂度:O(N)
- 最坏情况下需要遍历所有节点(例如所有节点都在范围内)。
- 空间复杂度:O(N)
- 递归栈的深度,最坏情况为树的高度 N。
108. 将有序数组转换为二叉搜索树
给你一个整数数组
nums,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。
cpp
// 递归法
class Solution {
public:
// 辅助函数:在区间 [left, right] 构建平衡二叉搜索树
TreeNode* build(vector<int>& nums, int left, int right) {
// 1. 终止条件:左边界超过右边界,说明区间为空,返回空节点
if (right < left) return NULL;
// 2. 找到中间位置(避免溢出的写法)
int mid = left + (right - left) / 2;
// 3. 以中间元素创建根节点
TreeNode* root = new TreeNode(nums[mid]);
// 优化:如果只剩一个元素,直接返回,减少不必要的递归调用
if (left == right) return root;
// 4. 递归构建左子树(区间 [left, mid-1])
root->left = build(nums, left, mid - 1);
// 5. 递归构建右子树(区间 [mid+1, right])
root->right = build(nums, mid + 1, right);
return root;
}
TreeNode* sortedArrayToBST(vector<int>& nums) {
// 从整个数组的范围开始构建
return build(nums, 0, nums.size() - 1);
}
};
// 迭代法
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
if (nums.empty()) return NULL; // 处理空数组情况
// 准备三个队列,分别模拟递归过程中的:节点、左边界、右边界
queue<TreeNode*> nodeque; // 存储正在处理的节点
queue<int> leftnum; // 存储对应节点的左边界索引
queue<int> rightnum; // 存储对应节点的右边界索引
// 1. 初始化根节点(先占位,后续赋值)
TreeNode* root = new TreeNode(0);
nodeque.push(root);
leftnum.push(0);
rightnum.push(nums.size() - 1);
while (!nodeque.empty()) {
// 2. 取出当前要处理的节点及其对应的区间范围
TreeNode* cur = nodeque.front(); nodeque.pop();
int left = leftnum.front(); leftnum.pop();
int right = rightnum.front(); rightnum.pop();
// 3. 计算中间索引,并给当前节点赋值
int mid = left + (right - left) / 2;
cur->val = nums[mid];
// 4. 处理左孩子:如果左边还有元素
if (left <= mid - 1) {
cur->left = new TreeNode(0); // 创建左孩子占位
nodeque.push(cur->left); // 入队等待处理
leftnum.push(left); // 传递左边界
rightnum.push(mid - 1); // 传递右边界
}
// 5. 处理右孩子:如果右边还有元素
if (right >= mid + 1) {
cur->right = new TreeNode(0); // 创建右孩子占位
nodeque.push(cur->right); // 入队等待处理
leftnum.push(mid + 1); // 传递左边界
rightnum.push(right); // 传递右边界
}
}
return root;
}
};
总结
1. 解题原理:分治法
- 因为数组是有序的,要平衡,就必须让左右子树的节点数量尽可能相近。
- 因此,数组的中间元素自然而然就是根节点。
- 左半部分递归构建左子树,右半部分递归构建右子树。
2. 递归 vs 迭代
- 递归法:逻辑非常清晰直观,代码简洁。利用系统栈自动维护状态。
- 迭代法:模拟了递归的过程。因为递归需要三个参数(节点、左边界、右边界),迭代时需要用三个队列同步维护这些状态。步骤是:先创建节点占位 -> 放入队列 -> 取出时计算
mid并赋值 -> 将子节点入队。
3. 复杂度分析
- 时间复杂度:O(N)
- 每个节点都会被创建且只被处理一次。
- 空间复杂度:
- 递归法:O(log N),递归栈深度。
- 迭代法:O(N),队列存储节点所需空间。
538. 把二叉搜索树转换为累加树
给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点
node的新值等于原树中大于或等于node.val的值之和。提醒一下,二叉搜索树满足下列约束条件:
- 节点的左子树仅包含键 小于 节点键的节点。
- 节点的右子树仅包含键 大于 节点键的节点。
- 左右子树也必须是二叉搜索树。
cpp
// 递归法(反中序遍历)
class Solution {
public:
int sum = 0; // 全局变量,记录累加和
TreeNode* convertBST(TreeNode* root) {
if (root == NULL) return NULL;
// 1. 递归右子树
// 按照"右-中-左"的顺序,先处理最大的节点
root->right = convertBST(root->right);
// 2. 处理当前节点(中)
// 累加当前节点的值到 sum
sum += root->val;
// 将当前节点的值更新为累加和
root->val = sum;
// 3. 递归左子树
// 最后处理较小的节点,此时 sum 已经包含了所有比它大的节点值
root->left = convertBST(root->left);
return root;
}
};
// 迭代法(栈模拟反中序)
class Solution {
public:
TreeNode* convertBST(TreeNode* root) {
if (root == NULL) return NULL;
stack<TreeNode*> st;
TreeNode* cur = root; // 工作指针
int sum = 0; // 累加和
// 迭代进行中序遍历(右 -> 中 -> 左)
while (cur || !st.empty()) {
// 1. 一路向右,将路径上的节点入栈
// 这样栈顶元素就是当前子树中最大的节点
if (cur) {
st.push(cur);
cur = cur->right;
}
else {
// 2. 弹出栈顶元素处理
cur = st.top();
st.pop();
// 【核心逻辑】累加并更新节点值
sum += cur->val;
cur->val = sum;
// 3. 转向左子树
// 处理比当前节点小的部分
cur = cur->left;
}
}
return root;
}
};
总结
1. 解题原理:反中序遍历
- 二叉搜索树(BST)的中序遍历是递增的。
- 如果我们把这个顺序反过来:右 -> 中 -> 左,那么遍历序列就是递减的。
- 既然序列是递减的,我们遍历到的每个节点,它的值一定比之前遍历过的所有节点的值都要小。
- 因此,我们只需要维护一个
sum变量,记录之前所有节点值的累加和。每遍历到一个新节点,就把sum加到当前节点上,就得到了"大于等于它的所有值之和"。
2. 为什么要在 cur->left 之前更新 sum?
因为是反中序遍历(降序):
- 先访问右子树(更大的值)。
- 回到当前节点,此时
sum包含了右子树所有节点的和。 - 更新当前节点:
cur->val = sum + cur->val。 - 再访问左子树(更小的值)。左子树的节点需要加上
sum(此时已经包含了右子树和当前节点的和)。
3. 复杂度分析
- 时间复杂度:O(N)
- 每个节点只被访问一次。
- 空间复杂度:O(N)
- 递归法取决于栈深度,迭代法取决于显式栈大小。最坏情况(链状树)为 O(N)。
二叉树章节总结
一、二叉树基础
1. 二叉树的种类
- 满二叉树:所有叶子节点都在同一层,且每个非叶子节点都有两个孩子。
- 完全二叉树:除了最后一层,其他层都是满的,且最后一层的节点从左到右排列。
- 二叉搜索树(BST):左子树所有节点值 < 根节点值 < 右子树所有节点值。
- 平衡二叉树(AVL):每个节点的左右子树高度差不超过1。
2. 二叉树的存储方式
- 链式存储:使用指针(left, right)连接节点,最常用。
- 顺序存储:使用数组,节点 i 的左孩子为 2i+1,右孩子为 2i+2。
3. 二叉树的遍历方式
- 深度优先遍历(DFS):
- 前序遍历(根左右)
- 中序遍历(左根右)
- 后序遍历(左右根)
- 广度优先遍历(BFS):
- 层序遍历
二、二叉树的遍历专题
1. 递归遍历
cpp
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 前序
traversal(cur->left, vec); // 左
// vec.push_back(cur->val); // 中序
traversal(cur->right, vec); // 右
// vec.push_back(cur->val); // 后序
}
2. 迭代遍历
- 前序/后序:使用栈,注意入栈顺序(前序:先右后左;后序:先左后右再反转)。
- 中序:使用指针+栈,一路向左压栈,处理节点后转向右子树。
3. 层序遍历
cpp
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
if (root) que.push(root);
vector<vector<int>> result;
while (!que.empty()) {
int size = que.size();
vector<int> vec;
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(vec);
}
return result;
}
4. 经典题目
- 144.二叉树的前序遍历
- 145.二叉树的后序遍历
- 94.二叉树的中序遍历
- 102.二叉树的层序遍历
- 107.二叉树的层次遍历II
- 199.二叉树的右视图
- 637.二叉树的层平均值
- 429.N叉树的层序遍历
- 515.在每个树行中找最大值
- 116.填充每个节点的下一个右侧节点指针
- 117.填充每个节点的下一个右侧节点指针II
- 104.二叉树的最大深度
- 111.二叉树的最小深度
三、二叉树的属性专题
1. 对称性判断
- 对称二叉树:比较左右子树是否镜像对称(递归或迭代)。
- 翻转二叉树:交换每个节点的左右孩子。
2. 深度与平衡
- 最大深度:后序遍历,求左右子树最大深度+1。
- 最小深度:注意单子树情况,不能简单取min。
- 平衡二叉树:后序遍历,计算高度差,超过1则返回-1。
3. 节点与路径
- 完全二叉树节点数:利用完全二叉树性质,递归判断满二叉树情况。
- 左叶子之和:通过父节点判断左叶子。
- 找树左下角的值:层序遍历最后一层第一个,或前序遍历记录最大深度。
4. 经典题目
- 226.翻转二叉树
- 101.对称二叉树
- 100.相同的树
- 572.另一个树的子树
- 110.平衡二叉树
- 222.完全二叉树的节点个数
- 404.左叶子之和
- 513.找树左下角的值
四、二叉树的修改与构造
1. 构造二叉树
- 从中序与后序遍历序列构造二叉树:
- 后序最后一个为根节点。
- 在中序中找到根节点,切割为左右子树。
- 递归处理左右子树。
- 最大二叉树:在数组中找最大值作为根,递归构建左右子树。
2. 合并二叉树
- 同时遍历两棵树,对应位置节点值相加,缺失的位置用另一棵树补齐。
3. 经典题目
- 106.从中序与后序遍历序列构造二叉树
- 105.从前序与中序遍历序列构造二叉树
- 654.最大二叉树
- 617.合并二叉树
五、二叉搜索树(BST)专题
1. BST的性质
- 左子树所有节点值 < 根节点值 < 右子树所有节点值。
- 中序遍历是递增序列。
2. BST的验证与属性
- 验证二叉搜索树:中序遍历,判断是否递增;或递归时传递上下界。
- 二叉搜索树中的搜索:根据值大小决定方向,O(H)时间复杂度。
- 最小绝对差:中序遍历,记录前驱节点,计算差值。
- 众数:中序遍历,统计出现频率。
3. BST的增删改查
- 插入节点:找到空位置插入,递归或迭代。
- 删除节点:
- 叶子节点:直接删除。
- 单孩子:孩子接替位置。
- 双孩子:用前驱或后继节点替换,或重构子树。
- 修剪二叉搜索树:根据区间修剪,注意跨越不合格节点。
- 将有序数组转换为BST:取中间为根,递归构建。
4. BST的转换
- 累加树:反中序遍历,累加和更新节点值。
5. 经典题目
- 98.验证二叉搜索树
- 700.二叉搜索树中的搜索
- 530.二叉搜索树的最小绝对差
- 501.二叉搜索树中的众数
- 701.二叉搜索树中的插入操作
- 450.删除二叉搜索树中的节点
- 669.修剪二叉搜索树
- 108.将有序数组转换为二叉搜索树
- 538.把二叉搜索树转换为累加树
六、二叉树公共祖先问题
1. 二叉树的最近公共祖先
- 后序遍历,自底向上查找。
- 若左右子树都找到,当前节点就是LCA。
- 若只有一边找到,返回找到的那一边。
2. 二叉搜索树的最近公共祖先
- 利用BST性质,从根节点开始:
- 若p、q都小于当前,向左;
- 若p、q都大于当前,向右;
- 否则当前节点就是LCA。
3. 经典题目
- 236.二叉树的最近公共祖先
- 235.二叉搜索树的最近公共祖先
七、方法总结与技巧
1. 递归三部曲
- 确定递归函数的参数和返回值。
- 确定终止条件。
- 确定单层递归逻辑。
2. 迭代法
- 栈模拟递归:显式使用栈保存节点状态。
- 队列模拟层序:BFS遍历。
3. 双指针技巧
- 在BST中序遍历时,使用
pre指针记录前驱节点。
4. 回溯思想
- 在路径问题中,记录当前路径,回溯时弹出节点。
5. 分治思想
- 在构造二叉树时,通过分割数组区间递归构建子树。
八、经典代码模板
1. 递归模板
cpp
TreeNode* recursion(TreeNode* root, ...) {
// 终止条件
if (root == nullptr) return nullptr;
// 单层逻辑
// ...
// 递归调用
root->left = recursion(root->left, ...);
root->right = recursion(root->right, ...);
return root;
}
2. 迭代模板(前序)
cpp
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root == nullptr) return result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
result.push_back(node->val);
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
}
return result;
}
3. BST删除节点模板
cpp
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) return root;
if (root->val == key) {
if (root->right == nullptr) { // 只有左孩子或叶子
return root->left;
}
TreeNode* cur = root->right;
while (cur->left) cur = cur->left; // 找右子树最左节点
cur->left = root->left; // 左子树挂到右子树最左
return root->right;
}
if (root->val > key) root->left = deleteNode(root->left, key);
if (root->val < key) root->right = deleteNode(root->right, key);
return root;
}
九、总结
- 递归返回值:注意是否需要接收返回值(如插入、删除操作)。
- 迭代法中入栈顺序:前序遍历入栈先右后左,保证处理顺序。
- 层序遍历分层:使用
size记录当前层节点数,一次性处理完一层。 - BST删除节点:注意双孩子情况,需要重构子树。
- 公共祖先问题:注意终止条件和返回值的传递。
- 构造二叉树:注意数组切割区间的开闭情况。