Fork me on GitHub

4. 寻找两个正序数组的中位数

题目

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

算法的时间复杂度应该为 O(log (m+n))

示例 1:

1
2
3
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2

示例 2:

1
2
3
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

提示:

  • nums1.length == m
  • nums2.length == n
  • 0 <= m <= 1000
  • 0 <= n <= 1000
  • 1 <= m + n <= 2000
  • -10^6 <= nums1[i], nums2[i] <= 10^6

简介

1
2
3
4
5
6
7
8
9
10
11
12
讨论在两个有序数组中寻找中位数的问题

观察单个有序数组,通过比较元素大小找到舍弃的元素,确定中位数。

类推两个有序数组,准备两个区间来存放偏小值和偏大值。
根据区间需要满足必要条件,使得偏小区间的元素都小于偏大区间。
问题转化为在单个有序数组中查找满足条件的关键元素位置,可以使用二分查找。

关键词:
偏小区间
偏大区间
二分查找

正文

首先思考我们是否可以避免全排序找到中位数

我们有一种想法就是通过比较元素的大小找到需要舍弃的元素

我们观察一个有序数组,我们准备两个区间 RSmall, RBig 分别存放这个数组的偏小值与偏大值(相对中位数而言)

分别用 left,right 指针从两头操作偏小值与偏大值,将他们依次放入对应的数组

直到 「偶数时 RSmall.size = RBig.size 或者 奇数时 RSmall.size = RBig.size + 1」,中位数显然易得

好,现在我们来看两个有序数组的复杂情况

我们同样准备两个区间 RSmall, RBig,同样我们需要将数组偏小值放进 RSmall,偏大值放进 RBig

与一个有序数组不同的是,现在我们必须综合考虑两个有序数组的情况

所以 RSmall 区间里可能既包含 num1 数组的偏小元素也包含 num2 数组的偏小元素,对于RBig也是一样

这就让我们的思考变得复杂了,这使得 RSmall,RBig 变得无序

现在将这个 RSmall 再细分为 RSmallN1 区间,以及 RSmallN2 区间,同样有 RBigN1,RBigN2 区间

想要得到中位数,那么偶数时 RSmall.size = RBig.size 或者 奇数时 RSmall.size = RBig.size + 1

也就有

「 必要条件一: 偶数时 RSmallN1.size + RSmallN2.size = RBigN1.size + RBigN2.size 奇数时 RSmallN1.size + RSmallN2.size = RBigN1.size + RBigN2.size +1 」

同时 RSmall 区间内任何一个元素都必须小于 RBig 区间

自然有 Small 区间的子区间的任何一个元素都小于 Big 区间的子区间的任何一个元素

所以得到

「 必要条件二: RSmallN1 的最大值 < RBigN1 的最小值 ( 因为数组有序,这是不用判断的 ) RSmallN1 的最大值 < RBigN2 的最小值 RSmallN2 的最大值 < RBigN1 的最小值 RSmallN2 的最大值 < RBigN2 的最小值 ( 因为数组有序,这是不用判断的 )」

简化一下

「 必要条件二: maxOf( RSmallN1 ) < minOf ( RBigN2 ) maxOf( RSmallN2 ) < minOf ( RBigN1 ) 」

显然必要条件一加上必要条件二就是充分必要条件

现在我们要得到这四个区间,或者说要在两个有序数组分别划分出这四个区间

我们要在这两个数组中分别找到关键的那个分隔元素

因为必要条件一的原因,我们找到了 num1 数组的分割元素,我们也就找到了 num2 数组的分割元素

这非常好理解,因为 Small 区间和 Big 区间的数量是固定的,RSmallN1 多了一个元素,RSmallN2 就要对应减少一个元素

所以我们只要找到 num1 数组的分隔元素即可

至此,问题成功转化成了

在 num1 数组中查找出一个关键元素位置,这个关键元素满足上述两条必要条件

这是一个典型的有判断条件的查找问题,我们就可以使用二分查找,测试每个元素是否符合上述两个条件

关于二分查找这里不再赘述。

