bigywater.tech
算法与数据结构 · LeetCode 108

LeetCode 108. 将有序数组转换为二叉搜索树

给定一个升序排列的整数数组,构造一棵高度平衡的二叉搜索树。正确做法是每次取当前区间的中点当根,递归处理左右两半,因为区间两侧元素数最多相差 1,构造出的树天然平衡。一个常见陷阱是:既然数组有序,就把它按顺序逐个插入一棵初始为空的 BST——这确实满足 BST 性质,但因为每个新值都比已有的都大,只会不断往右挂,退化成一条链,完全不平衡。本文用真实的规模对比验证这个规律,给出正确写法、5 种语言实现,以及取中点分治的分步演示。

15 vs 4
15 个有序元素,顺序插入 BST 得到的树高度是 15(退化成链),取中点分治只需要 4
4 步
分步演示:每次取当前区间的中点当根,递归处理左右两半,另附 5 种语言实现

题意与示例

本文只用自己的话概括题意,完整约束以官方页面为准。给定一个升序排列的整数数组,构造一棵高度平衡的二叉搜索树(本题允许多种正确答案,只要满足 BST 性质且高度平衡即可)。一共 5 组用例,覆盖:官方示例、空数组、单元素、两元素、七元素的完全填满情况:

真实实测 · 本机执行输出
[-10, -3, 0, 5, 9]  →  [0,-10,5,null,-3,null,9]
[]  →  []
[1]  →  [1]
[1, 2]  →  [1,null,2]
[1, 2, 3, 4, 5, 6, 7]  →  [4,2,6,1,3,5,7]

陷阱:把有序数组按顺序插入 BST,退化成一条链

一个真实会犯的错误:既然数组已经排好序了,有人会想"那就按顺序一个个插入一棵二叉搜索树"——这确实能得到一棵合法的 BST(中序遍历还原出来就是原数组),但完全不满足"高度平衡"的要求。因为数组本来就是升序的,按顺序插入时每个新值都比当前树里所有值大,只会不断往右边挂,结果是一条纯右链,退化成链表:

真实实测 · 本机执行输出
输入 1..15(15 个有序整数)\n正确写法(取中点分治)    →  高度 4,平衡\n陷阱写法(顺序插入 BST)  →  高度 15,不平衡

用一组不断增大的有序数组验证这个规律:

真实实测 · 本机执行输出
规模 n 与两种写法的树高:\nn=  1  正确写法高度=1(平衡=True)  陷阱写法高度=  1(平衡=True)\nn=  3  正确写法高度=2(平衡=True)  陷阱写法高度=  3(平衡=False)\nn=  7  正确写法高度=3(平衡=True)  陷阱写法高度=  7(平衡=False)\nn= 15  正确写法高度=4(平衡=True)  陷阱写法高度= 15(平衡=False)\nn= 31  正确写法高度=5(平衡=True)  陷阱写法高度= 31(平衡=False)\nn= 63  正确写法高度=6(平衡=True)  陷阱写法高度= 63(平衡=False)\nn=127  正确写法高度=7(平衡=True)  陷阱写法高度=127(平衡=False)

正确写法的高度始终是 ⌈log₂(n+1)⌉ 左右,陷阱写法的高度始终等于 n——因为数组有序,顺序插入这个操作对 BST 来说是最坏情况,每次都插在同一侧。只有当 n 很小(比如只有 1 个元素)时,两种写法碰巧看起来差不多,规模一大问题就暴露了。

取中点分治:五种语言的实现

分治:每次取当前区间 [l, r] 的中间元素 nums[(l+r)/2] 作为这一段的根,左半区间递归构造左子树,右半区间递归构造右子树。因为数组本身有序,中间元素两侧元素个数最多相差 1,这样递归下去每个结点的左右子树高度差也最多是 1,天然满足"高度平衡"。时间 O(n),空间 O(log n)(递归栈)。Python、Java、Go、JavaScript、C++ 都是真实编译运行的,用同一组 5 个测试用例。

