Leetcode Hot 100 继续在树上施工,这一题看着像“建树题”,实际上考的是一手很标准的分治递归:将有序数组转换为二叉搜索树。
题目给你一个升序数组,不是让你随便拼一棵树, 而是要你拼出一棵:
- 二叉搜索树
- 高度平衡
也就是说,这题不是“能建出来就行”, 而是“既要合法,还得长得匀称”。
题目链接
题目描述
给你一个整数数组 nums,其中元素已经按升序排列,请你将其转换为一棵高度平衡的二叉搜索树。
这里的“高度平衡”指的是:
每个节点的左右两个子树高度差的绝对值不超过
1。

解题思路:每次取中点作为根节点
这题最核心的一步,就是:
每次取当前有序数组的中间元素,作为这一棵子树的根节点。
为什么中点这么关键?
因为数组已经有序:
- 中点左边的元素都比它小
- 中点右边的元素都比它大
这正好满足二叉搜索树的性质:
- 左子树节点值 < 根节点值
- 右子树节点值 > 根节点值
同时,中点还能把数组尽量均匀地分成两半, 这样左右子树规模接近,整棵树就更容易保持平衡。
所以这题的套路非常清晰:
- 取中间元素建根节点
- 左半部分递归构造左子树
- 右半部分递归构造右子树
- 数组为空时返回
None
整个过程就是一个标准分治。
代码实现
class Solution(object): def sortedArrayToBST(self, nums): """ :type nums: List[int] :rtype: Optional[TreeNode] """ def f(num_lists): if not num_lists: return None n = len(num_lists) idx = n // 2 node = TreeNode(val=num_lists[idx]) node.left = f(num_lists[:idx]) node.right = f(num_lists[idx + 1:]) return node
if len(nums) == 0: return None return f(nums)代码解析
1. 递归函数 f(num_lists) 是干什么的
这个函数表示:
把当前这段有序数组,转换成一棵高度平衡的二叉搜索树,并返回根节点。
也就是说,函数的输入是一段有序数组, 输出是一棵对应的 BST 子树。
2. 边界情况:数组为空时返回 None
if not num_lists: return None这是递归里最重要的刹车片。
如果当前数组已经空了, 说明这里已经没有节点可以建, 那就返回空节点。
你提到的“要注意边界情况”,说得很对。 这题的递归不难,真正不能漏的恰恰就是这个空数组判断。
3. 为什么取 n // 2 作为根节点
n = len(num_lists)idx = n // 2node = TreeNode(val=num_lists[idx])取中间位置的元素作为根节点,有两个直接好处:
第一,满足 BST 性质
因为数组有序:
idx左边的都更小idx右边的都更大
所以左边天然适合当左子树, 右边天然适合当右子树。
第二,尽量保持平衡
因为中间元素把数组分成两半, 左右子树节点数量差距最小, 更容易形成高度平衡的结构。
如果你不取中点, 比如老挑最左边或者最右边, 那树就很容易一路歪下去, 最后长成一根“树形天线”。
4. 递归构造左右子树
node.left = f(num_lists[:idx])node.right = f(num_lists[idx + 1:])这一步就是分治的核心:
- 左半段递归生成左子树
- 右半段递归生成右子树
而且切出来的左右数组依然是有序的, 所以递归条件能够继续成立。
换句话说:
每一层都在做同一件事:拿中点做根,再把左右两段继续递归。
这就是递归之所以顺手的原因。
示例理解
比如输入:
nums = [-10, -3, 0, 5, 9]第一次取中点:
0于是根节点就是:
0接下来分成两段:
- 左边:
[-10, -3] - 右边:
[5, 9]
继续递归:
- 左边会选
-3做根 - 右边会选
9做根
然后再往下拆。
整个过程很像:
不断从中间劈开数组,再把每一块的中点拎出来当根节点。
最后就能得到一棵平衡 BST。
这份写法的优缺点
你现在这版代码的优点很明显:
- 写法直观
- 逻辑清楚
- 很适合讲题解
但它也有一个小代价:
num_lists[:idx]num_lists[idx + 1:]这里每次递归都会创建新的子数组切片。
所以它虽然好懂, 但不是最省空间、最省时间的写法。
复杂度分析(当前切片写法)
时间复杂度
由于递归过程中不断切片,会有额外拷贝开销, 整体时间复杂度通常可以看作:
O(n log n)空间复杂度
除了递归栈之外, 切片本身也会额外占空间, 所以空间复杂度也不算低。
这个版本的优势不在极致优化, 而在于:
好理解、好实现、好写题解。
如果是刷题或者写博客,这版完全够用。
如果想优化,可以怎么做
更优的办法是:
- 不传子数组
- 改成传左右边界
left和right
这样就不会反复创建新数组, 效率会更好。
不过对于当前这题, 先把“中点分治建树”的核心讲明白更重要。 优化版可以当进阶补充, 不一定非得第一时间上。
易错点总结
1. 忘记处理空数组
if not num_lists: return None这句必须有, 不然递归走到空列表时就会直接翻车。
2. 根节点不是随便选的
这题不是“从数组里挑一个数当根”就完事。
要满足“高度平衡”, 就必须尽量从中间挑。
3. 只顾 BST,忘了平衡
很多同学会觉得:
数组有序,那我怎么建都能比较顺吧?
不对。
“顺”不等于“平衡”。
题目要求的是:
- 是二叉搜索树
- 还得高度平衡
所以中点选择才是灵魂。
4. 以为 if len(nums) == 0 那句是必须的
if len(nums) == 0: return None这句当然没问题,写着也清楚。
但严格来说,有了递归里的:
if not num_lists: return None外层这句其实可以省掉,直接:
return f(nums)也能正确运行。
不过保留也没毛病,算是提前把空输入拦一下。
小结
这题的核心其实就一句:
每次取有序数组的中点作为根节点,再递归构造左右子树。
这样做可以同时满足两件事:
- 保证二叉搜索树性质
- 尽量保持整棵树高度平衡
所以它本质上是一道非常典型的分治递归建树题。 数组一劈两半,中点上位称王;左边去建左树,右边去建右树,整套动作行云流水。 只要边界别漏,这题基本就是递归稳稳拿下。🦐