每日一道 LeetCode (47):尋找兩個正序陣列的中位數
每天 3 分鐘,走上演算法的逆襲之路。
前文合集
程式碼倉庫
GitHub: https://github.com/meteor1993/LeetCode
Gitee: https://gitee.com/inwsy/LeetCode
題目:尋找兩個正序陣列的中位數
難度:困難
給定兩個大小為 m 和 n 的正序(從小到大)陣列 nums1 和 nums2。
請你找出這兩個正序陣列的中位數,並且要求演算法的時間複雜度為 O(log(m + n))。
你可以假設 nums1 和 nums2 不會同時為空。
示例 1:
nums1 = [1, 3]
nums2 = [2]
則中位數是 2.0
示例 2:
nums1 = [1, 2]
nums2 = [3, 4]
則中位數是 (2 + 3)/2 = 2.5
解題方案一:暴力法
果然是困難難度的題,這道題最簡單的暴力法我都調了半天程式才調好。
暴力法的思路很簡單,兩個有序陣列,直接拼起來組合成一個大的有序陣列,然後在這個有序數組裡面取中位數。
思路很簡單,不過程式碼還是稍微有點不是那麼好寫的,還好給的是有序陣列,如果是無序陣列,合併的難度更大。
public double findMedianSortedArrays(int[] nums1, int[] nums2) { int n1 = nums1.length; int n2 = nums2.length; int[] nums = new int[n1 + n2]; // 先處理極限情況, nums1 為空 if (n1 == 0) { // 如果 nums2 長度是偶數 if (n2 % 2 == 0) { return (nums2[n2 / 2 - 1] + nums2[n2 / 2]) / 2.0; } else { return nums2[n2 / 2]; } } // 同上,如果 nums2 為空 if (n2 == 0) { if (n1 % 2 == 0) { return (nums1[n1 / 2 - 1] + nums1[n1 / 2]) / 2.0; } else { return nums1[n1 / 2]; } } // 定義新陣列的指標 int count = 0; int i = 0, j = 0; while (count != (n1 + n2)) { // 當 nums1 迴圈結束而 nums2 還有的時候 if (i == n1) { while (j != n2){ nums[count++] = nums2[j++]; } break; } // 當 nums2 迴圈結束而 nums1 還有的時候 if (j == n2) { while (i != n1) { nums[count++] = nums1[i++]; } break; } if (nums1[i] < nums2[j]) { nums[count++] = nums1[i++]; } else { nums[count++] = nums2[j++]; } } if (count % 2 == 0) { return (nums[count / 2 - 1] + nums[count / 2]) / 2.0; } else { return nums[count / 2]; } }
好了,我這個智商基本上對一道困難難度的題能想到這裡應該已經算不錯的了,剩下的就開始翻答案了。
解題方案二:二分法
感覺答案上給這個方案叫二分法並不是很合適。
這個方案的思路是遮掩的,兩個陣列 nums1 和 nums2 的長度已經確定了,那麼中位數的的位置肯定是 (nums1.length + nums2.length) / 2
,假設長度正好是奇數哈,如果定義剛才那個位置是 k 的話,這個問題就可以成功的轉化成另一個問題,如何在兩個有序陣列中尋找第 k 小的數字。
可以定義分別為兩個陣列定義兩個指標,分別指向兩個陣列開頭的位置,然後移動兩個指標,直到第 k 次移動,那麼我們就找到了第 k 小的數字。這種方案相當於還是要迴圈兩個陣列,接著想辦法減少迴圈次數。
那麼就不能是使用迴圈了,這裡就用到了 「二分法」 。
- 兩個陣列 nums1 和 nums2 ,先取 nums1[k/2 - 1] 記為 pivot1 和 nums2[k/2 - 1] 記為 pivot2 。
- 接著取 pivot = min(pivot1, pivot2) ,那麼這時兩個陣列中小於等於 pivot 的最多不會超過 k - 2 個。
- 這時 pivot 最大也可能是第 k-1 小的元素。
- 如果 pivot = pivot1 ,那麼在 nums1 中,從 0 到 k / 2 - 1 都不會是第 k 個元素,把這些元素全部 "刪除",剩下的作為新的 nums1 陣列。
- 如果 pivot = pivot2 ,那麼在 nums2 中,從 0 到 k / 2 - 1 都不會是第 k 個元素,把這些元素全部 "刪除",剩下的作為新的 nums2 陣列。
- 由於前面刪除了一些元素,這時我們需要修改 k 的值,減去我們刪掉的元素。
- 重複上面的過程,就可以找到第 k 個元素了。
public double findMedianSortedArrays_1(int[] nums1, int[] nums2) {
int l1 = nums1.length, l2 = nums2.length;
int totalLength = l1 + l2;
if (totalLength % 2 == 1) {
int midIndex = totalLength / 2;
return getKthElement(nums1, nums2, midIndex + 1);
} else {
int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
return (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
}
}
// 獲取第 k 小的數字
private int getKthElement(int[] nums1, int[] nums2, int k) {
int length1 = nums1.length, length2 = nums2.length;
int index1 = 0, index2 = 0;
while (true) {
// 邊界情況
if (index1 == length1) {
return nums2[index2 + k - 1];
}
if (index2 == length2) {
return nums1[index1 + k - 1];
}
if (k == 1) {
return Math.min(nums1[index1], nums2[index2]);
}
// 正常情況
int half = k / 2;
int newIndex1 = Math.min(index1 + half, length1) - 1;
int newIndex2 = Math.min(index2 + half, length2) - 1;
int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
if (pivot1 <= pivot2) {
k -= (newIndex1 - index1 + 1);
index1 = newIndex1 + 1;
} else {
k -= (newIndex2 - index2 + 1);
index2 = newIndex2 + 1;
}
}
}
果然不愧是困難模式,上面這段操作我看了好幾遍,還自己徒手推導了好幾次,才理解答案中的含義,建議各位看不大懂的小夥伴自己在紙上畫兩個陣列,徒手推導一次,絕對事半功倍。