📖 第45课:有序数组转BST

1 阅读13分钟

想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。

📖 第45课:有序数组转BST

模块:二叉树 | 难度:Easy ⭐⭐ LeetCode 链接:leetcode.cn/problems/co… 前置知识:二叉搜索树基础、递归、二分思想 预计学习时间:20分钟


🎯 题目描述

给你一个整数数组 nums,其中元素已按升序排列,请将其转换为一棵高度平衡的二叉搜索树(BST)。

高度平衡二叉树是指:一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1。

示例:

输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:可以构造如下BST(答案不唯一):
      0
     / \
   -3   9
   /   /
 -10  5

约束条件:

  • 1 ≤ nums.length ≤ 10⁴
  • -10⁴ ≤ nums[i] ≤ 10⁴
  • nums 按严格递增顺序排列

🧪 边界用例(面试必考)

用例类型输入期望输出考察点
单元素nums = [0]TreeNode(0)基本功能
两元素nums = [1,3]TreeNode(3, TreeNode(1))左右子树选择
奇数长度nums = [1,2,3,4,5]根节点为3中点选择
偶数长度nums = [1,2,3,4]根节点为2或3中点有两个选择

💡 思路引导

生活化比喻

想象你在组织一场拔河比赛,要把队员按身高分成两队,并确保两队人数尽量平衡:

🐌 笨办法:随机选一个人当队长,其他人按高矮分左右队。但这样可能一队10人,一队只有2人,不平衡!

🚀 聪明办法:把所有人按身高排成一列(已经排好了!),选中间的人当队长,左边的自动成为左队,右边的成为右队。这样两队人数最接近!然后递归地在左右队中再选队长...最终得到一个完全平衡的组织结构。

关键洞察

有序数组的中点元素作为根节点,左半部分构建左子树,右半部分构建右子树,递归进行


🧠 解题思维链

这一节模拟你在面试中"从零开始思考"的过程。

Step 1:理解题目 → 锁定输入输出

  • 输入:有序数组 nums(严格递增)
  • 输出:TreeNode 根节点,构成高度平衡的BST
  • 限制:必须是BST(左<根<右)且高度平衡(左右子树高度差≤1)

Step 2:先想笨办法(贪心法)

选第一个元素作为根,剩余元素递归构建右子树。但这样会退化成链表,完全不平衡。

  • 时间复杂度:O(n)
  • 瓶颈在哪:无法保证平衡性,左子树为空,高度变为O(n)

Step 3:瓶颈分析 → 优化方向

  • 核心问题:如何选根节点才能让左右子树"平衡"?
  • 优化思路:选中点作为根,左右两边元素数量最接近,天然平衡

Step 4:选择武器

  • 选用:递归 + 二分思想
  • 理由:有序数组的中点分割正好对应BST的平衡划分,递归结构天然匹配树的构建

🔑 模式识别提示:当题目给定"有序数组"+"构建平衡树"时,立即想到"取中点递归"模式


🔑 解法一:递归分治(中点选择)

思路

每次选择数组中点作为根节点,左半部分递归构建左子树,右半部分递归构建右子树。使用左右边界索引避免数组切片。

图解过程

示例:nums = [-10,-3,0,5,9]
索引:      0   1  2 3 4

Step 1:选择中点 mid=2, root=0
  [-10, -3, 0, 5, 9]
           ↑
  左半:[0,1][-10,-3]
  右半:[3,4][5,9]

        0
       / \
     ?   ?

Step 2:处理左半[-10,-3], mid=0, root=-3
  [-10, -3]
        ↑
  左半:[-10] (索引0到-1,不存在,为None)
  右半:无

       -3
       /
     -10

Step 3:处理右半[5,9], mid=3, root=5
  [5, 9]
   ↑
  左半:无
  右半:[9]

        5
         \
          9

Step 4:组合结果
        0
       / \
     -3   9
     /   /
   -10  5

✓ 左右子树高度差≤1,平衡BST

Python代码

from typing import List, Optional


class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right


