一 实现思路
1 思维层面
方法1: 转化为求 第k小个数的值
1.1 题意转化:求2个有序数组的中位数 ==> 找到 nums1 和 nums2 总长度m+n里,第k小的数
- 把nums1.length 记作m,nums2.length 记作n
- 如果m+n是奇数,则k是 第(m+n)/2个数, 最后 res值 = 第k个数的值
- 如果m+n是偶数,则k是 v1 = 第(m+n)/2 和 v2 = 第(m+n)/2+1 个数,最后 res值 = (v1 + v2) / 2
2.1 如何在2个有序数组里分别操作,使其和 在一个m+n的数组里寻找中位数,效果是等价的
- 我们可以不实际合并数组,直接尝试寻找 2个数组中的 第k小元素
- 每次从2个数组中 各取第k/2个元素进行比较, 较小的那部分元素肯定不可能是第k小元素,可以安全排除(具体原因见3.1)
- 在剩余的部分中, 继续寻找第(k-排除数量) 小的元素
2.2 为什么排除k/2个元素后,继续递归能找到 正确的第k小元素
- 排除 2/k个元素的 安全性:见3.1,会具体证明
- 收敛性保证:
- 每次排除k/2个元素后,k的值会减半
- 到最后边界情况下,k=1时,找到第1小的数, 其中最小值就是 m+n范围内的 第k小的数
举一个具体例子来理解
假设nums1=[1,3,5,7],nums2=[2,4,6],寻找第4小的元素:
- k=4,k/2=2,比较 nums1[1]=3 和 nums2[1]=4
- 3 < 4,排除nums1的前2个元素[1,3]
- 现在问题变成:在 nums1=[5,7] 和 nums2=[2,4,6]中,找第(4-2)=2小的元素
- k=2,k/2=1,比较 nums1[0]=5 和 nums2[0]=2
- 5 > 2,排除 nums2的前1个元素[2]
- 现在问题变成:在 nums1=[5,7] 和 nums2=[4,6]中,找第(2-1)=1小的元素
- k=1,直接返回 min(5,4)=4 这就是原问题的第4小元素
3.1 具体 如何找到第k小的数
-
在 nums1 和 nums2 里,分别找到 第k/2小的数,记为 num1[i] 和 nums2[j]
-
如果 nums1[i] > nums2[j],那么num[j]最大的情况下,无非是
- nums1[0] <= nums1[1] <= ... <= nums1[i-1] <= nums1[j],此时 j至多也就是 第 k/2-1 + 2/k, 也就是 j最多是 第 k-1 小的数,不可能是 第k小的数
-
同样如果 nums1[i] <= nums2[j],那么num[i]最大的情况下,无非是
- nums2[0] <= nums2[1] <= ... <= nums2[j-1] <= nums1[i],此时 i至多也就是 第 k/2-1 + 2/k, 也就是 i最多是 第 k-1 小的数,不可能是 第k小的数
-
也就是说,通过每次在 nums1 和 nums2 里,通过比较各自 第k/2小的数,就可以排除掉 k/2 个数
-
一直递归缩小,直到找到第k小的数
方法2: 上下比较 + 二分查找法
1.1 题意转化:求2个有序数组的中位数 ==>
- 分别在数组 num1 和 num2里,各自找到1个切分点,使其满足已下中位数性质
- 性质1:左右区间 总成员数量大致相等
- 性质2:左区间最大值 <= 右区间最小值
1.2 性质1 抽象为数学表达
-
A的数量为m, B的数量为n,则总成员个数是 m+n
-
把 num1 拆分成的 2个区间,分别记作 L1、R1
-
把 num2 拆分成的 2个区间,分别记作 L2、R2
-
则 L = L1 + L2, R = R1 + R2
-
如果 m+n 是偶数,则 L.length = L1.length + L2.length = (m+n)/2, 注意 本文所有 / 都理解为底板除
-
如果 m+n 是奇数,则 L.length = L1.length + L2.length = (m+n+1)/2
-
无论 m+n 是奇数还是偶数,都可以统一表示为 L.length = (m+n+1)/2
-
即 无论 m+n 是奇数还是偶数,+1后的地板除的结果,对于L的值 都是正确的
1.3 性质2 抽象为数学表达
-
把 num1 的左区间的最大值,记作maxL1;num1的右区间的最小值,记作minR1
-
把 num2 的左区间的最大值,记作maxL2;num2的右区间的最小值,记作minR2
-
如果满足 maxL1 <= minR1, maxL2 <= minR2 且 maxL1 <= minR2, maxL2 <= minR1,则 max(L) <= min(R) 成立
-
这里默认 maxL1/maxL2/minR1/minR2 都存在,不存在的边界情况后续会 特殊处理
-
把 max(maxL1, maxL2) 记作 maxL, min(minR1, minR2) 记作 minR
-
此时,如果 m+n 是偶数,则 中位数的值res = (maxL + minR) / 2
-
如果 m+n 是奇数,则 中位数的值res = maxL
2 确定 i 和 j 的 取值关系
- i/j 表示的是 L1/L2里各自的 成员个数
- 又因为 L.length = L1 + L2 = (m+n+1)/2
- 所以 i + j = (m+n+1)/2, 即 j = (m+n+1)/2 - i
- 即 i 和 j 是 反向关系,i 越大,j 越小
- 这里需要保证 i是 相对 m/n里的 较小值,原因是:
-
假设 m < n, 如果 i 的 取值范围是[0, n]
-
因为 j = (m+n+1)/2 - i, 所以 j 的取值范围是[(m+n+1)/2 - n, (m+n+1)/2 - 0]
-
如果m极小,则 (m+n+1)/2 - n 趋向于 (n+1)/2 -n, 大概率越界
-
假设 m < n, 如果 i 的 取值范围是[0, m]
-
因为 j = (m+n+1)/2 - i, 所以 j 的取值范围是[(m+n+1)/2 - m, (m+n+1)/2 - 0]
-
如果m极小,则 (m+n+1)/2 - m 保证不会越界
-
- 综上,j = (m+n+1)/2 - i
- i是 相对 m/n里的 较小值,这样能同时保证 L1/L2 的 取值范围不会越界
3 确定 二分查找 缩放逻辑
-
由于 num1/num2 都是有序递增的,所以默认情况下,就满足一下性质
- maxL1 = num1[i-1], minR1 = num1[i], 且 maxL1 <= minR1
- 同理 maxL2 = num2[j-1], minR2 = num2[j], 且 maxL2 <= minR2
-
如果 maxL1 > minR2, 说明
- maxL1应该属于 R1区间, 即 i 需要减少/左移
- 而 i 减少/左移,又会让 j 对应增大/右移
- 这样就能同时实现 maxL1变小,minR2变大
- 由于我们是统一处理i的,所以此时 让i变小即可
-
如果 maxL2 > minR1, 说明
- maxL2应该属于 R2区间, 即 j 需要减少/左移
- 而 j 减少/左移,又会让 i 对应增大/右移
- 这样就能同时实现 maxL2变小,minR1变大
- 由于我们是统一处理i的,所以此时 让i变大即可
2 参考实现
二 代码实现
方法1: 求第k小个数的值 时间复杂度O( log(m + n) ) 空间复杂度 O( log(m + n) )
function findMedianSortedArrays(nums1, nums2) {
// 对于偶数长度数组,需要找第 total/2 和 (total/2 + 1) 个数
// 对于奇数长度数组,需要找第 total/2 + 1 个数
const total = nums1.length + nums2.length;
const k = Math.floor(total / 2);
// 奇数,中位值的值是 nums中第k+1小的数的值
if (total % 2) {
return findK(nums1, nums2, 0, 0, k + 1);
} else {
// 偶数,中位值的值是 (nums中第k小的数的值 + nums中第k+1小的数的值) / 2
const lVal = findK(nums1, nums2, 0, 0, k);
const rVal = findK(nums1, nums2, 0, 0, k + 1);
return (lVal + rVal) / 2;
}
}
// 在arr1[idx1, len1] 和 arr2[idx2, len2]范围内寻找 第k小的数
// 返回第k小个数 对应的值, 即 arr[k-1]
function findK(arr1, arr2, idx1, idx2, k) {
// 递归中止条件1: 如果arr1/arr2 已经遍历完,则直接返回arr2/arr1里的第k个数
// 易错点1.1: 注意这里idx1/idx2的值是有可能取到len1/len2的
// 易错点1.2: 需要注意如果arr1已空,此时arr2经过前几轮递归,已经偏移了idx2 个位置
// 所以此时索引不是 直接的k-1, 而是 idx2 + k - 1
if (idx1 >= arr1.length) return arr2[idx2 + k - 1];
if (idx2 >= arr2.length) return arr1[idx1 + k - 1];
// 递归中止条件2: 如果k=1,则直接返回arr1/arr2里的 当前idx位置 的数
if (k === 1) return Math.min(arr1[idx1], arr2[idx2]);
// 重点1: 每次尝试比较第2/k个数,从而一次性排除2/k个成员
const stepK = Math.floor(k / 2);
// 易错点2: 每次递归 获取本轮新的idx时,要 防止成员越界
const newIdx1 = Math.min(idx1 + stepK - 1, arr1.length - 1);
const newIdx2 = Math.min(idx2 + stepK - 1, arr2.length - 1);
// 比较2个值大小,排除掉较小的部分
// 每次递归的过程,就是 向右移动增大 idx1/idx2 的值 && 减少k的值
if (arr1[newIdx1] <= arr2[newIdx2]) {
// 易错点3.1: 即将右移的 新的idx1值,要排除掉本轮比较的 newIdx1,所以是 newIdx1 + 1
// 由于 newIdx的范围是[0, len1-1],所以 newIdx1 + 1 可能取到 len1的
// 这也是上面 易错点1.1的 原因
// 易错点3.2: 这里新的缩小的 k值,其实是 k - 本轮排除掉的成员数量
// 而 本轮排除掉的成员数量 = 新位置索引 - 旧位置索引 + 1 = newIdx1 - idx1 + 1
return findK(arr1, arr2, newIdx1 + 1, idx2, k - (newIdx1 - idx1 + 1));
} else {
return findK(arr1, arr2, idx1, newIdx2 + 1, k - (newIdx2 - idx2 + 1));
}
}
方法2: 上下比较 + 二分查找法 时间复杂度O(log(min(m,n))) 空间复杂度O(1)
function findMedianSortedArrays(nums1, nums2) {
// 保证 nums1 是 较短的数组
if (nums1.length > nums2.length) {
[nums1, nums2] = [nums2, nums1];
}
const m = nums1.length;
const n = nums2.length;
// 保证i是在 较小长度的数组里取值的,确保i/j的 取值范围不会越界
let l = 0, r = m;
while (l <= r) {
let i = Math.floor((l + r) / 2);
let j = Math.floor((m + n + 1) / 2) - i;
// 易错点1:如果是 nums1[i - 1] || -Infinity 的写法
// 那么在遇到 nums1[i - 1]为0时,也会错误返回 -Infinity,而不是0
const maxL1 = nums1[i - 1] ?? -Infinity;
const maxL2 = nums2[j - 1] ?? -Infinity;
const minR1 = nums1[i] ?? Infinity;
const minR2 = nums2[j] ?? Infinity;
// 说明maxL1应该在R1而不是L1, i左移减少
if (maxL1 > minR2) {
r = i - 1;
} else if (maxL2 > minR1) {
// 说明maxL2应该在R2而不是L2, j需要左移减少==> i需要右移增大
l = i + 1;
} else {
// m+n是奇数,则直接返回maxL
// m+n是偶数,则返回(maxL + minR) / 2
return (m + n) % 2 === 1
? Math.max(maxL1, maxL2)
: (Math.max(maxL1, maxL2) + Math.min(minR1, minR2)) / 2;
}
}
}