代码随想录算法训练营 Day15 | 二叉树 part05

654. 最大二叉树

给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums递归地构建:

  1. 创建一个根节点,其值为 nums 中的最大值。
  2. 递归地在最大值 左边 的 子数组前缀上 构建左子树。
  3. 递归地在最大值 右边 的 子数组后缀上 构建右子树。

返回 nums 构建的 最大二叉树

cpp 复制代码
class Solution {
public:
    // dfs 递归构建函数
    // 参数说明:数组、当前区间的左边界、当前区间的右边界
    TreeNode* buildTree(vector<int>& nums, int left, int right) {
        // 1. 终止条件:如果左边界超过右边界,说明区间无效,返回空
        if (right < left) return NULL;
        // 2. 寻找当前区间的最大值及其下标
        // 初始化最大值为左边界元素,最大值下标为 left
        int val = nums[left];
        int index = left;
        // 遍历区间寻找最大值
        for (int i = left + 1; i <= right; i++) {
            if (nums[i] > val) { 
                val = nums[i]; // 更新最大值
                index = i;     // 更新最大值下标
            }
        }
        // 3. 创建根节点(最大值节点)
        TreeNode* root = new TreeNode(val);
        // 4. 剪枝:如果区间只有一个元素,它就是叶子节点,直接返回
        if (left == right) return root;
        // 5. 递归构建左子树
        // 区间范围:[left, index - 1] (最大值左边的部分)
        root->left = buildTree(nums, left, index - 1);
        // 6. 递归构建右子树
        // 区间范围:[index + 1, right] (最大值右边的部分)
        root->right = buildTree(nums, index + 1, right);
        return root;
    }
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        return buildTree(nums, 0, nums.size() - 1);
    }
};

总结

1. 解题思路:分治法
  1. 找根:在当前数组区间中找到最大值,它就是根节点。
  2. 分割:最大值左边的部分是左子树,右边的部分是右子树。
  3. 递归:对左右两部分重复上述过程。
2. 细节分析
  • 寻找最大值:代码中使用线性扫描 for 循环查找,逻辑清晰。
  • 区间划分:
    • 左子树区间:[left, index - 1]
    • 右子树区间:[index + 1, right]
    • 这与二叉树的前序/中序构造逻辑一致,关键在于确定 index(分割点)。
3. 复杂度分析
  • 时间复杂度:O(N²)
    • 最坏情况:数组本身有序(如 [1, 2, 3, 4, 5]),每次只能切分出一个右节点,递归深度为 N,每层都要扫描剩余数组,总和为 N+(N−1)+...+1≈O(N2)
  • 空间复杂度:O(N)
    • 主要是递归调用栈的开销,最坏情况(有序数组)深度为 N。

617. 合并二叉树

给你两棵二叉树: root1root2

想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。

返回合并后的二叉树。

注意: 合并过程必须从两个树的根节点开始。

cpp 复制代码
class Solution {
public:
    // merge 函数:递归合并两棵树
    // 返回值:合并后的新树的根节点
    TreeNode* merge(TreeNode* root1, TreeNode* root2) {
        // 1. 终止条件(处理空节点的情况)
        // 如果树1为空,直接返回树2(不管树2是空还是有节点,都直接接过去)
        if (!root1) return root2;
        // 如果树2为空,直接返回树1
        else if (!root2) return root1;
        // 2. 处理当前节点
        // 两棵树都不为空,创建新节点,值为两者之和
        TreeNode* root = new TreeNode(root1->val + root2->val);
        // 3. 递归合并左右子树
        // 这里体现了"同步遍历"的思想:两棵树同时向左走,同时向右走
        root->left = merge(root1->left, root2->left);
        root->right = merge(root1->right, root2->right);
        // 4. 返回合并后的节点
        return root;
    }
    TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
        return merge(root1, root2);
    }
};

总结

1. 解题思路:同步递归

这道题的逻辑非常清晰,就是两棵树"同位置节点相加"。

  • 同步移动:merge(root1->left, root2->left) 保证了两棵树在同一层级、同一位置进行操作。
  • 新树构建:这是一个构造新树的过程,通过 new TreeNode(...) 创建新节点,而不是修改原有的树(虽然修改原有树也可以解题,但新建树更安全、逻辑更清晰)。
3. 复杂度分析
  • 时间复杂度:O(N)
    • N 是两棵树中节点数量的最小值。因为只要任意一棵树遍历完了(遇到空节点),递归就会直接返回。每个节点只访问一次。
  • 空间复杂度:O(N)
    • 取决于递归调用栈的深度。最坏情况(树退化为链表)为 O(N)。

700. 二叉搜索树中的搜索

给定二叉搜索树(BST)的根节点 root 和一个整数值 val

你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null

cpp 复制代码
// 方法一
class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        // 1. 终止条件:节点为空,说明没找到,返回 NULL
        if (root == NULL) return NULL;
        // 2. 单层搜索逻辑
        // 目标值比当前节点大,根据 BST 性质,去右子树搜索
        if (root->val < val) {
            return searchBST(root->right, val);
        }
        // 目标值比当前节点小,去左子树搜索
        else if (root->val > val) {
            return searchBST(root->left, val);
        }
        // 3. 找到目标值,直接返回当前节点
        return root;
    }
};

// 方法二
class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        // 只要节点不为空,就继续向下搜索
        while (root) {
            // 目标值小于当前节点,向左走
            if (root->val > val) {
                root = root->left;
            }
            // 目标值大于当前节点,向右走
            else if (root->val < val) {
                root = root->right;
            }
            // 找到了,直接返回当前节点
            else {
                return root;
            }
        }
        // 循环结束(root 为空),说明遍历到叶子也没找到,返回 NULL
        return NULL;
    }
};

