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


题目描述:

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

示例 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

示例 3:

1
2
输入:nums1 = [0,0], nums2 = [0,0]
输出:0.00000

示例 4:

1
2
输入:nums1 = [], nums2 = [1]
输出:1.00000

示例 5:

1
2
输入:nums1 = [2], nums2 = []
输出:2.00000

提示:

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

链接:

https://leetcode-cn.com/problems/median-of-two-sorted-arrays


题目分析

1.合并数组解法

  由于这是两个已经有序的数组,很容易想到归并排序。也就是将两个数组合并成一个,然后再返回处在中间的数字即可。注意合并的时候要考虑某一个数组已经遍历完毕的情况,另外对于奇数个数字和偶数个数字,中位数的计算也不同。

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
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size();
int n = nums2.size();
vector<int> nums(m+n);
int i = 0, j = 0, cnt = 0;
while(cnt < m + n){
// nums1 已经遍历完毕
if(i >= m){
while(j < n){
nums[cnt++] = nums2[j++];
}
break;
}
// nums2 已经遍历完毕
if(j >= n){
while(i < m){
nums[cnt++] = nums1[i++];
}
break;
}

if(nums1[i] < nums2[j]){
nums[cnt++] = nums1[i++];
}
else{
nums[cnt++] = nums2[j++];
}
}

// 数目为偶数时中位数是中间两个数字的平均数
if(cnt % 2 == 0){
return (nums[cnt/2 - 1] + nums[cnt/2]) / 2.0;
}
else{
return nums[cnt/2];
}
}
};

  时间复杂度:$O(m+n)$,其中 $m、n$ 分别是两个数组的大小。因为顺序遍历了两个数组进行合并,合并后返回中位数的过程是 $O(1)$ 的。
  空间复杂度:$O(m+n)$,其中 $m、n$ 分别是两个数组的大小。因为开辟了一个新数组用来存放合并后的数组。

2.合并数组解法改进

  由于我们只需要寻找中位数,其实可以不用将真的将两个数组合并,只需要按顺序寻找到中位数就可以了。由于奇数和偶数计算中位数方式的不同,我们用变量 right 来记录当前第 cnt 小的数,而用 left 来记录上一个 right 也即第 cnt-1 小的数。遍历次数 cnt 为 $(m+n)/2+1$,这样当 $(m+n)$ 为奇数时,我们找到了第 $(m+n)/2+1$ 小的数也即中位数,返回 right;当 $(m+n)$ 为偶数时,left 是第 $(m+n)/2$ 小的数,right 是第 $(m+n)/2+1$ 小的数,中位数是这两个数的平均值,返回 (left+right)/2.0。(这也是变量命名的含义,中位数是左右两个数的平均值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size();
int n = nums2.size();
int i = 0, j = 0, left = -1, right = -1;
// 遍历次数 (m+n)/2+1
for(int cnt = 0; cnt <= (m+n)/2; cnt++){
left = right;
if(i < m && (j >= n || nums1[i] < nums2[j])){
right = nums1[i];
i++;
}
else{
right = nums2[j];
j++;
}
}

if((m + n) % 2 == 0) return (left + right) / 2.0;
else return right;
}
};

  时间复杂度:$O(m+n)$,其中 $m、n$ 分别是两个数组的大小。思路和上面一致,但是只需要遍历一半,因为我们只需要找到中位数就可以停止了。
  空间复杂度:$O(1)$。我们只需要常数个变量对遍历结果进行记录。

