代码随想录算法训练营 Day18 | 二叉树 part08

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

因为是反中序遍历(降序):

  1. 先访问右子树(更大的值)。
  2. 回到当前节点,此时 sum 包含了右子树所有节点的和。
  3. 更新当前节点:cur->val = sum + cur->val
  4. 再访问左子树(更小的值)。左子树的节点需要加上 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. 构造二叉树
  • 从中序与后序遍历序列构造二叉树:
    1. 后序最后一个为根节点。
    2. 在中序中找到根节点,切割为左右子树。
    3. 递归处理左右子树。
  • 最大二叉树:在数组中找最大值作为根,递归构建左右子树。
2. 合并二叉树
  • 同时遍历两棵树,对应位置节点值相加,缺失的位置用另一棵树补齐。
3. 经典题目
  • 106.从中序与后序遍历序列构造二叉树
  • 105.从前序与中序遍历序列构造二叉树
  • 654.最大二叉树
  • 617.合并二叉树

五、二叉搜索树(BST)专题

1. BST的性质
  • 左子树所有节点值 < 根节点值 < 右子树所有节点值。
  • 中序遍历是递增序列。
2. BST的验证与属性
  • 验证二叉搜索树:中序遍历,判断是否递增;或递归时传递上下界。
  • 二叉搜索树中的搜索:根据值大小决定方向,O(H)时间复杂度。
  • 最小绝对差:中序遍历,记录前驱节点,计算差值。
  • 众数:中序遍历,统计出现频率。
3. BST的增删改查
  • 插入节点:找到空位置插入,递归或迭代。
  • 删除节点:
    1. 叶子节点:直接删除。
    2. 单孩子:孩子接替位置。
    3. 双孩子:用前驱或后继节点替换,或重构子树。
  • 修剪二叉搜索树:根据区间修剪,注意跨越不合格节点。
  • 将有序数组转换为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. 递归三部曲
  1. 确定递归函数的参数和返回值。
  2. 确定终止条件。
  3. 确定单层递归逻辑。
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;
}

九、总结

  1. 递归返回值:注意是否需要接收返回值(如插入、删除操作)。
  2. 迭代法中入栈顺序:前序遍历入栈先右后左,保证处理顺序。
  3. 层序遍历分层:使用size记录当前层节点数,一次性处理完一层。
  4. BST删除节点:注意双孩子情况,需要重构子树。
  5. 公共祖先问题:注意终止条件和返回值的传递。
  6. 构造二叉树:注意数组切割区间的开闭情况。
相关推荐
hanlin033 小时前
刷题笔记:力扣第43、67题(字符串计算)
笔记·算法·leetcode
yang_B6213 小时前
最小二乘法 拟合平面
算法·平面·最小二乘法
放下华子我只抽RuiKe53 小时前
深度学习全景指南:硬核实战版
人工智能·深度学习·神经网络·算法·机器学习·自然语言处理·数据挖掘
吴秋霖3 小时前
【某音电商】protobuf聊天协议逆向
python·算法·protobuf
m0_587958954 小时前
C++中的命令模式变体
开发语言·c++·算法
似水এ᭄往昔4 小时前
【数据结构】--链表OJ
数据结构·算法·链表
2501_924952694 小时前
代码生成器优化策略
开发语言·c++·算法
MORE_774 小时前
leecode100-划分区间-贪心算法
算法·贪心算法
Book思议-5 小时前
【数据结构实战】C语言实现栈的链式存储:从初始化到销毁,手把手教你写可运行代码
数据结构·算法·链表··408