总结

1. 解题思路对比

这道题利用了二叉搜索树(BST)的核心性质:左 < 根 < 右。

  • 递归法:逻辑非常直观,利用系统栈来保存状态。代码简洁,符合直觉。
  • 迭代法:因为 BST 的搜索路径是单向的(不需要回溯),所以非常适合使用迭代(循环)来实现。
2. 复杂度分析
  • 时间复杂度:O(H)
    • H 是树的高度。平均情况 O(log N),最坏情况 O(N)。因为每次比较都能排除一半的子树。
  • 空间复杂度:
    • 递归:O(H)(栈空间)。
    • 迭代:O(1)。

98. 验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 严格小于 当前节点的数。
  • 节点的右子树只包含 严格大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。
cpp 复制代码
// 递归法(存数组)
class Solution {
public:
    vector<int> ans; // 存储中序遍历的结果
    // 中序遍历递归函数
    void traversal(TreeNode* root) {
        if (root == NULL) return;
        traversal(root->left);      // 左
        ans.push_back(root->val);   // 中:将节点值存入数组
        traversal(root->right);     // 右
    }
    bool isValidBST(TreeNode* root) {
        // 1. 执行中序遍历
        traversal(root);
        // 2. 检查遍历结果是否严格递增
        // BST 的中序遍历结果应该是一个严格递增的序列
        for (int i = 1; i < ans.size(); i++) {
            // 如果后一个元素 <= 前一个元素,说明不是 BST
            if (ans[i] <= ans[i - 1]) return false;
        }
        return true;
    }
};

// 递归法(双指针)
class Solution {
public:
    TreeNode* pre = NULL; // 指针,用于记录遍历过程中的前一个节点
    bool isValidBST(TreeNode* root) {
        if (root == NULL) return true;
        // 1. 递归左子树
        bool left = isValidBST(root->left);
        // 2. 处理当前节点(中序遍历的位置)
        // 如果前驱节点存在,且前驱值 >= 当前值,违反 BST 规则
        if (pre && pre->val >= root->val) return false;
        // 更新前驱节点为当前节点
        pre = root;
        // 3. 递归右子树
        bool right = isValidBST(root->right);
        // 左右子树都合法才返回 true
        return left && right;
    }
};

// 迭代法(栈模拟中序)
class Solution {
public:
    bool isValidBST(TreeNode* root) {
        if (root == NULL) return true;
        stack<TreeNode*> st; // 栈,用于模拟递归调用栈
        TreeNode* cur = root; // 工作指针
        TreeNode* pre = NULL; // 记录前一个访问的节点
        // 循环条件:节点没遍历完 或 栈不为空
        while (cur || !st.empty()) {
            // 1. 模拟递归深入左子树
            if (cur) {
                st.push(cur);   // 将访问过的节点入栈
                cur = cur->left; // 继续向左
            }
            // 2. 左边走到头了,开始处理节点
            else {
                cur = st.top(); // 弹出栈顶元素(即当前最左节点)
                st.pop();
                // 【核心判断】比较当前节点和前驱节点
                if (pre && pre->val >= cur->val) return false;
                pre = cur;       // 更新前驱指针
                cur = cur->right; // 转向右子树
            }
        }
        return true;
    }
};

总结

1. 核心原理:中序遍历特性

二叉搜索树(BST)的一个重要性质是:中序遍历序列是一个严格递增的序列。这三种方法都是基于这个原理实现的。

2. 方法对比
  • 方法一(存数组):
    • 优点:逻辑最简单,容易理解。
    • 缺点:空间复杂度高。需要额外的 O(N) 空间存储数组,且需要二次遍历数组。
  • 方法二(递归双指针):
    • 优点:空间复杂度较优(仅需递归栈空间),在遍历的同时直接比较,不需要存全部节点。
    • 缺点:需要定义全局变量 pre,面试时要注意初始化问题。
  • 方法三(迭代法):
    • 优点:最优解。既避免了递归栈溢出的风险,又实现了 O(1) 的额外空间(不算栈空间),逻辑紧凑,一次遍历即可完成判断。
3. 关键细节:pre 指针

在方法二和方法三中,pre 指针代表中序遍历过程中的前一个节点。

  • 判断条件必须是 pre->val >= cur->val,因为 BST 要求严格递增,等于的情况也是非法的。
  • 在处理当前节点后,必须更新 pre = cur,为下一次比较做准备。
4. 复杂度分析
  • 时间复杂度:O(N)。无论哪种方法,都需要遍历所有节点一次。
  • 空间复杂度:
    • 方法一:O(N)(数组 + 递归栈)。
    • 方法二:O(H)(递归栈,H 为树高)。
    • 方法三:O(H)(显式栈,H 为树高)。
相关推荐
代码栈上的思考1 小时前
消息队列持久化:文件存储设计与实现全解析
java·前端·算法
qq_417695052 小时前
内存对齐与缓存友好设计
开发语言·c++·算法
2301_816651222 小时前
实时系统下的C++编程
开发语言·c++·算法
2401_831824962 小时前
C++与Python混合编程实战
开发语言·c++·算法
2301_816651222 小时前
C++中的策略模式高级应用
开发语言·c++·算法
LDR0062 小时前
如何使用OpenClaw提高工作效率?
数据结构·算法
liuyao_xianhui2 小时前
优选算法_模拟_替换所有的‘?‘_C++
开发语言·javascript·数据结构·c++·算法·链表·动态规划
币之互联万物2 小时前
LLM 偏好算法解析:大语言模型内容收录倾向与 NEOXGEO 技术底蕴
人工智能·算法·语言模型