题目
标题和出处
标题:将有序链表转换为二叉搜索树
难度
5 级
题目描述
要求
给定单链表的头结点 head \texttt{head} head,其中元素已经按升序排列,将其转换为高度平衡二叉搜索树。
高度平衡二叉树满足每个结点的左右子树的高度差的绝对值不超过 1 \texttt{1} 1。
示例
示例 1:
输入: head = [-10,-3,0,5,9] \texttt{head = [-10,-3,0,5,9]} head = [-10,-3,0,5,9]
输出: [0,-3,9,-10,null,5] \texttt{[0,-3,9,-10,null,5]} [0,-3,9,-10,null,5]
解释:一个可能的答案是 [0,-3,9,-10,null,5] \texttt{[0,-3,9,-10,null,5]} [0,-3,9,-10,null,5],表示如图所示的高度平衡二叉搜索树。
示例 2:
输入: head = [] \texttt{head = []} head = []
输出: [] \texttt{[]} []
数据范围
- 链表 head \texttt{head} head 中结点数目在范围 [0, 2 × 10 4 ] \texttt{[0, 2} \times \texttt{10}^\texttt{4}\texttt{]} [0, 2×104] 内
- -10 5 ≤ Node.val ≤ 10 5 \texttt{-10}^\texttt{5} \le \texttt{Node.val} \le \texttt{10}^\texttt{5} -105≤Node.val≤105
前言
这道题和「将有序数组转换为二叉搜索树」相似,区别在于这道题给定有序链表。
为了得到高度平衡二叉搜索树,构造的二叉搜索树应满足根结点的左子树和右子树的结点数尽可能接近,左子树和右子树也都是高度平衡二叉搜索树。
解法一
思路和算法
由于二叉搜索树的中序遍历序列是单调递增的,因此给定的升序链表即为二叉搜索树的中序遍历序列。在只有中序遍历序列的情况下,无法唯一地确定二叉搜索树。
为了得到高度平衡二叉搜索树,构造的二叉搜索树应满足根结点的左子树和右子树的结点数尽可能接近。当结点总数是奇数时,根结点值应为中序遍历序列的中间位置的结点值,根结点的左子树和右子树的结点数应相等;当结点总数是偶数时,根结点值应为中序遍历序列的中间位置的两个结点值之一,根结点的左子树和右子树的结点数之差的绝对值应等于 1 1 1。
确定高度平衡二叉搜索树的根结点之后,其余的结点值分别位于根结点的左子树和右子树中,链表中位于根结点左侧的值都在左子树中,链表中位于根结点右侧的值都在右子树中,左子树和右子树也是高度平衡二叉搜索树。可以通过数学归纳法证明,如果两个高度平衡二叉搜索树的结点数之差的绝对值不超过 1 1 1,则这两个高度平衡二叉搜索树的高度之差的绝对值不超过 1 1 1。
由于高度平衡二叉搜索树的每个子树也都是高度平衡二叉搜索树,每个子树包含的结点值的集合对应给定的链表中的连续子链表,因此可以使用递归分治的方式构造高度平衡二叉搜索树,递归的过程中只要指定每个子树包含的结点值的集合对应的连续子链表的区间 [ start , end ) [\textit{start}, \textit{end}) [start,end) 即可,该区间是左闭右开区间,区间包含 start \textit{start} start,不包含 end \textit{end} end。
递归的终止条件是区间为空,即 start = end \textit{start} = \textit{end} start=end,此时对应的子树为空。对于其余情况,首先根据 start \textit{start} start 和 end \textit{end} end 定位到子链表区间的中间结点并使用中间结点值创建根结点,然后分别使用中间结点左边和右边的两个区间创建根结点的左子树和右子树。定位到子链表区间的中间结点可以通过快慢指针实现。
当 start ≠ end \textit{start} \ne \textit{end} start=end 时,中间结点的唯一性取决于区间 [ start , end ) [\textit{start}, \textit{end}) [start,end) 内的元素个数的奇偶性。如果区间 [ start , end ) [\textit{start}, \textit{end}) [start,end) 内的元素个数是奇数,则中间结点是唯一的;如果区间 [ start , end ) [\textit{start}, \textit{end}) [start,end) 内的元素个数是偶数,则中间结点是不唯一的,可以是中间位置左边的结点或者中间位置右边的结点。
由此可以得到三种构造高度平衡二叉搜索树的方法。
-
每次都将根结点值取为中间位置左边的结点值。
-
每次都将根结点值取为中间位置右边的结点值。
-
每次随机将根结点值取为中间位置左边或右边的结点值。
代码
下面的代码为每次都将根结点值取为中间位置左边的结点值的做法。
class Solution {
public TreeNode sortedListToBST(ListNode head) {
return createBST(head, null);
}
public TreeNode createBST(ListNode start, ListNode end) {
if (start == end) {
return null;
}
ListNode slow = start, fast = start.next;
while (fast != end && fast.next != end) {
slow = slow.next;
fast = fast.next.next;
}
return new TreeNode(slow.val, createBST(start, slow), createBST(slow.next, end));
}
}
下面的代码为每次都将根结点值取为中间位置右边的结点值的做法。
class Solution {
public TreeNode sortedListToBST(ListNode head) {
return createBST(head, null);
}
public TreeNode createBST(ListNode start, ListNode end) {
if (start == end) {
return null;
}
ListNode slow = start, fast = start;
while (fast != end && fast.next != end) {
slow = slow.next;
fast = fast.next.next;
}
return new TreeNode(slow.val, createBST(start, slow), createBST(slow.next, end));
}
}
下面的代码为每次随机将根结点值取为中间位置左边或右边的结点值的做法。
class Solution {
Random random = new Random();
public TreeNode sortedListToBST(ListNode head) {
return createBST(head, null);
}
public TreeNode createBST(ListNode start, ListNode end) {
if (start == end) {
return null;
}
ListNode slow = start, fast = start.next;
while (fast != end && fast.next != end) {
slow = slow.next;
fast = fast.next.next;
}
if (fast != end && random.nextBoolean()) {
slow = slow.next;
}
return new TreeNode(slow.val, createBST(start, slow), createBST(slow.next, end));
}
}
复杂度分析
-
时间复杂度: O ( n log n ) O(n \log n) O(nlogn),其中 n n n 是链表 head \textit{head} head 的长度。用 T ( n ) T(n) T(n) 表示用长度为 n n n 的链表构造高度平衡二叉搜索树的时间,则有 T ( n ) = 2 × T ( n 2 ) + O ( n ) T(n) = 2 \times T\Big(\dfrac{n}{2}\Big) + O(n) T(n)=2×T(2n)+O(n),根据主定理可知 T ( n ) = O ( n log n ) T(n) = O(n \log n) T(n)=O(nlogn)。
-
空间复杂度: O ( log n ) O(\log n) O(logn),其中 n n n 是链表 head \textit{head} head 的长度。空间复杂度主要是递归调用的栈空间,由于构造的是高度平衡二叉搜索树,因此递归调用栈的深度是 O ( log n ) O(\log n) O(logn)。注意返回值不计入空间复杂度。
解法二
思路和算法
解法一需要多次定位中间结点,因此时间复杂度较高。由于给定的升序链表即为二叉搜索树的中序遍历序列,因此可以在遍历链表的过程中构造高度平衡二叉搜索树。
首先遍历链表得到链表的长度 n n n,则高度平衡二叉搜索树对应的下标区间是 [ 0 , n − 1 ] [0, n - 1] [0,n−1]。对于下标区间 [ start , end ] [\textit{start}, \textit{end}] [start,end],将该区间对应的子树的根结点值的下标记为 mid \textit{mid} mid,则有 mid = ⌊ start + end 2 ⌋ \textit{mid} = \Big\lfloor \dfrac{\textit{start} + \textit{end}}{2} \Big\rfloor mid=⌊2start+end⌋ 或者 mid = ⌊ start + end + 1 2 ⌋ \textit{mid} = \Big\lfloor \dfrac{\textit{start} + \textit{end} + 1}{2} \Big\rfloor mid=⌊2start+end+1⌋,左子树和右子树分别对应下标区间 [ start , mid − 1 ] [\textit{start}, \textit{mid} - 1] [start,mid−1] 和 [ mid + 1 , end ] [\textit{mid} + 1, \textit{end}] [mid+1,end]。
由于中序遍历序列的方法为依次遍历左子树、根结点和右子树,因此在遍历链表的过程中依次构造左子树、根结点和右子树,遍历链表结束时,高度平衡二叉搜索树构造完毕。
由于当下标区间 [ start , end ] [\textit{start}, \textit{end}] [start,end] 内的元素个数是偶数时, mid \textit{mid} mid 的取值不唯一,因此同样存在三种构造高度平衡二叉搜索树的方法。
-
每次都将根结点值取为中间位置左边的结点值。
-
每次都将根结点值取为中间位置右边的结点值。
-
每次随机将根结点值取为中间位置左边或右边的结点值。
代码
下面的代码为每次都将根结点值取为中间位置左边的结点值的做法。
class Solution {
ListNode curr;
public TreeNode sortedListToBST(ListNode head) {
curr = head;
int length = 0;
ListNode node = head;
while (node != null) {
length++;
node = node.next;
}
return createBST(0, length - 1);
}
public TreeNode createBST(int start, int end) {
if (start > end) {
return null;
}
int mid = (end - start) / 2 + start;
TreeNode left = createBST(start, mid - 1);
int rootVal = curr.val;
curr = curr.next;
TreeNode right = createBST(mid + 1, end);
return new TreeNode(rootVal, left, right);
}
}
下面的代码为每次都将根结点值取为中间位置右边的结点值的做法。
class Solution {
ListNode curr;
public TreeNode sortedListToBST(ListNode head) {
curr = head;
int length = 0;
ListNode node = head;
while (node != null) {
length++;
node = node.next;
}
return createBST(0, length - 1);
}
public TreeNode createBST(int start, int end) {
if (start > end) {
return null;
}
int mid = (end - start + 1) / 2 + start;
TreeNode left = createBST(start, mid - 1);
int rootVal = curr.val;
curr = curr.next;
TreeNode right = createBST(mid + 1, end);
return new TreeNode(rootVal, left, right);
}
}
下面的代码为每次随机将根结点值取为中间位置左边或右边的结点值的做法。
class Solution {
Random random = new Random();
ListNode curr;
public TreeNode sortedListToBST(ListNode head) {
curr = head;
int length = 0;
ListNode node = head;
while (node != null) {
length++;
node = node.next;
}
return createBST(0, length - 1);
}
public TreeNode createBST(int start, int end) {
if (start > end) {
return null;
}
int mid = (end - start + random.nextInt(2)) / 2 + start;
TreeNode left = createBST(start, mid - 1);
int rootVal = curr.val;
curr = curr.next;
TreeNode right = createBST(mid + 1, end);
return new TreeNode(rootVal, left, right);
}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是链表 head \textit{head} head 的长度。用 T ( n ) T(n) T(n) 表示用长度为 n n n 的链表构造高度平衡二叉搜索树的时间,则有 T ( n ) = 2 × T ( n 2 ) + O ( 1 ) T(n) = 2 \times T\Big(\dfrac{n}{2}\Big) + O(1) T(n)=2×T(2n)+O(1),根据主定理可知 T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n)。
-
空间复杂度: O ( log n ) O(\log n) O(logn),其中 n n n 是链表 head \textit{head} head 的长度。空间复杂度主要是递归调用的栈空间,由于构造的是高度平衡二叉搜索树,因此递归调用栈的深度是 O ( log n ) O(\log n) O(logn)。注意返回值不计入空间复杂度。