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

(1)题目

给定两个大小分别为 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
  • -106 <= nums1[i], nums2[i] <= 106

(2)题解

给定两个有序数组,要求找到两个有序数组的中位数,最直观的思路有以下两种:

  1. 使用归并的方式,合并两个有序数组,得到一个大的有序数组。大的有序数组的中间位置的元素,即为中位数。第一种思路的时间复杂度是 O(m+n) ,空间复杂度是 O(m+n) ;
  2. 不需要合并两个有序数组,只要找到中位数的位置即可。由于两个数组的长度已知,因此中位数对应的两个数组的下标之和也是已知的。维护两个指针,初始时分别指向两个数组的下标 0 的位置,每次将指向较小值的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针),直到到达中位数的位置。空间复杂度降到 O(1) ,时间复杂度仍是 O(m+n) ;
  • 不合并数组,依次取数,直至中位数位置

    中位数的位置一定在 (m+n)/2 附近:

    • 如果是偶数,中位数就是最后取的两个数的平均数;
    • 如果是奇数,中位数就是最后取的那个数。

    取数阶段,循环 (m + n) / 2.0 + 1 次即可;判断中位数时,关键是奇数情况,可能有指针溢出,需要判断一下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 从 nums1 和 nums2 中每次取一个数,直到取出的数的个数 <= (m+n)/2 + 1
    int m = nums1.size(), n = nums2.size();
    int x = 0, y = 0; // x, y 分别是 nums1 和 nums2 的下标
    for (int i = 1; i <= (m + n) / 2.0 + 1; ++i){
    if (x < m && y < n){
    if (nums1[x] < nums2[y]) ++x;
    else ++y;
    }else if (x < m) ++x;
    else ++y;
    }

    if ((m + n) % 2 == 0){
    return (nums1[x - 1] + nums2[y - 1]) / 2.0;
    }else{
    if (x >= m){
    return nums1[x - 1];
    }else if (y >= n){
    return nums2[y - 1];
    }else{
    return min(nums1[x - 1], nums2[y - 1]);
    }
    }

    该方法测试用例可行,但 LeetCode 无法通过,可能是指针溢出形成空指针不被允许。

  • 改进

    我们首先计算出两个数组的长度,然后将它们的长度相加,得到总长度。接下来,我们使用一个循环来遍历这两个数组,直到到达中位数的位置。在循环中,我们维护两个指针 aStartbStart,它们分别指向两个数组的下标 0 的位置。我们还维护两个变量 leftright,它们分别表示当前的中位数和前一个元素。在每次循环中,我们将 left 的值更新为 right,然后将指向较小值的指针后移一位。最后,如果总长度是偶数,则中位数是 leftright 的平均值,否则中位数是 right

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    int m = nums1.size(), n = nums2.size();
    int len = m + n;
    int left = -1, right = -1;
    int aStart = 0, bStart = 0;

    for (int i = 0; i <= len / 2; i++){
    left = right;
    if (aStart < m && (bStart >= n || nums1[aStart] < nums2[bStart])){
    right = nums1[aStart++];
    }else{
    right = nums2[bStart++];
    }
    }

    if (len % 2 == 0){
    return (left + right) / 2.0;
    }else{
    return right;
    }
  • 二分查找

    如果对时间复杂度的要求有 log⁡ ,通常都需要用到二分查找:

    根据中位数的定义,当 m+n 是奇数时,中位数是两个有序数组中的第 (m+n)/2 个元素,当 m+n 是偶数时,中位数是两个有序数组中的第 (m+n)/2 个元素和第 (m+n)/2+1 个元素的平均值。因此,这道题可以转化成寻找两个有序数组中的第 k 小的数,其中 k 为 (m+n)/2 或 (m+n)/2+1 。

    假设两个有序数组分别是 A 和 B。要找到第 k 个元素,我们可以比较 A[k/2−1] 和 B[k/2−1] ,其中 / 表示整数除法。由于 A 和 B 的前面分别有 k/2−1 个元素,对于 A[k/2−1] 和 B[k/2−1] 中的较小值,最多只会有 (k/2−1)+(k/2−1)≤k−2 个元素比它小,那么它就不能是第 k 小的数了。

    因此我们可以归纳出三种情况:

    1. 如果 A[k/2−1]<B[k/2−1] ,则比 A[k/2−1] 小的数最多只有 A 的前 k/2−1 个数和 B 的前 k/2−1 个数,即比 A[k/2−1] 小的数最多只有 k−2 个,因此 A[k/2−1] 不可能是第 k 个数,A[0] 到 A[k/2−1] 也都不可能是第 k 个数,可以全部排除。
    2. 如果 A[k/2−1]>B[k/2−1] ,则可以排除 B[0] 到 B[k/2−1] 。
    3. 如果 A[k/2−1]=B[k/2−1] ,则可以归入第一种情况处理。

    可以看到,比较 A[k/2−1] 和 B[k/2−1] 之后,可以排除 k/2 个不可能是第 k 小的数,查找范围缩小了一半。同时,我们将在排除后的新数组上继续进行二分查找,并且根据我们排除数的个数,减少 k 的值,这是因为我们排除的数都不大于第 k 小的数。

    有以下三种情况需要特殊处理:

    1. 如果 A[k/2−1] 或者 B[k/2−1] 越界,那么我们可以选取对应数组中的最后一个元素。在这种情况下,我们必须根据排除数的个数减少 k 的值,而不能直接将 k 减去 k/2。
    2. 如果一个数组为空,说明该数组中的所有元素都被排除,我们可以直接返回另一个数组中第 k 小的元素。
    3. 如果 k=1 ,我们只要返回两个数组首元素的最小值即可。

    用一个例子说明上述算法。假设两个有序数组如下:

    1
    2
    A: 1 3 4 9
    B: 1 2 3 4 5 6 7 8 9

    两个有序数组的长度分别是 4 和 9,长度之和是 13,中位数是两个有序数组中的第 7 个元素,因此需要找到第 k=7 个元素。

    比较两个有序数组中下标为 k/2−1=2 的数,即 A[2]=4 和 B[2]=3 ,如下面所示:
    由于 A[2]>B[2] ,因此排除 B[0] 到 B[2] ,即数组 B 的下标偏移(offset)变为 3,同时更新 k 的值:k=k−k/2=4。

    1
    2
    A: 1 3 4 9
    B: [1 2 3] 4 5 6 7 8 9

    下一步寻找,比较两个有序数组中下标为 k/2−1=1 的数,即 A[1]=3 和 B[4]=5 ,如下面所示,其中方括号部分表示已经被排除的数。
    由于 A[1]<B[4],因此排除 A[0] 到 A[1],即数组 A 的下标偏移变为 2,同时更新 k 的值:k=k−k/2=2。

    1
    2
    A: [1 3] 4 9
    B: [1 2 3] 4 5 6 7 8 9

    下一步寻找,比较两个有序数组中下标为 k/2−1=0 的数,即比较 A[2]=4 和 B[3]=4 ,如下面所示,其中方括号部分表示已经被排除的数。
    由于 A[2]=B[3],根据之前的规则,排除 A 中的元素,因此排除 A[2] ,即数组 A 的下标偏移变为 3 ,同时更新 k 的值: k=k−k/2=1 。

    1
    2
    A: [1 3 4] 9
    B: [1 2 3] 4 5 6 7 8 9

    由于 k 的值变成 1,因此比较两个有序数组中的未排除下标范围内的第一个数,其中较小的数即为第 k 个数,由于 A[3]=9>B[3]=4 ,因此第 k 个数是 B[3]=4。

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
class Solution {
public:
int getKthElement(const vector<int>& nums1, const vector<int>& nums2, int k) {
/* 主要思路:要找到第 k (k>1) 小的元素,那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
* 这里的 "/" 表示整除
* nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个
* nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个
* 取 pivot = min(pivot1, pivot2),两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个
* 这样 pivot 本身最大也只能是第 k-1 小的元素
* 如果 pivot = pivot1,那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums1 数组
* 如果 pivot = pivot2,那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums2 数组
* 由于我们 "删除" 了一些元素(这些元素都比第 k 小的元素要小),因此需要修改 k 的值,减去删除的数的个数
*/

int m = nums1.size();
int n = nums2.size();
int index1 = 0, index2 = 0;

while (true) {
// 边界情况
if (index1 == m) {
return nums2[index2 + k - 1];
}
if (index2 == n) {
return nums1[index1 + k - 1];
}
if (k == 1) {
return min(nums1[index1], nums2[index2]);
}

// 正常情况
int newIndex1 = min(index1 + k / 2 - 1, m - 1);
int newIndex2 = min(index2 + k / 2 - 1, n - 1);
int pivot1 = nums1[newIndex1];
int pivot2 = nums2[newIndex2];
if (pivot1 <= pivot2) {
k -= newIndex1 - index1 + 1;
index1 = newIndex1 + 1;
}
else {
k -= newIndex2 - index2 + 1;
index2 = newIndex2 + 1;
}
}
}

double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int totalLength = nums1.size() + nums2.size();
if (totalLength % 2 == 1) {
return getKthElement(nums1, nums2, (totalLength + 1) / 2);
}
else {
return (getKthElement(nums1, nums2, totalLength / 2) + getKthElement(nums1, nums2, totalLength / 2 + 1)) / 2.0;
}
}
};

(3)知识点