以下是基于二分查找的 Kotlin 代码实现,用于找出两个有序数组的中位数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 该函数用于在两个已排序整数数组中找到它们的中位数,返回类型为 Double。
fun findMedianSortedArrays(nums1: IntArray, nums2: IntArray): Double {
// 获取 nums1 和 nums2 的长度。
val n1 = nums1.size
val n2 = nums2.size
// 如果 nums1 的长度大于 nums2,则交换它们,确保 nums1 的长度小于等于 nums2。
if (n1 > n2) {
return findMedianSortedArrays(nums2, nums1)
}
// 设置左右指针的初始值。
var left = 0 // 如果把 num1 和 num2 合并元素重新排序,最左边
var right = n1 // 如果把 num1 和 num2 合并元素重新排序,相当于 num2 最左边
// 当左指针小于等于右指针时,执行二分查找。
while (left <= right) { // 因为 n1 < n2
// 计算 nums1 中间的索引 i 和 nums2 中间的索引 j。
val num1HalfIndex = (left + right) / 2
val num2HalfIndex = ((n1 + n2 + 1) / 2) - num1HalfIndex
// 计算 nums1 和 nums2 的左侧和右侧的最大值和最小值。
val maxOfNum1SmallRange = if (num1HalfIndex == 0) Int.MIN_VALUE else nums1[num1HalfIndex - 1] // nums1 小区间的最大值
val minOfNum1BigRange = if (num1HalfIndex == n1) Int.MAX_VALUE else nums1[num1HalfIndex] // nums1 大区间的最小值
val maxOfNum2SmallRange = if (j == 0) Int.MIN_VALUE else nums2[j - 1] // nums2 小区间的最大值
val minOfNum2BigRange = if (j == n2) Int.MAX_VALUE else nums2[j] // nums2 大区间的最小值
// 如果满足条件,则返回中位数。
if (maxOfNum1SmallRange <= minOfNum2BigRange && maxOfNum2SmallRange <= minOfNumBigRange) { // num1小 < num2大 && num2小 < num1大, 也就是说“小区间都小于大区间”
return if ((n1 + n2) % 2 == 0) { // 如果 nums1 和 nums2 的长度之和为偶数
// 则返回左右两侧最大值和最小值的平均值。
(maxOf(maxOfNum1SmallRange, maxOfNum2SmallRange).toDouble() + minOf(minOfNum1BigRange, minOfNum2BigRange).toDouble()) / 2.0
} else {
// 如果 nums1 和 nums2 的长度之和为奇数,则返回左侧最大值。
maxOf(maxOfNum1SmallRange, maxOfNum2SmallRange).toDouble()
}
} else if (maxOfNum1SmallRange > minOfNum2BigRange) { // num1大 < num2小
// 如果 nums1 的左侧最大值大于 nums2 的右侧最小值,则需要将 num1HalfIndex 向左移动。
/*
left num1HalfIndex right
| | |
1 1 1 1 1 1 1 1 1 1 1 1 E
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 E
|
num2HalfIndex
*/
right = num1HalfIndex - 1
/*
num1HalfIndex
left | right
| | |
1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 E
|
num2HalfIndex
*/
} else {
// 如果 nums2 的左侧最大值大于 nums1 的右侧最小值,则需要将 num1HalfIndex 向右移动。
/*
left num1HalfIndex right
| | |
1 1 1 1 1 1 1 1 1 1 1 1 E
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 E
|
num2HalfIndex
*/
left = num1HalfIndex + 1
/*
留给大家画
*/
}
}
// 如果找不到中位数,则返回 0.0。
return 0.0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fun findMedianSortedArrays(nums1: IntArray, nums2: IntArray): Double {
val n1 = nums1.size
val n2 = nums2.size
if (n1 > n2) {
return findMedianSortedArrays(nums2, nums1)
}
var left = 0
var right = n1
while (left <= right) { // 因为 n1 < n2
val num1HalfIndex = (left + right) / 2
val num2HalfIndex = ((n1 + n2 + 1) / 2) - num1HalfIndex
val maxOfNum1SmallRange = if (num1HalfIndex == 0) Int.MIN_VALUE else nums1[num1HalfIndex - 1]
val minOfNum1BigRange = if (num1HalfIndex == n1) Int.MAX_VALUE else nums1[num1HalfIndex]
val maxOfNum2SmallRange = if (j == 0) Int.MIN_VALUE else nums2[j - 1]
val minOfNum2BigRange = if (j == n2) Int.MAX_VALUE else nums2[j]
if (maxOfNum1SmallRange <= minOfNum2BigRange && maxOfNum2SmallRange <= minOfNumBigRange) {
return if ((n1 + n2) % 2 == 0) {
(maxOf(maxOfNum1SmallRange, maxOfNum2SmallRange).toDouble() + minOf(minOfNum1BigRange, minOfNum2BigRange).toDouble()) / 2.0
} else {
maxOf(maxOfNum1SmallRange, maxOfNum2SmallRange).toDouble()
}
} else if (maxOfNum1SmallRange > minOfNum2BigRange) {
right = num1HalfIndex - 1
} else {
left = num1HalfIndex + 1
}
}
return 0.0
}

留给大家一个问题,问 N 个有序数组的中位数怎么求?

, ,