给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
示例:
给定的有序链表: [-10, -3, 0, 5, 9],
一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树:
0
/ \
-3 9
/ /
-10 5
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/convert-sorted-list-to-binary-search-tree
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题目中最重要的要求是需要利用链表中的节点,构建一颗高度平衡的二叉搜索树,好消息是链表中的元素是升序的。
众所周知,一棵二叉搜索树是一棵有根二叉树并且对于所有节点满足特殊的性质:对于树中任意一个点,它的权值必然 ≥ \geq ≥ 所有左子树节点的权值, ≤ \leq ≤ 所有右子树节点的权值。因为二叉树具有递归的子结构,二叉搜索树也同理:所有子树也是二叉搜索树。
当前方法和下一个方法的主要思路是:
给定列表中的中间元素将会作为二叉搜索树的根,该点左侧的所有元素递归的去构造左子树,同理右侧的元素构造右子树。这必然能够保证最后构造出的二叉搜索树是平衡的。
通过样例来理解算法的具体过程。
/**
* Definition for singly-linked list. public class ListNode { int val; ListNode next; ListNode(int
* x) { val = x; } }
*/
/**
* Definition for a binary tree node. public class TreeNode { int val; TreeNode left; TreeNode
* right; TreeNode(int x) { val = x; } }
*/
class Solution {
private ListNode findMiddleElement(ListNode head) {
// The pointer used to disconnect the left half from the mid node.
ListNode prevPtr = null;
ListNode slowPtr = head;
ListNode fastPtr = head;
// Iterate until fastPr doesn't reach the end of the linked list.
while (fastPtr != null && fastPtr.next != null) {
prevPtr = slowPtr;
slowPtr = slowPtr.next;
fastPtr = fastPtr.next.next;
}
// Handling the case when slowPtr was equal to head.
if (prevPtr != null) {
prevPtr.next = null;
}
return slowPtr;
}
public TreeNode sortedListToBST(ListNode head) {
// If the head doesn't exist, then the linked list is empty
if (head == null) {
return null;
}
// Find the middle element for the list.
ListNode mid = this.findMiddleElement(head);
// The mid becomes the root of the BST.
TreeNode node = new TreeNode(mid.val);
// Base case when there is just one element in the linked list
if (head == mid) {
return node;
}
// Recursively form balanced BSTs using the left and right halves of the original list.
node.left = this.sortedListToBST(head);
node.right = this.sortedListToBST(mid.next);
return node;
}
}
时间复杂度: O ( N log N ) O(N\log N) O(NlogN)。假设链表包含 N N N 个元素,对于传入递归函数的每个列表,我们需要计算它的中间元素。对于一个大小为 N N N 的列表,需要 N / 2 N/2 N/2 步找到中间元素,也就是花费 O ( N ) O(N) O(N) 的时间。我们对于原始数组的每一半都要同样的操作,看上去这是一个 O ( N 2 ) O(N^2) O(N2)的算法,但仔细分析会发现比 O ( N 2 ) O(N^2) O(N2)更高效。
统计一下每一半列表中所需要的操作数,根据上文所述对于 N N N 个元素我们需要 N / 2 N / 2 N/2 步得到中间元素。当找到中间元素之后我们将剩下两个大小为 N / 2 N/2 N/2 的子列表,对这两个部分都需要找中间元素的操作,需要时间开销为 2 × N / 4 2\times N / 4 2×N/4 步,同理对于更小的列表也是递归的操作。
N 2 + 2 ⋅ N 4 + 4 ⋅ N 8 + 8 ⋅ N 16    … \frac{N}{2} + 2 \cdot \frac{N}{4} + 4 \cdot \frac{N}{8} + 8 \cdot \frac{N}{16} \; \ldots 2N+2⋅4N+4⋅8N+8⋅16N…
显然,每次将列表分成一半,需要 log N \log N logN 的时间结束。因此,上面的等式可写成:
∑ i = 1 log N 2 i − 1 ⋅ N 2 i =    ∑ i = 1 log N N 2 =    N 2    log N =    O ( N log N ) \begin{aligned} &\sum_{i = 1}^{\log N} 2^{i - 1} \cdot \frac{N}{2^i} \\ = \; &\sum_{i = 1}^{\log N}\frac{N}{2} \\ = \; &\frac{N}{2} \; \log N \\ = \; &O(N\log N) \end{aligned} ===i=1∑logN2i−1⋅2iNi=1∑logN2N2NlogNO(NlogN)
空间复杂度: O ( log N ) O(\log N) O(logN)。因为使用递归的方法,所以需要考虑递归栈的空间复杂度。对于一棵费平衡二叉树,可能需要 O ( N ) O(N) O(N) 的空间,但是问题描述中要求维护一棵平衡二叉树,所以保证树的高度上界为 O ( log N ) O(\log N) O(logN),因此空间复杂度为 O ( log N ) O(\log N) O(logN)。
当前做法的主要问题是找到中间元素,由于链表的数据结构导致花费了很多额外时间,下面的方法可以尝试解决这一问题。
这个方法是空间换时间的经典案例。
你可以通过使用更多空间来降低时间复杂度。
在这个方法中,我们将给定的链表转成数组并利用数组来构建二叉搜索树。数组找中间元素只需要 O ( 1 ) O(1) O(1) 的时间,所以会降低整个算法的时间复杂度开销。
下面是算法的实现部分以及复杂度分析。
/**
* Definition for singly-linked list. public class ListNode { int val; ListNode next; ListNode(int
* x) { val = x; } }
*/
/**
* Definition for a binary tree node. public class TreeNode { int val; TreeNode left; TreeNode
* right; TreeNode(int x) { val = x; } }
*/
class Solution {
private List<Integer> values;
public Solution() {
this.values = new ArrayList<Integer>();
}
private void mapListToValues(ListNode head) {
while (head != null) {
this.values.add(head.val);
head = head.next;
}
}
private TreeNode convertListToBST(int left, int right) {
// Invalid case
if (left > right) {
return null;
}
// Middle element forms the root.
int mid = (left + right) / 2;
TreeNode node = new TreeNode(this.values.get(mid));
// Base case for when there is only one element left in the array
if (left == right) {
return node;
}
// Recursively form BST on the two halves
node.left = convertListToBST(left, mid - 1);
node.right = convertListToBST(mid + 1, right);
return node;
}
public TreeNode sortedListToBST(ListNode head) {
// Form an array out of the given linked list and then
// use the array to form the BST.
this.mapListToValues(head);
// Convert the array to
return convertListToBST(0, this.values.size() - 1);
}
}
时间复杂度:时间复杂度降到了 O ( N ) O(N) O(N) ,因为需要将链表转成数组。而取中间元素的开销变成了 O ( 1 ) O(1) O(1) 所以整体的时间复杂度降低了。
空间复杂度:因为我们利用额外空间换取了时间复杂度的降低,空间复杂度变成了 O ( N ) O(N) O(N),相较于之前算法的 O ( log N ) O(\log N) O(logN) 有所提升,因为创建数组的开销。
我们知道,二叉树有三种不同的遍历方法:
中序遍历一棵二叉搜索树会有一个非常有趣的结论。
中序遍历一棵二叉搜索树的结果是得到一个升序序列。
这个方法模拟了二叉搜索树的构造过程,因为我们已经获得有序的链表,所以自然的产生了这样的想法。
在描述算法之前,先看一下中序遍历是如何获得有序值的。
基于解决这个问题的中序遍历的思想:
我们知道中序遍历最左边的元素一定是给定链表的头部,类似地下一个元素一定是链表的下一个元素,以此类推。这是肯定的因为给定的初始链表保证了升序排列。
在了解了中序遍历二叉搜索树和有序数组的关系之后,让我们来看看算法。
首先用伪代码来理解一下算法。
➔ function formBst(start, end)
➔ mid = (start + end) / 2
➔ formBst(start, mid - 1)
➔
➔ TreeNode(head.val)
➔ head = head.next
➔
➔ formBst(mid + 1, end)
➔
看一下算法的模拟,理解的更清楚一点。
/**
* Definition for singly-linked list. public class ListNode { int val; ListNode next; ListNode(int
* x) { val = x; } }
*/
/**
* Definition for a binary tree node. public class TreeNode { int val; TreeNode left; TreeNode
* right; TreeNode(int x) { val = x; } }
*/
class Solution {
private ListNode head;
private int findSize(ListNode head) {
ListNode ptr = head;
int c = 0;
while (ptr != null) {
ptr = ptr.next;
c += 1;
}
return c;
}
private TreeNode convertListToBST(int l, int r) {
// Invalid case
if (l > r) {
return null;
}
int mid = (l + r) / 2;
// First step of simulated inorder traversal. Recursively form
// the left half
TreeNode left = this.convertListToBST(l, mid - 1);
// Once left half is traversed, process the current node
TreeNode node = new TreeNode(this.head.val);
node.left = left;
// Maintain the invariance mentioned in the algorithm
this.head = this.head.next;
// Recurse on the right hand side and form BST out of them
node.right = this.convertListToBST(mid + 1, r);
return node;
}
public TreeNode sortedListToBST(ListNode head) {
// Get the size of the linked list first
int size = this.findSize(head);
this.head = head;
// Form the BST now that we know the size
return convertListToBST(0, size - 1);
}
}
时间复杂度:时间复杂度仍然为 O ( N ) O(N) O(N) 因为我们需要遍历链表中所有的顶点一次并构造相应的二叉搜索树节点。
空间复杂度: O ( log N ) O(\log N) O(logN) ,额外空间只有一个递归栈,由于是一棵高度平衡的二叉搜索树,所以高度上界为 log N \log N logN。
作者:LeetCode
链接:https://leetcode-cn.com/problems/two-sum/solution/you-xu-lian-biao-zhuan-huan-er-cha-sou-suo-shu-by-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
把链表转化成数组然后套108题的代码就完事了:
执行用时 :3 ms, 在所有 Java 提交中击败了73.26%的用户
内存消耗 :41.6 MB, 在所有 Java 提交中击败了36.64%的用户
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode sortedListToBST(ListNode head) {
int[] nums = listToArr(head);
return arrToBST(nums,0,nums.length);
}
private int[] listToArr(ListNode head){
if(head==null) return new int[0];
int count = 0;
ListNode p = head;
while(p.next!=null){
p=p.next;
count++;
}
count++;
int[] nums = new int[count];
p=head;
count=0;
nums[count] = p.val;
while(p.next!=null){
p=p.next;
count++;
nums[count] = p.val;
}
return nums;
}
private TreeNode arrToBST(int[] nums, int start,int len){
if(len==0) return null;
TreeNode res = new TreeNode(nums[start+len/2]);
if(len>1){
res.left = arrToBST(nums, start, len/2);
res.right = arrToBST(nums, start+len/2+1,len-len/2-1);
}
return res;
}
}