def sortedArrayToBST_v1(nums: List[int]) -> Optional[TreeNode]:
    """
    解法一:递归分治(选择左中点)
    思路:每次选中点作为根,递归构建左右子树
    """
    def build(left, right):
        """递归构建[left, right]区间的BST"""
        if left > right:
            return None

        # 选择中点(偶数长度时选左中点)
        mid = (left + right) // 2

        # 创建根节点
        root = TreeNode(nums[mid])

        # 递归构建左右子树
        root.left = build(left, mid - 1)
        root.right = build(mid + 1, right)

        return root

    return build(0, len(nums) - 1)


# ✅ 测试
nums1 = [-10, -3, 0, 5, 9]
root1 = sortedArrayToBST_v1(nums1)
# 中序遍历验证BST性质
def inorder(node):
    if not node:
        return []
    return inorder(node.left) + [node.val] + inorder(node.right)

print(inorder(root1))  # 期望输出:[-10, -3, 0, 5, 9](有序)

nums2 = [1, 3]
root2 = sortedArrayToBST_v1(nums2)
print(inorder(root2))  # 期望输出:[1, 3]

复杂度分析

  • 时间复杂度:O(n) — 每个元素创建一次节点
    • 具体地说:如果数组有100个元素,需要创建恰好100个TreeNode
  • 空间复杂度:O(log n) — 递归栈深度(树高度)
    • 平衡树高度约为log₂(n),如n=1000时,栈深度约为10

优缺点

  • ✅ 思路清晰,代码简洁
  • ✅ 保证高度平衡
  • ⚠️ 偶数长度数组时,选择左中点或右中点都可以(答案不唯一)

🏆 解法二:迭代优化(右中点选择,最优解)

优化思路

与解法一完全相同,唯一区别是偶数长度数组时选择右中点 mid = (left + right + 1) // 2,使得答案更倾向于右侧平衡。

💡 关键想法:左中点和右中点都能保证平衡,选择哪个都是正确答案

图解过程

示例:nums = [1,2,3,4](偶数长度)

解法一(左中点):mid = (0+3)//2 = 1 → root=2
      2
     / \
    1   3
         \
          4

解法二(右中点):mid = (0+3+1)//2 = 2 → root=3
      3
     / \
    1   4
     \
      2

两者都是高度平衡的BST ✓

Python代码

def sortedArrayToBST(nums: List[int]) -> Optional[TreeNode]:
    """
    🏆 解法二:递归分治(选择右中点,最优解)
    思路:与解法一相同,偶数时选右中点
    """
    def build(left, right):
        if left > right:
            return None

        # 选择右中点(偶数长度时选右中点)
        mid = (left + right + 1) // 2

        root = TreeNode(nums[mid])
        root.left = build(left, mid - 1)
        root.right = build(mid + 1, right)

        return root

    return build(0, len(nums) - 1)


# ✅ 测试
nums1 = [-10, -3, 0, 5, 9]
root1 = sortedArrayToBST(nums1)
print(inorder(root1))  # 期望输出:[-10, -3, 0, 5, 9]

nums2 = [1, 3]
root2 = sortedArrayToBST(nums2)
print(inorder(root2))  # 期望输出:[1, 3]

nums3 = [1]
root3 = sortedArrayToBST(nums3)
print(inorder(root3))  # 期望输出:[1]

复杂度分析

  • 时间复杂度:O(n) — 每个元素访问一次
  • 空间复杂度:O(log n) — 递归栈深度

🚀 解法三:数组切片版本(非最优,仅供理解)

思路

使用Python数组切片 nums[:mid]nums[mid+1:] 直接传递子数组,代码更简洁但空间效率低。

Python代码

def sortedArrayToBST_slice(nums: List[int]) -> Optional[TreeNode]:
    """
    解法三:数组切片版(非最优)
    思路:直接切片传递子数组,代码简洁但空间O(nlogn)
    """
    if not nums:
        return None

    mid = len(nums) // 2
    root = TreeNode(nums[mid])

    # 直接切片传递(会创建新数组)
    root.left = sortedArrayToBST_slice(nums[:mid])
    root.right = sortedArrayToBST_slice(nums[mid + 1:])

    return root


# ✅ 测试
nums = [-10, -3, 0, 5, 9]
root = sortedArrayToBST_slice(nums)
print(inorder(root))  # 期望输出:[-10, -3, 0, 5, 9]

复杂度分析

  • 时间复杂度:O(n log n) — 每层递归O(n)复制数组,共log n层
  • 空间复杂度:O(n log n) — 切片创建的新数组空间

