“两个有序数组的中位数”其实就是 “求两个有序数组的第 kk 小元素” 的一个特殊情况(当 k=(m+n)/2k = (m+n)/2 或 (m+n+1)/2(m+n+1)/2 时)。
我们可以在原来“二分切分”的思想上稍加修改,把它改造成一个通用的 寻找第 kk 小元素 的解法。下面我给你详细说明:
🔑 思路改造
-
原来找中位数的核心:
- 我们在 nums1 中二分切一刀,得到左边 i 个数,右边 m-i 个数;
- 在 nums2 中对应切一刀,得到左边 j 个数,右边 n-j 个数;
- 保证 i+j = (m+n+1)/2,这样左半部分总长度和右半部分一样。
-
改成找第 kk 小:
- 我们依然在 nums1 中二分一个位置 i;
- 那么 nums2 的切分位置 j = k - i;
- 此时就保证 左边总共 i+j=k 个数,也就是第 kk 小一定出现在边界。
-
判定条件与边界:
-
和中位数类似:
- 如果 nums1[i-1] > nums2[j],说明 i 太大,要往左;
- 如果 nums2[j-1] > nums1[i],说明 i 太小,要往右;
- 否则,说明边界刚好平衡,第 k 小就是
max(nums1[i-1], nums2[j-1])。
-
-
为什么只要考虑左边最大值:
- 因为第 kk 小意味着左边正好有 kk 个数,第 kk 小就是左半部分的最大值。
🔨 代码实现(在原来代码上最小改动)
class Solution {
public int findKth(int[] nums1, int[] nums2, int k) {
int m = nums1.length;
int n = nums2.length;
// 保证 nums1 是较短的数组
if (m > n) {
return findKth(nums2, nums1, k);
}
int left = Math.max(0, k - n); // i 的下限(不能让 j<0)
int right = Math.min(k, m); // i 的上限(不能超过 m)
while (left <= right) {
int i = left + (right - left) / 2;
int j = k - i;
int nums1Left = (i == 0 ? Integer.MIN_VALUE : nums1[i - 1]);
int nums1Right = (i == m ? Integer.MAX_VALUE : nums1[i]);
int nums2Left = (j == 0 ? Integer.MIN_VALUE : nums2[j - 1]);
int nums2Right = (j == n ? Integer.MAX_VALUE : nums2[j]);
if (nums1Left <= nums2Right && nums2Left <= nums1Right) {
// 找到了平衡点
return Math.max(nums1Left, nums2Left);
} else if (nums1Left > nums2Right) {
right = i - 1;
} else {
left = i + 1;
}
}
throw new IllegalArgumentException("Input not valid");
}
}
📌 使用例子
int[] nums1 = {1, 3, 8};
int[] nums2 = {2, 4, 5, 6, 7, 9};
// 找第 5 小的数
System.out.println(new Solution().findKth(nums1, nums2, 5)); // 输出 5
⚡ 总结与中位数的关系
- 中位数 = 求第 (m+n+1)/2(m+n+1)/2 小(如果奇数)或平均 ((m+n)/2)((m+n)/2) 小与 ((m+n)/2+1)((m+n)/2+1) 小(如果偶数)。
- 第 k 小 = 求第 kk 小,直接用上面方法。
所以中位数问题其实就是 第 k 小问题的特例。
关于left、right初始化的解读:
这两行其实是 整个第 kk 小算法里最容易迷惑的地方。我们一步步拆开讲清楚:
背景回顾
我们要在 nums1 和 nums2 里切开,使得:
- 在 nums1 左边取了 ii 个元素;
- 在 nums2 左边取了 jj 个元素;
- 并且满足 i+j=ki + j = k。
这样左半边一共正好有 kk 个数,第 kk 小就是左半边的最大值。
所以:
j=k−ij = k - i
但要注意:i 和 j 都必须是合法范围。
- i 的范围:0,m0, m
- j 的范围:0,n0, n
因为切分不能超过数组的长度。
🔎 为什么要限制 i 的范围?
① 下限 Math.max(0, k - n)
-
因为
j = k - i,我们必须保证j >= 0。
如果j < 0,说明从 nums2 拿的数太少,不合法。 -
转换一下:
j=k−i≥0⇒i≤kj = k - i \geq 0 \quad \Rightarrow \quad i \leq k
同时还要保证
j=k−i≤n⇒i≥k−nj = k - i \leq n \quad \Rightarrow \quad i \geq k-n
-
所以 i 的下限就是
k - n(但至少是 0,因为 i 不能负数)。
👉left = Math.max(0, k - n)。
② 上限 Math.min(k, m)
- 因为
i至多是m,不能超过 nums1 的长度。 - 同时 i 不能大于 k(否则 j = k - i 会变负数)。
- 所以上限就是
Math.min(k, m)。
🌰 举个例子
假设:
nums1 = [1,3,8] (m=3)
nums2 = [2,4,5,6,7,9] (n=6)
k = 5
- 下限:
Math.max(0, k-n) = Math.max(0, 5-6) = 0 - 上限:
Math.min(k, m) = Math.min(5, 3) = 3
所以 i 的范围是 [0, 3]。
如果我们选:
- i=2 → j=3 → 左半边 nums1 取前 2 个 {1,3}, nums2 取前 3 个 {2,4,5},正好 5 个数 ✅。
- i=4 就不行,因为 nums1 只有 3 个数。
- i=-1 也不行,因为不能切在负数下标。
📌 总结一句话
这两行代码的作用就是:
保证 i 的取值范围既合法(0 ≤ i ≤ m),又能保证对应的 j = k - i 在 [0, n] 内。
换句话说:
👉 left = Math.max(0, k-n) = 防止 j 太大(i 太小)。
👉 right = Math.min(k, m) = 防止 j 太小(i 太大)。