当前:LC108 · 将有序数组转换为二叉搜索树 · 首次出现于 Day 24 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC108 · Convert Sorted Array to BST · 树 / BST

有序数组转 BST:中点做根,挡住偏斜

升序数组就是 BST 的中序。每次取区间中点当根,左右两侧人数至多差 1,高度被压在对数级。总拿端点当根会退化成链表。

区间 [lo, hi]:mid = lo+(hi−lo)/2 做根,左子树建 [lo, mid−1],右子树建 [mid+1, hi],lo>hi 返回空。中点保证左右元素数至多差 1,从而高度平衡。偶数长度偏左或偏右都合法。时间 O(n),空间 O(log n)。

时间 O(n)空间 O(log n)结论先行 · 全文约 6 节
导读

给你一个升序整数数组,把它转成一棵高度平衡的二叉搜索树。高度平衡:每个节点左右子树的高度差不超过 1。

主例 nums = [-10, -3, 0, 5, 9]。五个数已经有序。取中间的 0 做根,左边留给 [-10, -3],右边留给 [5, 9],两边再各自取中点。无论中点偏左还是偏右,根都是 0,整棵高度是 3,平衡。

按顺序一个个插入会得到一条右链,高度 5,不是本题要的树。本文要回答:为什么中点能挡住偏斜,以及偶数长度两种中点为什么都收。

中序已经有了,缺的是每次把区间劈成两半

BST 的中序遍历就是升序。输入已经是中序,节点值的左右关系不用再猜:区间里左边的数只能进左子树,右边的数只能进右子树。缺的是根选谁。根选得太偏,一侧元素远多于另一侧,高度就会被长的那侧拖高。

第一直觉是拿最小的当根、其余往右挂。主例会得到 -10 为根的一条右链,每个节点左空右一,高度差是 1 但整体高度是 5,叶子和根的高度差远超平衡。题目要的是每个节点都平衡,链状 BST 过不了。缺的不是「是不是 BST」,而是「左右人数尽量一样」。

人数尽量一样的切法就是取中点。mid = lo + (hi−lo)/2,nums[mid] 做根,左区间 [lo, mid−1],右区间 [mid+1, hi]。左右长度至多差 1,再递归下去每层都减半,高度 O(log n)。空区间 lo > hi 返回空,这是叶子外侧的边界。

用手走主例。全区间 [0,4],mid=2,值 0 做根。演示第一帧 mid 下标是 2,注释里的 0 是根的值。左区间 [0,1] 是 -10 和 -3,偏左中点下标 0,-10 做左子树根,-3 只能挂在它右边。右区间 [3,4] 是 5 和 9,偏左中点下标 3,5 做右子树根,9 挂右边。按这套中点,层序是 [0, -10, 5, null, -3, null, 9]。

偶数长度把中点改成偏右,左区间根会变成 -3(-10 挂左边),右区间根会变成 9(5 挂左边),层序是 [0, -3, 9, -10, null, 5]。演示最后一帧画的就是这种同样合法的平衡树。题目不要求唯一形态,两种都满足「每个节点左右高度差 ≤ 1」。不要把某一种中点当成唯一答案。

中点挡的是链总拿端点当根,有序插入会退化成链表。中点让两侧人数至多差 1,平衡是人数差逼出来的,不是事后旋转。
区间 [0,4]:mid=2 为根
-10-3059
mid=0 为根

Go:区间递归

solution.goGo
func sortedArrayToBST(nums []int) *TreeNode {
var build func(int, int) *TreeNode
build = func(lo, hi int) *TreeNode {
if lo > hi { return nil }
mid := lo + (hi-lo)/2
return &TreeNode{
Val: nums[mid],
Left: build(lo, mid-1),
Right: build(mid+1, hi),
}
}
return build(0, len(nums)-1)
}

1空区间返回空,必须写在取 mid 之前。

2偏左中点。主例全区间取到 0;左半取到 -10,右半取到 5。

3左右闭区间递归。每个数恰好当一次根,时间线性。

总结

有序数组当中序,中点当根,左右再切。主例根是 0,链状插法高度 5,中点法高度 3。

  • 端点当根会退化成链表。中点让左右人数至多差 1。
  • 偶数长度偏左、偏右都平衡。主例两种层序都合法。
  • 空区间 lo>hi 返回空。空间是递归深度 O(log n)。
同族题目
LC105从前序与中序遍历序列构造二叉树LC109有序链表转换二叉搜索树LC98验证二叉搜索树