記錄這幾天刷題做到的一個陣列的演演算法題的過程:
給定兩個大小分別為 m
和 n
的正序(從小到大)陣列 nums1
和 nums2
。請你找出並返回這兩個正序陣列的 中位數 。
示例 1:
輸入:nums1 = [1,3], nums2 = [2]
輸出:2.00000
解釋:合併陣列 = [1,2,3] ,中位數 2
示例 2:
輸入:nums1 = [1,2], nums2 = [3,4]
輸出:2.50000
解釋:合併陣列 = [1,2,3,4] ,中位數 (2 + 3) / 2 = 2.5
演演算法的時間複雜度應該為 O(log (m+n))
。
來源:力扣(LeetCode)
連結:https://leetcode.cn/problems/...
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
}
分析
此題目在力扣的難度為困難,乍一看題目並不是很難,用陣列合並的笨方法就可以解決。
真正的難點在於時間複雜度是 Log 級別
我們先來看一下不要求的時間複雜度的情況下如何處理。
奇偶判斷
如果總數是奇數,返回中間值,如果總數是偶數,返回中間值和中間值+1(由於給出的資料結構是陣列,所有的下標-1)
如果不想用 IF 實現判斷,也可以使用條件值的判斷,如:
return ( nums2_length % 2 == 0 ) ? // 是0嗎?
( nums2[middle - 1] + nums2[middle] ) / 2.0 : // 如果是0說明是偶數,返回中間兩個樹的平均值
nums2[middle]; // 如果不是0說明是奇數,返回中間的數
邊界條件
首先考慮邊界條件,題目的輸入可能會出現一個陣列為空的情況,如:
輸入:nums1 = [1,3], nums2 = []
輸入:nums1 = [], nums2 = [2,3,4]
這種情況單獨加一個判斷,如果有一個陣列為空,就直接用下標求出另一個陣列的中位數並直接返回,不需要合併陣列:
// 長度
int nums1_length = nums1.length;
int nums2_length = nums2.length;
int total_length = nums1_length + nums2_length;
// 如果陣列1為空,求陣列2的中位數
if (nums1_length == 0) {
int middle = nums2_length / 2;
return ( nums2_length % 2 == 0 ) ?
( nums2[middle - 1] + nums2[middle] ) / 2.0 :
nums2[middle];
}
// 如果陣列2為空,求陣列1的中位數
if (nums2_length == 0) {
int middle = nums1_length / 2;
return ( nums1_length % 2 == 0 ) ?
( nums1[middle - 1] + nums1[middle] ) / 2.0 :
nums1[middle];
}
一般情況
在兩個陣列都非空的情況下,嘗試合併陣列,然後取最終陣列的中位數
首先定義了三個索引,分別是合併後的索引,nums1的索引,nums2的索引,然後建立一個永真迴圈:
int[] nums = new int[total_length];
// i:合併後的列表的索引,j:nums1的索引,k:nums2的索引
int i = 0, j = 0, k = 0;
//
while (true) {
}
接下來看幾種情況:
如果 nums1的索引等於它的長度,說明這個陣列迴圈結束了,只把 nums2拼接到合併後陣列的後面即可
if (j == nums1_length && k != nums2_length) {
nums[i] = nums2[k];
k++;
i++;
}
else if (j != nums1_length && k == nums2_length) {
nums[i] = nums1[j];
j++;
i++;
}
如果兩個陣列的索引都等於他們的長度,說明迴圈結束,跳出迴圈,返回合併後陣列的中位數
else if (j == nums1_length && k == nums2_length) {
int middle = total_length / 2;
return ( total_length % 2 == 0 ) ?
( nums[middle - 1] + nums[middle] ) / 2.0 :
nums[middle];
}
其他情況就是正常的情況了,由於原有陣列是升序,合併後的陣列也是升序, 也就是插入每次比較時更小的那個
else {
if (nums1[j] > nums2[k]) {
nums[i] = nums2[k];
k++;
} else {
nums[i] = nums1[j];
j++;
}
i++;
}
小結
整個流程就變成了:
①看是否有一個陣列是空,如果有,直接返回另一個陣列的中位數
②開始迴圈,每次迴圈驗證是否有陣列的索引等於陣列長度,
如果一個陣列滿足,說明這個陣列迴圈完畢,直接把另一個陣列的尾部插入到合併後陣列的尾部
如果兩個陣列滿足,說明迴圈結束,返回合併後陣列的中位數
如果沒有陣列滿足,進入③
③正常情況下:比較兩個數,插入更小的數字,進行下一次迴圈
最後發現這個演演算法時間複雜度是 O(m+n),空間複雜度 O(m+n)
完整程式碼:
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 長度
int nums1_length = nums1.length;
int nums2_length = nums2.length;
int total_length = nums1_length + nums2_length;
//
if (nums1_length == 0) {
int middle = nums2_length / 2;
return ( nums2_length % 2 == 0 ) ?
( nums2[middle - 1] + nums2[middle] ) / 2.0 :
nums2[middle];
}
if (nums2_length == 0) {
int middle = nums1_length / 2;
return ( nums1_length % 2 == 0 ) ?
( nums1[middle - 1] + nums1[middle] ) / 2.0 :
nums1[middle];
}
// 定義陣列
int[] nums = new int[total_length];
// i:合併後的列表的索引,j:nums1的索引,k:nums2的索引
int i = 0, j = 0, k = 0;
// 開始迴圈
while (true) {
// 陣列1迴圈結束
if (j == nums1_length && k != nums2_length) {
nums[i] = nums2[k];
k++;
i++;
}
// 陣列2迴圈結束
else if (j != nums1_length && k == nums2_length) {
nums[i] = nums1[j];
j++;
i++;
}
// 整體迴圈結束
else if (j == nums1_length && k == nums2_length) {
int middle = total_length / 2;
return ( total_length % 2 == 0 ) ?
( nums[middle - 1] + nums[middle] ) / 2.0 :
nums[middle];
}
// 一般情況
else {
if (nums1[j] > nums2[k]) {
nums[i] = nums2[k];
k++;
} else {
nums[i] = nums1[j];
j++;
}
i++;
}
}
return 0;
}
}
初步最佳化
在不改變整體思路的情況下,最佳化思路就是
①不迴圈整個陣列,而是索引達到中位數就停止迴圈,這樣迴圈次數減少一半,但複雜度仍是 O(m+n)
②不記錄所有的合併結果,而是隻保留幾個固定元素,達到中位數就停止迴圈,這樣空間複雜度能降到O(1)
但這樣還是無法滿足題目要求,所以只能把整個思路都推翻掉,合併陣列的方案在一開始就是不滿足要求的。
改變思路——二分法
這個思路是真正的解法,但目前看起來比較難
暫時附上鍊接:https://leetcode.cn/problems/...
到筆者彙報的時候還沒完全看懂,後面再補充