問題:
There are two sorted arrays nums1 and nums2 of size m and n respectively.
Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).
You may assume nums1 and nums2 cannot be both empty.
給定兩個大小爲 m 和 n 的有序數組 nums1
和 nums2
。
請你找出這兩個有序數組的中位數,並且要求算法的時間複雜度爲 O(log(m + n))。
你可以假設 nums1
和 nums2
不會同時爲空。
分析:
題目要求log複雜度,加上兩個數組是有序的,很容易就想到二分查找的方法
第一,找到中位數的問題,其實就是找到排序爲第k位(k從0開始)的數的問題
第二,二分查找法的思路就是取mid——比較大小——縮小查找區間的過程,其他都好說,比較大小怎麼比?假設我們在nums1中查找第k位的數,兩個數組都是有序的,所以nums1[mid1]這個元素,在nums1中有mid1個元素比它小,那麼如果nums2中恰好有k-mid1個數比nums1[mid1]小,nums1[mid1]就是第k位數,如果nums2中比它小的數多於k-mid1個,nums1[mid1]就太大,反之太小
第三,如果第k位的數不在nums1中,需要在nums2裏再查找嗎?不用的,在nums1的查找過程中,lo1-1永遠指向比第k位數小的數,當查找結束後,就表示nums1中有lo1個數是比第k位數小的,顯而易見,第k位數就是nums2[k-lo1]
代碼:
/**
* 尋找兩個有序數組的中位數
* Median of Two Sorted Arrays
*
* @author DongWei
* @version 2019/5/31
*/
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
// 如果有空數組,直接返回結果
if (m == 0) {
if (n % 2 == 0) {
return (nums2[n >> 1] + nums2[(n >> 1) - 1]) / 2.0;
} else {
return nums2[n >> 1];
}
}
if (n == 0) {
if (m % 2 == 0) {
return (nums1[m >> 1] + nums1[(m >> 1) - 1]) / 2.0;
} else {
return nums1[m >> 1];
}
}
// 開始在 nums1 中查找第 k 位數,即中位數,對於偶數長度的數組而言,查找較大的那個中位數
int k = (m + n) >> 1;
int lo1 = 0;
int hi1 = Math.min(m - 1, k);
while(true) {
if (lo1 <= hi1) {
// 二分查找
int mid1 = (lo1 + hi1) >> 1;
double res = binarySearch(mid1, k, nums1, nums2);
if (res == Double.MAX_VALUE) {
hi1 = mid1 - 1;
} else if (res == Double.MIN_VALUE) {
lo1 = mid1 + 1;
} else {
return res;
}
} else {
// 中位數在 nums2 中
if ((n + m) % 2 == 0) {
// 長度爲偶數,根據不同情況計算較小的中位數
if (k <= lo1) {
// 較大中位數是 nums2[0],較小中位數必然是 num1 的最後一個元素
return (nums2[k - lo1] + nums1[lo1 - 1]) / 2.0;
} else if (lo1 == 0) {
// nums1 中沒有任何元素比較大中位數小,較小中位數必然在 nums2 中
return (nums2[k - lo1] + nums2[k - lo1 - 1]) / 2.0;
} else {
// 較小中位數可能是在 nums1 或 nums2 中,比較兩個數的大小確定
return (nums2[k - lo1] + Math.max(nums2[k - lo1 - 1], nums1[lo1 - 1])) / 2.0;
}
} else {
// 長度爲奇數,直接返回結果
return nums2[k - lo1];
}
}
}
}
/**
* 判斷元素是不是中位數
*
* @param v 待比較元素
* @param need nums 中如果有 need 個元素比 v 小,說明 v 恰好是中位數
* @param nums2 另一個數組
* @return 判斷這個元素比中位數大還是小
*/
private int compare(int v, int need, int[] nums2) {
// 處理數組邊界情況
int val1 = Integer.MIN_VALUE;
int val2 = Integer.MAX_VALUE;
if (need > 0 && need < nums2.length) {
val1 = nums2[need - 1];
val2 = nums2[need];
} else if (need == 0) {
val2 = nums2[need];
} else if (need == nums2.length) {
val1 = nums2[need - 1];
} else {
val1 = Integer.MAX_VALUE;
}
// 如果 nums2[need] 比 v 大,nums2[need - 1] 比 v 小,則 nums2 中恰好有 need 個元素比 v 小
if (val2 >= v && val1 <= v) {
return 0;
} else if (val2 < v) {
return 1;
} else {
return -1;
}
}
/**
* 二分查找中位數
*
* @param mid 二分查找中點
* @param k 中位數是第 k 位元素
* @param nums1 數組 1
* @param nums2 數組 2
* @return 如果 mid 元素是中位數,依照數組長度奇偶,計算中位數結果,否則用 Double.MAX_VALUE 和 Double.MIN_VALUE 表示大了還是小了
*/
private double binarySearch(int mid, int k, int[] nums1, int[] nums2) {
// nums2 中需要恰好有 k - mid 個元素比 nums1[mid] 小
int need = k - mid;
int cmp = compare(nums1[mid], need, nums2);
if (cmp == 0) {
// 計算中位數
if ((nums1.length + nums2.length) % 2 == 0) {
if (need == 0) {
// 較大中位數恰好是 nums1 的第 k 位數,nums2 中沒有比中位數小的元素
return (nums1[mid] + nums1[mid - 1]) / 2.0;
} else if(mid == 0) {
// 較大中位數恰好是 nums1 的首元素,較小中位數必然在 nums2 中
return (nums1[mid] + nums2[need - 1]) / 2.0;
} else {
// 較小中位數可能是在 nums1 或 nums2 中,比較兩個數的大小確定
return (Math.max(nums1[mid - 1], nums2[need - 1]) + nums1[mid]) / 2.0;
}
} else {
return nums1[mid];
}
} else if (cmp == 1) {
return Double.MAX_VALUE;
} else {
return Double.MIN_VALUE;
}
}
}
這個代碼跑起來速度10ms~14ms不等,一般是12ms左右
覺得自己這個算法寫起來十分冗長, 不優雅,參考了別人的算法,用的另一種二分查找思路,不是在nums1和nums2裏查找,而是在k上進行二分查找
第一,查找第k位數,數組中有k個元素比它小,這k個元素分佈在nums1和nums2中,必然有一個數組中比中位數小的元素個數較大,且數量大於等於k/2個,相當於每次二分查找我們可以確定k/2個比中位數小的數字在哪。例如下圖中,一共9個元素,中位數是第4位元素5,在數組2中有3個元素比5小,超過了4/2,確定了4/2個元素比中位數小,下次查找就可以排除灰色的範圍
第二,怎麼判斷比中位數小的元素在哪個數組裏分佈的數量更多?比較一下兩個數組的第k/2個元素,比較小的元素所在數組,必然有k/2個元素比中位數小
代碼如下,比我的快1~2ms,簡潔
/**
* 尋找兩個有序數組的中位數
* Median of Two Sorted Arrays
*
* @author DongWei
* @date 2019/5/31
*/
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int target = ((m + n) >> 1) + 1;
if ((m + n) % 2 == 0) {
return (getKth(target, 0, 0, nums1, nums2) + getKth(target - 1, 0, 0, nums1, nums2)) / 2.0;
} else {
return getKth(target, 0, 0, nums1, nums2);
}
}
/**
* 查找第 k 位數
*
* @param k 查找第 k 位數
* @param lo1 nums1 的查找起始範圍
* @param lo2 nums2 的查找起始範圍
* @param nums1 數組 1
* @param nums2 數組 2
* @return 第 k 位數
*/
private double getKth(int k, int lo1, int lo2, int[] nums1, int[] nums2) {
// 如果一個數組的查找起始範圍超過了下標範圍,說明第 k 位數在另一個數組裏
if (lo1 >= nums1.length) return nums2[lo2 + k - 1];
if (lo2 >= nums2.length) return nums1[lo1 + k - 1];
// 查找第 1 位數,比較大小返回
if (k == 1) {
return Math.min(nums1[lo1], nums2[lo2]);
}
// 判斷哪個數組中有 k / 2 個元素小於中位數
// 如果數組的剩餘長度小於 k / 2,用 Integer.MAX_VALUE 處理,比較大小後判定結果一定是另一個數組
int value1 = Integer.MAX_VALUE;
int value2 = Integer.MAX_VALUE;
if (lo1 + (k >> 1) <= nums1.length) value1 = nums1[lo1 + (k >> 1) - 1];
if (lo2 + (k >> 1) <= nums2.length) value2 = nums2[lo2 + (k >> 1) - 1];
// 比較一下兩個數組的第 k/2 個元素,比較小的元素所在數組,必然有 k/2 個元素比中位數小
if (value1 < value2) {
return getKth(k - (k >> 1), lo1 + (k >> 1), lo2, nums1, nums2);
} else {
return getKth(k - (k >> 1), lo1, lo2 + (k >> 1), nums1, nums2);
}
}
}
運行速度9ms~13ms