【LeetCode Hot100 刷题日记 (42/100)】108. 将有序数组转换为二叉搜索树 ——树、二叉搜索树、数组、分治、递归🌳

6 阅读5分钟

📌 题目链接:108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)

🔍 难度:简单 | 🏷️ 标签:树、二叉搜索树、数组、分治、递归

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(log n) (递归栈深度)


🧠 题目分析

题目要求将一个严格升序排列的整数数组转换为一棵高度平衡的二叉搜索树(BST)

  • 二叉搜索树(BST)性质:对任意节点,其左子树所有节点值 < 当前节点值 < 右子树所有节点值。
  • 高度平衡定义:每个节点的左右两个子树的高度差的绝对值不超过 1。
  • 关键观察BST 的中序遍历结果是升序序列。因此,给定的数组其实就是目标 BST 的中序遍历结果。

但仅凭中序遍历无法唯一确定一棵 BST(因为根节点可以任选)。然而,若加上“高度平衡”这一限制,我们就可以通过每次选择中间元素作为根,确保左右子树节点数量尽可能相等,从而自然满足平衡性。

✅ 这正是分治思想的经典应用场景:将大问题拆解为结构相同的子问题,递归求解。


🔑 核心算法及代码讲解:分治 + 递归构建

本题的核心在于 “如何从有序数组中递归地构造一棵平衡 BST”

📌 算法思想(分治策略)

  1. 选择根节点:在当前子数组 [left, right] 中,选择中间位置的元素作为根节点。

    • 若长度为奇数:中间唯一;
    • 若长度为偶数:可选左中或右中(两种都合法,LeetCode 接受任意一种)。
  2. 递归构建左右子树

    • 左子树由 [left, mid - 1] 构建;
    • 右子树由 [mid + 1, right] 构建。
  3. 递归终止条件:当 left > right 时,返回 nullptr(空树)。

💡 为什么这样能保证“平衡”?

每次将数组近乎均分,左右子树的节点数最多相差 1,因此树的高度为 ⌊log₂n⌋ + 1,天然满足 AVL 树的平衡条件(高度差 ≤ 1)。

🧩 三种实现方式(官方题解总结)

方法根节点选择公式
方法一中间偏左mid = (left + right) / 2
方法二中间偏右mid = (left + right + 1) / 2
方法三随机中间mid = (left + right + rand() % 2) / 2

⚠️ 注意:C++ 中 (a + b) / 2 是向下取整(floor division),而 (a + b + 1) / 2 是向上取整(ceil division)。

我们采用方法一(中间偏左) ,这是最常见、最简洁的写法。

✅ C++ 核心代码(带详细行注释)

TreeNode* helper(vector<int>& nums, int left, int right) {
    // 基线条件:区间无效,返回空节点
    if (left > right) {
        return nullptr;
    }

    // 选择中间偏左的位置作为根(向下取整)
    int mid = (left + right) / 2;

    // 创建当前根节点
    TreeNode* root = new TreeNode(nums[mid]);

    // 递归构建左子树:使用左半部分 [left, mid - 1]
    root->left = helper(nums, left, mid - 1);

    // 递归构建右子树:使用右半部分 [mid + 1, right]
    root->right = helper(nums, mid + 1, right);

    // 返回当前子树的根
    return root;
}

🎯 面试高频考点

  • 能否解释为何此方法构造的树一定是平衡的?
  • 如何避免整数溢出?(虽然本题 n ≤ 1e4 不会溢出,但可提 mid = left + (right - left) / 2 更安全)
  • 时间/空间复杂度分析是否清晰?

🧭 解题思路(分步拆解)

  1. 理解输入输出

    • 输入:升序数组 nums
    • 输出:一棵平衡 BST 的根节点指针
  2. 利用 BST 中序特性

    • 升序数组 = BST 的中序遍历 → 可反向构造
  3. 应用分治策略

    • 每次选中点作根,左右递归
  4. 处理边界情况

    • 空数组 → 返回 nullptr
    • 单元素 → 叶子节点
  5. 验证平衡性

    • 因每次均分,树高 ≈ log n → 自动平衡

📊 算法分析

项目分析
时间复杂度O(n):每个元素恰好被访问一次,用于创建节点
空间复杂度O(log n):递归调用栈的深度等于树高(平衡树高为 log n) ⚠️ 注意:不包括返回树所占的 O(n) 空间
稳定性方法一和方法二结果不同,但都合法;方法三具有随机性
扩展思考若数组无序?→ 需先排序(O(n log n)),再构造

💼 面试加分项

  • 提到“这与快速排序的分治思想类似,但这里是构造而非排序”
  • 对比“自顶向下(本题) vs 自底向上(如链表转 BST)”的构造方式
  • 引申到 “1382. 将二叉搜索树变平衡” —— 先中序遍历得有序数组,再用本题方法重建!

💻 完整代码

✅ C++ 实现

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// Definition for a binary tree node.
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};

class Solution {
public:
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        return helper(nums, 0, nums.size() - 1);
    }

private:
    TreeNode* helper(vector<int>& nums, int left, int right) {
        if (left > right) {
            return nullptr;
        }
        int mid = (left + right) / 2;
        TreeNode* root = new TreeNode(nums[mid]);
        root->left = helper(nums, left, mid - 1);
        root->right = helper(nums, mid + 1, right);
        return root;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    
    // 测试用例 1
    vector<int> nums1 = {-10, -3, 0, 5, 9};
    TreeNode* root1 = sol.sortedArrayToBST(nums1);
    // 验证输出结构(略,LeetCode 自动判)

    // 测试用例 2
    vector<int> nums2 = {1, 3};
    TreeNode* root2 = sol.sortedArrayToBST(nums2);

    // 可添加层序遍历打印函数辅助调试(此处省略)

    return 0;
}

✅ JavaScript 实现

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */

/**
 * @param {number[]} nums
 * @return {TreeNode}
 */
var sortedArrayToBST = function(nums) {
    const helper = (left, right) => {
        if (left > right) return null;
        
        const mid = Math.floor((left + right) / 2);
        const root = new TreeNode(nums[mid]);
        
        root.left = helper(left, mid - 1);
        root.right = helper(mid + 1, right);
        
        return root;
    };
    
    return helper(0, nums.length - 1);
};

🔁 JS 注意点:

  • 使用 Math.floor() 显式向下取整(虽然 / 在整数下也 trunc,但更清晰)
  • 闭包 helper 函数避免全局污染

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!