题目
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
示例 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
提示:
nums1.length == mnums2.length == n0 <= m <= 10000 <= n <= 10001 <= m + n <= 2000-106 <= nums1[i], nums2[i] <= 106
思路解析
我们发现题目给的数据,俩个数组都是有序的,我们要找中位数,那么就转换为给定一个函数,找第k大的数问题。
那么会发现,实际上,我们num1和num2,在起始状态下,我们都去找它们的第k/2大的数
假定num1、num2数组的k/2的数如果为val1和val2,那么此时,如果val1的值如果<=val2的值,那么val1的前面k/2个值都不用看了他们肯定可以排除掉。
因为我们要找的是第k个值,k/2,向下取整,那么num1的前面k/2个值如果<=num2的第k/2个值,那么肯定是<=我们第k个值的,这个时候排除后,我们就可以用k减去我们排除掉的元素的个数
相似的,我们以此类推,知道最后num1的值或者num2的值都排除或者k为1的时候,那就可以很容易找到我们需要的值了。
例子
核心思路出现:"一次扔掉一大块"
我们能不能一次扔掉很多数?
假设:
nums1: [ 1, 3, 5, 7, 9 ] nums2: [ 2, 4, 6, 8, 10 ]
我们要找:
第 7 小的数
我们做一件事(非常关键)
各自看第 k/2 个数
- k = 7 → k/2 = 3
nums1 第 3 个是 5 nums2 第 3 个是 6
比较这两个数
5 < 6
现在问你一个问题:
nums1 里 [1,3,5] 有没有可能是第 7 小?
答案:不可能
为什么?
-
[1,3,5]已经只有 3 个 -
即使把 nums2 里比它们小的全算上
-
它们最多也排在前面
-
永远排不到第 7
重要结论
如果
nums1[k/2] <= nums2[k/2]那么
nums1 的前 k/2 个元素一定不可能是第 k 小
可以 直接删掉
六、删掉之后发生了什么?
我们删掉了:
nums1: [1, 3, 5]
现在:
-
已经排除了 3 个最小的
-
第 7 小 → 变成 第 4 小
k = 7 - 3 = 4
数组状态变成:
nums1: [7, 9] nums2: [2, 4, 6, 8, 10]
七、继续重复这个过程
第 2 轮
-
k = 4 → k/2 = 2
-
nums1 第 2 个:9
-
nums2 第 2 个:4
9 > 4
删除 nums2 的前 2 个:
[2, 4]
更新:k = 4 - 2 = 2
第 3 轮
-
k = 2 → k/2 = 1
-
比较当前最小的两个数:
7 vs 6
删除 6
k = 1
k == 1 是什么?
当前两个数组中最小的那个数
min(7, 8) = 7
这就是第 7 小的数
八、为什么这个方法快?
每一轮都在干嘛?
-
删除
k/2个元素 -
k 直接砍一半
k → k/2 → k/4 → k/8 ...
这是 对数级别
九、为什么不会删错?(最重要的问题)
因为:
-
两个数组是 有序的
-
每次比较的是「第 k/2 个」
-
被删掉的那一边:
-
数量 + 排名上限
-
永远不可能达到第 k
-
这是一个 严格数学保证
十、把它和中位数连起来
中位数 = 第 k 小
-
奇数长度 → 找一个 k
-
偶数长度 → 找两个 k,求平均
所以整个题的本质就是:
在两个有序数组中找第 k 小的数
代码实现
java
class Solution {
/**
* 主函数:在两个有序数组中寻找中位数
*/
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int length1 = nums1.length;
int length2 = nums2.length;
// 总元素个数
int n = length1 + length2;
// 如果总长度是奇数
// 中位数就是第 (n/2 + 1) 小的元素(注意:k 从 1 开始)
if (n % 2 == 1) {
return getKthElement(nums1, nums2, n / 2 + 1);
}
// 如果总长度是偶数
// 中位数是第 n/2 小 和 第 n/2 + 1 小 的平均值
else {
return (getKthElement(nums1, nums2, n / 2)
+ getKthElement(nums1, nums2, n / 2 + 1)) / 2.0;
}
}
/**
* 在两个【有序数组】中查找第 k 小的元素
* 核心思想:二分删除法(每次排除 k/2 个不可能的元素)
*/
public int getKthElement(int[] nums1, int[] nums2, int k) {
int length1 = nums1.length;
int length2 = nums2.length;
// 当前指向 nums1、nums2 的起始下标
int index1 = 0, index2 = 0;
while (true) {
// 情况 1:nums1 已经被排除完
// 那第 k 小的元素一定在 nums2 中
if (index1 == length1) {
return nums2[index2 + k - 1];
}
// 情况 2:nums2 已经被排除完
if (index2 == length2) {
return nums1[index1 + k - 1];
}
// 情况 3:k == 1
// 只需要比较当前两个指针所指的最小值
if (k == 1) {
return Math.min(nums1[index1], nums2[index2]);
}
// 各自向前走 k/2 步(注意不能越界)
int newIndex1 = Math.min(length1, index1 + k / 2) - 1;
int newIndex2 = Math.min(length2, index2 + k / 2) - 1;
// 取出这两个位置的值
int val1 = nums1[newIndex1];
int val2 = nums2[newIndex2];
// 如果 nums1 的第 k/2 个元素 <= nums2 的
// 说明 nums1 的 [index1 ... newIndex1] 这部分
// 一定不可能是第 k 小的元素
if (val1 <= val2) {
// 排除 nums1 的这部分元素
k -= (newIndex1 - index1 + 1);
index1 = newIndex1 + 1;
}
// 否则,排除 nums2 的一部分
else {
k -= (newIndex2 - index2 + 1);
index2 = newIndex2 + 1;
}
}
}
}