Python · 解法代码def sorted_array_to_bst(nums): def helper(l, r): if l > r: return None mid = (l + r) // 2 root = TreeNode(nums[mid]) root.left = helper(l, mid - 1) root.right = helper(mid + 1, r) return root return helper(0, len(nums) - 1)
真实实测 · Python · 前几组用例的输出
$ python3 solution.py
[-10, -3, 0, 5, 9]  →  [0,-10,5,null,-3,null,9]
[]  →  []
[1]  →  [1]
[1, 2]  →  [1,null,2]
[1, 2, 3, 4, 5, 6, 7]  →  [4,2,6,1,3,5,7]
Java · 解法代码class Solution { public TreeNode sortedArrayToBST(int[] nums) { return helper(nums, 0, nums.length - 1); } private TreeNode helper(int[] nums, int l, int r) { if (l > r) return null; int mid = (l + r) / 2; TreeNode root = new TreeNode(nums[mid]); root.left = helper(nums, l, mid - 1); root.right = helper(nums, mid + 1, r); return root; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java && java -cp build Main
[-10, -3, 0, 5, 9]  →  [0,-10,5,null,-3,null,9]
[]  →  []
[1]  →  [1]
[1, 2]  →  [1,null,2]
[1, 2, 3, 4, 5, 6, 7]  →  [4,2,6,1,3,5,7]
Go · 解法代码func sortedArrayToBST(nums []int) *TreeNode { var helper func(l, r int) *TreeNode helper = func(l, r int) *TreeNode { if l > r { return nil } mid := (l + r) / 2 root := &TreeNode{Val: nums[mid]} root.Left = helper(l, mid-1) root.Right = helper(mid+1, r) return root } return helper(0, len(nums)-1) }
真实实测 · Go · 前几组用例的输出
$ go run solution.go
[-10, -3, 0, 5, 9]  →  [0,-10,5,null,-3,null,9]
[]  →  []
[1]  →  [1]
[1, 2]  →  [1,null,2]
[1, 2, 3, 4, 5, 6, 7]  →  [4,2,6,1,3,5,7]
JavaScript · 解法代码function sortedArrayToBST(nums) { function helper(l, r) { if (l > r) return null; const mid = Math.floor((l + r) / 2); const root = new TreeNode(nums[mid]); root.left = helper(l, mid - 1); root.right = helper(mid + 1, r); return root; } return helper(0, nums.length - 1); }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js
[-10, -3, 0, 5, 9]  →  [0,-10,5,null,-3,null,9]
[]  →  []
[1]  →  [1]
[1, 2]  →  [1,null,2]
[1, 2, 3, 4, 5, 6, 7]  →  [4,2,6,1,3,5,7]
C++ · 解法代码class Solution { public: TreeNode* sortedArrayToBST(vector<int>& nums) { return helper(nums, 0, (int)nums.size() - 1); } private: TreeNode* helper(vector<int>& nums, int l, int r) { if (l > r) return nullptr; int mid = (l + r) / 2; TreeNode* root = new TreeNode(nums[mid]); root->left = helper(nums, l, mid - 1); root->right = helper(nums, mid + 1, r); return root; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin
[-10, -3, 0, 5, 9]  →  [0,-10,5,null,-3,null,9]
[]  →  []
[1]  →  [1]
[1, 2]  →  [1,null,2]
[1, 2, 3, 4, 5, 6, 7]  →  [4,2,6,1,3,5,7]
真实实测 · 本机执行输出
共 5 组用例(含边界),以 Python 结果为基准逐组比对:\n  Python      通过 5/5\n  Java        通过 5/5\n  Go          通过 5/5\n  JavaScript  通过 5/5\n  C++         通过 5/5

随机对拍

Python 版本另外做了一次针对"取中点分治"写法本身的随机验证:对每一棵构造出来的树,检查两件事——是不是高度平衡,以及中序遍历回去是不是能精确还原原数组(BST 性质的直接体现)。

真实实测 · 本机执行输出
随机验证 30,000 组(数组长度 0 到 50 的随机升序数组),构造出的树全部满足「高度平衡」且「中序遍历还原原数组」,0 组不满足。

另外四种语言只在上面 5 组固定用例上验证过,没有做大规模随机验证。

分步演示:每次取中点分治

下面的演示用官方示例 [-10,-3,0,5,9]:每一步取当前区间的中点作为根,左半区间递归构造左子树,右半区间递归构造右子树,因为两侧元素个数最多相差 1,构造出来的树天然是高度平衡的。蓝色是当前正在处理的区间,绿色是已经放进树里的结点。每一帧都是把上面的解法在这组输入上真实执行记录下来的。

有序数组转平衡BST:每次取中点分治
当前区间已经放进树里的结点

    参考与说明

    • 官方题目页:LeetCode 108. 将有序数组转换为二叉搜索树。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:偶数长度区间时选左中点还是右中点对树形状的影响(两种取法都平衡,只是形状不同,本题允许多种正确答案)。
    京ICP备2026067502号-1京公网安备11010502063487号