題目描述(中等難度)
和 108 題 是一樣的,都是給定一個升序序列,然後生成二分平衡查找樹。區別在於 108 題給定的是數組,這裏給的是鏈表。
解法一
大家先看一下 108 題 吧,算法的關鍵是取到中間的數據做爲根節點。而這裏鏈表的話,由於不支持隨機訪問,所以會麻煩些。最簡單的思路就是我們把鏈表先用線性表存起來,然後題目就轉換成 108 題了。
爲了方便,把上一道題的數組參數改爲List
。
public TreeNode sortedListToBST(ListNode head) {
ArrayList<Integer> nums = new ArrayList<>();
while (head != null) {
nums.add(head.val);
head = head.next;
}
return sortedArrayToBST(nums);
}
public TreeNode sortedArrayToBST(ArrayList<Integer> nums) {
return sortedArrayToBST(nums, 0, nums.size());
}
private TreeNode sortedArrayToBST(ArrayList<Integer> nums, int start, int end) {
if (start == end) {
return null;
}
int mid = (start + end) >>> 1;
TreeNode root = new TreeNode(nums.get(mid));
root.left = sortedArrayToBST(nums, start, mid);
root.right = sortedArrayToBST(nums, mid + 1, end);
return root;
}
時間複雜度:O(n)
。
空間複雜度:數組進行輔助,O(n)
。
解法二
參考 這裏。
有沒有一種方案,不用數組的輔助呢?那麼我們需要解決怎麼得到 mid 的值的問題。
最直接的思路就是根據 start 和 end,求出 mid,然後從 head 遍歷 mid - start 次,就到達了 mid 值。但最開始的 end,我們還得遍歷一遍鏈表才能得到,總體來說就是太複雜了。
這裏有一個求中點節點值的技巧,利用快慢指針。
快指針和慢指針同時從頭部開始遍歷,快指針每次走兩步,慢指針每次走一步,當快指針走到鏈表尾部,此時慢指針就指向了中間位置。
除了求中點節點的值不一樣,基本架構和 108 題 是一樣的。
public TreeNode sortedListToBST(ListNode head) {
return sortedArrayToBST(head, null);
}
private TreeNode sortedArrayToBST(ListNode head, ListNode tail) {
if (head == tail) {
return null;
}
ListNode fast = head;
ListNode slow = head;
while (fast != tail && fast.next != tail) {
slow = slow.next;
fast = fast.next.next;
}
TreeNode root = new TreeNode(slow.val);
root.left = sortedArrayToBST(head, slow);
root.right = sortedArrayToBST(slow.next, tail);
return root;
}
時間複雜度:根據遞歸式可知,T(n) = 2 * T(n / 2 ) + n
,O(nlog(n))
。
空間複雜度:O(log(n))
。
解法三
解法二雖然沒有藉助數組,優化了空間複雜度,但是時間複雜度增加了,那麼有沒有一種兩全其美的方法,時間複雜度是解法一,空間複雜度是解法二。還真有,參考 這裏。
主要思想是,因爲我們知道題目給定的升序數組,其實就是二叉搜索樹的中序遍歷。那麼我們完全可以按照這個順序去爲每個節點賦值。
實現的話,我們套用中序遍歷的遞歸過程,並且將 start
和 end
作爲遞歸參數,當 start == end
的時候,就返回 null
。
先回想一下中序遍歷的算法。
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
getAns(root, ans);
return ans;
}
private void getAns(TreeNode node, List<Integer> ans) {
if (node == null) {
return;
}
getAns(node.left, ans);
ans.add(node.val);
getAns(node.right, ans);
}
之前是將 node.val
進行保存,這裏的話我們是給當前節點進行賦值,爲了依次賦值,我們需要一個cur
指針指向給定的數列,每賦一個值就進行後移。
ListNode cur = null;
public TreeNode sortedListToBST(ListNode head) {
cur = head;
int end = 0;
while (head != null) {
end++;
head = head.next;
}
return sortedArrayToBSTHelper(0, end);
}
private TreeNode sortedArrayToBSTHelper(int start, int end) {
if (start == end) {
return null;
}
int mid = (start + end) >>> 1;
//遍歷左子樹並且將根節點返回
TreeNode left = sortedArrayToBSTHelper(start, mid);
//遍歷當前根節點並進行賦值
TreeNode root = new TreeNode(cur.val);
root.left = left;
cur = cur.next; //指針後移,進行下一次的賦值
//遍歷右子樹並且將根節點返回
TreeNode right = sortedArrayToBSTHelper(mid + 1, end);
root.right = right;
return root;
}
時間複雜度:O(n)
,主要是得到開始的 end,需要遍歷一遍鏈表。
空間複雜度:O(log(n))
,遞歸壓棧的消耗。
總
快慢指針求鏈表的中間值,這個技巧不錯。此外,解法三的模仿中序遍歷的過程,然後把給定的數組依次賦值過去,太強了。
更多詳細通俗題解詳見 leetcode.wang 。