3.二分查找解法

  由于数组都是有序的,而我们只需要找到处在中间的那个数,可以考虑用二分查找的方法加快搜索速度。一个思路是对于 nums1nums2 两个数组,我们要寻找第 k 小的数,则每个数组都寻找第 k/2 个数,假设分别为 nums1[k/2-1]nums2[k/2-1],比较他们的大小。

  • nums1[k/2-1] >= nums2[k/2-1],则说明 nums2[k/2-1] 最多也只能比 nums1[0 ~ k/2-2]nums2[0 ~ k/2-2] 都大,最多只有 $(k/2-1)+(k/2-1)=k-2$ 个数比它小,则它小于第 k 小的数,也即 nums2[0 ~ k/2-1] 都可以进行排除。
  • nums1[k/2-1] < nums2[k/2-1],则相应排除 nums1[0 ~ k/2-1]

  在剩下的数组中,我们就要找第 k-(k/2) 小的数了。如此二分排除下去就能够得到答案,我们可以采用循环的写法,依然将 k-(k/2) 当做下一个 k。这样的算法下,也有几种特殊情况。

  • 数组中剩下的数字已经不够 k/2 个,这个时候,我们只需要选取数组最后一个数字作为比较对象,若进行排除则整个数组排除即可。当然,相应的,我们排除的数字个数可能就不再是 k/2,更新 k 值的时候需要注意。
  • 如果一个数组已经全部排除,则说明中位数存在于另外一个数组中,我们直接返回另外一个数组中第 k 个数即可。
  • k1,则到了循环的终点,我们返回两个数组中首元素值更小的那个数即可。
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
class Solution {
public:
int getKthNumber(vector<int>& nums1, vector<int>&nums2, int k){
int m = nums1.size();
int n = nums2.size();
int i = 0, j = 0;
// 循环查找
while(true){
// nums1 已经全部排除
if(i == m) return nums2[j + k - 1];
// nums2 已经全部排除
if(j == n) return nums1[i + k - 1];
// k == 1
if(k == 1) return min(nums1[i], nums2[j]);

// 正常情况
int newi = min(i+k/2-1, m-1);
int newj = min(j+k/2-1, n-1);
if(nums1[newi] <= nums2[newj]){
k -= newi - i + 1;
i = newi + 1;
}
else{
k -= newj - j + 1;
j = newj + 1;
}
}

}

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

  时间复杂度:$O(log(m+n))$,其中 $m、n$ 分别是两个数组的大小。因为初始 $k=(m+n)/2$ 或 $k=(m+n)/2+1$,而每一次搜索会将范围减半。
  空间复杂度:$O(1)$。我们只需要常数个变量对搜索结果进行记录。

4.划分数组解法

  从另一个角度想,中位数其实就是将数组分割为两个相等长度的子集。而数组的长度我们是知道的,则每个子集的长度我们也是知道的。则我们在 i 处将 nums1 分割为两部分时,相应也就知道了在 nums2 中的哪一处分割为两部分,假设为 j。如果满足 nums1[i-1] <= nums2[j]nums1[i] >= nums2[j-1]。即左半部分为 nums1[0 ~ i-1]nums2[0 ~ j-1],右半部分为 nums1[i ~ m-1]nums2[j ~ n-1]。此时可以满足左边的所有值都小于右边,则中位数就找到了。这样我们只需要对其中某一个数组进行二分查找,时间复杂度会更低一些。

  • m+n 为偶数,则我们应该有 $i+j=m-i+n-j$。
    中位数是 (max(nums1[i-1], nums2[j-1])+min(nums1[i], nums2[j]))/2
  • m+n 为奇数,则我们应该有 $i+j=m-i+n-j+1$。(左半部分多一个数)
    中位数是 max(nums1[i-1], nums2[j-1])
  • 根据上面两点可以得到 $j = \lfloor\frac{m+n+1}{2}\rfloor-i$。
    我们令 nums1 的长度小于 nums2 的长度,则可以保证 $j\in[0,n]$。
  • 如果 nums1 长度大于 nums2 则交换两个数组即可。
  • 对于整个数组都存在某半部分的情况,我们可以令 nums1[-1]=nums2[-1]=INT_MINnums1[m]=nums2[n]=INT_MAX。这样不会对左半部分的最大值或者右半部分的最小值产生影响。

  PS::代码来自官方题解。

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
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
if (nums1.size() > nums2.size()) {
return findMedianSortedArrays(nums2, nums1);
}

int m = nums1.size();
int n = nums2.size();
int left = 0, right = m;
// median1:前一部分的最大值
// median2:后一部分的最小值
int median1 = 0, median2 = 0;

while (left <= right) {
// 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
// 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
int i = (left + right) / 2;
int j = (m + n + 1) / 2 - i;

// nums_im1, nums_i, nums_jm1, nums_j 分别对应
// nums1[i-1], nums1[i], nums2[j-1], nums2[j]
int nums_im1 = (i == 0 ? INT_MIN : nums1[i - 1]);
int nums_i = (i == m ? INT_MAX : nums1[i]);
int nums_jm1 = (j == 0 ? INT_MIN : nums2[j - 1]);
int nums_j = (j == n ? INT_MAX : nums2[j]);

if (nums_im1 <= nums_j) {
median1 = max(nums_im1, nums_jm1);
median2 = min(nums_i, nums_j);
left = i + 1;
} else {
right = i - 1;
}
}

return (m + n) % 2 == 0 ? (median1 + median2) / 2.0 : median1;
}
};

  时间复杂度:$O(log(min(m,n)))$,其中 $m、n$ 分别是两个数组的大小。这是因为我们只对长度较小的那个数组进行了二分查找。
  空间复杂度:$O(1)$。我们只需要常数个变量对查找结果进行记录。