优缺点

  • ✅ 代码极简,易于理解
  • ❌ 空间效率低,不推荐面试使用

🐍 Pythonic 写法

利用Python的三元表达式和列表切片,可以写成一行(不推荐):

# 方法:递归一行流(仅供炫技,可读性差)
def sortedArrayToBST_oneliner(nums: List[int]) -> Optional[TreeNode]:
    return (TreeNode(nums[mid := len(nums) // 2],
                     sortedArrayToBST_oneliner(nums[:mid]),
                     sortedArrayToBST_oneliner(nums[mid + 1:]))
            if nums else None)

这个写法使用了Python 3.8的海象运算符 := 和三元表达式,虽然简洁但牺牲了可读性。

⚠️ 面试建议:先写清晰版本展示思路,再提 Pythonic 写法展示语言功底。 面试官更看重你的思考过程,而非代码行数。


📊 解法对比

维度解法一:左中点🏆 解法二:右中点(最优)解法三:切片版
时间复杂度O(n)O(n) ← 最优O(n log n)
空间复杂度O(log n)O(log n) ← 最优O(n log n)
代码难度简单简单极简
面试推荐⭐⭐⭐⭐⭐⭐ ← 首选
适用场景通用面试标准解法仅供理解

为什么解法二是最优解:

  • 时间O(n)已经是理论最优(必须访问每个元素一次)
  • 空间O(log n)是递归栈的最小开销,无法避免
  • 使用索引而非切片,避免了额外的数组复制开销
  • 代码清晰,面试中容易写对
  • 选择右中点符合LeetCode官方答案习惯

面试建议:

  1. 开口就说:"有序数组转BST用递归+中点分治是标准解法"
  2. 画图演示如何选中点并递归构建左右子树
  3. 重点讲解核心思想:"中点作为根保证左右子树元素数量最接近,递归保证子树也平衡"
  4. 强调为什么平衡:每次二分,左右子树高度差最多为1
  5. 提及答案不唯一:偶数长度时左右中点都可以
  6. 手动模拟测试用例,展示递归过程

🎤 面试现场

模拟面试中的完整对话流程,帮你练习"边想边说"。

面试官:请你将有序数组转换为高度平衡的BST。

:(审题20秒)好的,这道题给定有序数组,要求构建平衡BST。

我的思路是递归+二分:

  1. 选择数组中点作为根节点(保证左右子树元素数量最接近)
  2. 左半部分递归构建左子树
  3. 右半部分递归构建右子树
  4. 递归边界:空区间返回None

这样构建的树天然平衡,因为每次二分后左右子树高度差≤1。时间复杂度O(n),空间复杂度O(log n)递归栈。

面试官:很好,请写一下代码。

:(边写边说关键步骤)

def sortedArrayToBST(nums):
    def build(left, right):
        if left > right:  # 递归边界
            return None

        mid = (left + right) // 2  # 选中点
        root = TreeNode(nums[mid])

        # 递归构建左右子树
        root.left = build(left, mid - 1)
        root.right = build(mid + 1, right)

        return root

    return build(0, len(nums) - 1)

面试官:测试一下?

:用示例 [-10,-3,0,5,9] 走一遍:

  • mid=2选0作为根
  • 左半[-10,-3]递归:mid=0选-3,左子-10
  • 右半[5,9]递归:mid=3选5,右子9
  • 最终树高度为3,左右平衡 ✓

再测试边界:单元素[1]返回TreeNode(1),空数组(如果允许)返回None ✓,结果正确。

高频追问

追问应答策略
"为什么选中点一定平衡?""中点左右两边元素数量最接近(差≤1),递归构建的子树高度差也≤1,根据平衡定义满足要求。"
"偶数长度时选左中点还是右中点?""都可以,都能保证平衡。我的实现选择 (left+right)//2 即左中点,也可以 +1 选右中点。"
"能否迭代实现?""可以,但需要手动维护栈来模拟递归,代码复杂度大幅增加,不如递归直观,面试中递归是首选。"
"如果数组无序呢?""需要先排序O(n log n),然后再转换O(n),总时间O(n log n)。"

🎓 知识点总结

Python技巧卡片 🐍

# 技巧1:整数除法选中点 — 左中点 vs 右中点
left_mid = (left + right) // 2      # 左中点:[0,3]→1
right_mid = (left + right + 1) // 2  # 右中点:[0,3]→2

# 技巧2:海象运算符:= — 赋值同时使用(Python 3.8+)
if (n := len(nums)) > 0:
    mid = n // 2

# 技巧3:递归边界的优雅写法 — 提前返回
if left > right: return None
# 等价于:
if left <= right:
    # ...构建树
else:
    return None

💡 底层原理(选读)

为什么中点分治能保证平衡?

平衡树的定义:每个节点的左右子树高度差≤1。

数学证明:

  • 设区间长度为n,选中点后左半长度为⌊n/2⌋,右半长度为⌈n/2⌉
  • 两者差值≤1,满足元素数量平衡
  • 完全二叉树性质:n个节点的完全二叉树高度=⌊log₂n⌋+1
  • 因此左右子树高度差≤1,递归保证所有节点满足平衡条件

BST性质:中序遍历有序数组,根据定义左<根<右,自动满足BST

算法模式卡片 📐

  • 模式名称:递归分治(二分构建)
  • 适用条件:有序数组构建平衡树、归并排序、快速排序等需要二分的场景
  • 识别关键词:"有序数组"、"平衡"、"二分"、"递归构建"
  • 模板代码:
def divide_and_conquer(arr, left, right):
    # 递归边界
    if left > right:
        return None

    # 选择中点(分治点)
    mid = (left + right) // 2

    # 处理当前节点
    result = process(arr[mid])

    # 递归处理左右子问题
    result.left = divide_and_conquer(arr, left, mid - 1)
    result.right = divide_and_conquer(arr, mid + 1, right)

    return result

易错点 ⚠️

  1. 递归边界判断错误:使用 if not nums 而非 if left > right

    • ❌ 错误(切片版):if not nums: return None → 每次切片都要判断
    • ✅ 正确(索引版):if left > right: return None → 边界清晰
  2. 中点计算溢出(在其他语言如Java/C++):

    • ❌ 错误:mid = (left + right) / 2 → left+right可能溢出
    • ✅ 正确:mid = left + (right - left) // 2 → 避免溢出
  3. 忘记返回根节点:递归函数必须返回构建的节点

    • 💡 解决:每次递归调用都要 return build(...) 并赋值给 root.left/right

🏗️ 工程实战(选读)

这个算法思想在真实项目中的应用,让你知道"学了有什么用"。

  • 场景1:数据库索引 — 从有序数据构建B+树索引,保证查询O(log n)
  • 场景2:负载均衡 — 将有序服务器列表构建成平衡树,快速查找最优服务器
  • 场景3:游戏开发 — 从排序后的技能点数构建技能树,保证技能搜索效率
  • 场景4:文件系统 — 构建平衡的目录树,优化文件查找性能

🏋️ 举一反三

完成本课后,试试这些同类题目来巩固知识:

题目难度相关知识点提示
LeetCode 109. 有序链表转BSTMedium快慢指针找中点+递归链表版本,需要O(n)找中点
LeetCode 1382. 将BST变平衡Medium中序遍历+重建先中序得到有序数组,再用本题方法
LeetCode 617. 合并二叉树Easy递归合并相似的递归分治思想
LeetCode 654. 最大二叉树Medium递归分治选最大值作为根,递归构建

📝 课后小测

试试这道变体题,不要看答案,自己先想5分钟!

题目:给定有序数组 nums,构建一棵最小高度的BST(不要求完全平衡,只要求高度最小)。

💡 提示(实在想不出来再点开)

最小高度 = 平衡树的高度。本题解法完全相同!

✅ 参考答案
def minHeightBST(nums: List[int]) -> Optional[TreeNode]:
    """构建最小高度BST(与平衡BST解法相同)"""
    def build(left, right):
        if left > right:
            return None

        mid = (left + right) // 2
        root = TreeNode(nums[mid])
        root.left = build(left, mid - 1)
        root.right = build(mid + 1, right)

        return root

    return build(0, len(nums) - 1)


# 测试
nums = [1, 2, 3, 4, 5, 6, 7]
root = minHeightBST(nums)
# 树高度为3(log₂7 + 1),最小高度

核心思路:最小高度要求左右子树尽可能平衡,因此与本题"平衡BST"解法完全一致。中点分治天然保证高度最小。


如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。