想系统提升编程能力、查看更完整的学习路线,欢迎访问 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官方答案习惯
面试建议:
- 开口就说:"有序数组转BST用递归+中点分治是标准解法"
- 画图演示如何选中点并递归构建左右子树
- 重点讲解核心思想:"中点作为根保证左右子树元素数量最接近,递归保证子树也平衡"
- 强调为什么平衡:每次二分,左右子树高度差最多为1
- 提及答案不唯一:偶数长度时左右中点都可以
- 手动模拟测试用例,展示递归过程
🎤 面试现场
模拟面试中的完整对话流程,帮你练习"边想边说"。
面试官:请你将有序数组转换为高度平衡的BST。
你:(审题20秒)好的,这道题给定有序数组,要求构建平衡BST。
我的思路是递归+二分:
- 选择数组中点作为根节点(保证左右子树元素数量最接近)
- 左半部分递归构建左子树
- 右半部分递归构建右子树
- 递归边界:空区间返回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
易错点 ⚠️
-
递归边界判断错误:使用
if not nums而非if left > right- ❌ 错误(切片版):
if not nums: return None→ 每次切片都要判断 - ✅ 正确(索引版):
if left > right: return None→ 边界清晰
- ❌ 错误(切片版):
-
中点计算溢出(在其他语言如Java/C++):
- ❌ 错误:
mid = (left + right) / 2→ left+right可能溢出 - ✅ 正确:
mid = left + (right - left) // 2→ 避免溢出
- ❌ 错误:
-
忘记返回根节点:递归函数必须返回构建的节点
- 💡 解决:每次递归调用都要
return build(...)并赋值给root.left/right
- 💡 解决:每次递归调用都要
🏗️ 工程实战(选读)
这个算法思想在真实项目中的应用,让你知道"学了有什么用"。
- 场景1:数据库索引 — 从有序数据构建B+树索引,保证查询O(log n)
- 场景2:负载均衡 — 将有序服务器列表构建成平衡树,快速查找最优服务器
- 场景3:游戏开发 — 从排序后的技能点数构建技能树,保证技能搜索效率
- 场景4:文件系统 — 构建平衡的目录树,优化文件查找性能
🏋️ 举一反三
完成本课后,试试这些同类题目来巩固知识:
| 题目 | 难度 | 相关知识点 | 提示 |
|---|---|---|---|
| LeetCode 109. 有序链表转BST | Medium | 快慢指针找中点+递归 | 链表版本,需要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 学习资料都在这里,后续复习和拓展会更省时间。