[LeetCode刷题笔记]4 - 寻找两个正序数组的中位数(归并 / 递归 / 二分查找)
创始人
2024-01-17 12:55:01

一、题目描述

  • 给定两个大小分别为 m 和 n 的正序(排好序了,从小到大)数组 nums1 和 nums2。请你找出并返回其 中位数 (这里指的是合并成一个数组后的中位数)。
  • 要求所用的算法时间复杂度应该在 O(log(m+n))O(log (m+n))O(log(m+n)) 以内。

示例:

输入输出
nums1=[1,3],nums2=[2]nums1 = [1,3], nums2 = [2]nums1=[1,3],nums2=[2]2.000002.000002.00000
nums1=[1,2],nums2=[3,4]nums1 = [1,2], nums2 = [3,4]nums1=[1,2],nums2=[3,4]2.500002.500002.50000

提示:

  • nums1.length=mnums1.length = mnums1.length=m
  • nums2.length=nnums2.length = nnums2.length=n
  • 0<=m<=10000 <= m <= 10000<=m<=1000
  • 0<=n<=10000 <= n <= 10000<=n<=1000
  • 1<=m+n<=20001 <= m + n <= 20001<=m+n<=2000
  • −106<=nums1[i],nums2[i]<=106-10^6 <= nums1[i], nums2[i] <= 10^6−106<=nums1[i],nums2[i]<=106

二、求解思路

思路一:归并(O(m+n)O(m+n)O(m+n))

  • 使用归并的方式,合并两个有序数组,得到一个大的有序数组。大的有序数组的中间位置的元素(这里需要根据元素个数做个判断),即为中位数。
  • 此时时间复杂度为 O(m+n)O(m+n)O(m+n),肯定不符合题意,需要进行改进。

C++代码

class Solution {
public:double findMedianSortedArrays(vector& nums1, vector& nums2) {int m = nums1.size(), n = nums2.size();vector mg;  // merge后的有序大数组int i = 0, j = 0;  // 双指针归并算法while (i < m && j < n) {// 把小的优先放入mgif (nums1[i] < nums2[j]) mg.push_back(nums1[i++]);else mg.push_back(nums2[j++]);}while (i < m) mg.push_back(nums1[i++]);  // 把nums1剩余元素直接依次放入mgwhile (j < n) mg.push_back(nums2[j++]);  // 把nums2剩余元素直接依次放入mg// 找到中位数int k = mg.size();double res;if (k % 2) res = mg[(k - 1) >> 1];else res = 1.0*(mg[k/2 - 1] + mg[k/2])/2;return res;}
};

复杂度分析

  • 时间复杂度:O(m+n)O(m+n)O(m+n)。
  • 空间复杂度:O(m+n)O(m+n)O(m+n)。

思路二:递归(O(log(m+n))O(log(m+n))O(log(m+n)))

原问题难以直接递归求解,所以我们先考虑这样一个问题:

在两个有序数组中,找出第 kkk 小的数。

如果该问题可以解决,那么第 (n+m)/2(n+m)/2(n+m)/2 小的数就是我们要求的中位数。

先从简单情况入手,假设 m,n≥k/2m,n≥k/2m,n≥k/2,我们先从 nums1nums1nums1 和 nums2nums2nums2 中各取前 k/2k/2k/2 个元素:

  • 如果 nums1[k/2−1]>nums2[k/2−1]nums1\left[k/2−1\right]>nums2\left[k/2−1\right]nums1[k/2−1]>nums2[k/2−1],则说明 nums1nums1nums1 中取的元素过多,nums2nums2nums2 中取的元素过少;因此 nums2nums2nums2 中的前 k/2k/2k/2 个元素一定都小于等于第 kkk 小数,所以我们可以先取出这些数,将问题归约成在剩下的数中找第 k−⌊k/2⌋k−⌊k/2⌋k−⌊k/2⌋ 小数。
  • 如果 nums1[k/2−1]≤nums2[k/2−1]nums1\left[k/2−1\right]≤nums2\left[k/2−1\right]nums1[k/2−1]≤nums2[k/2−1],同理可说明 nums1nums1nums1 中的前 k/2k/2k/2 个元素一定都小于等于第 kkk 小数,类似可将问题的规模减少一半。

现在考虑边界情况,如果 m

  • 如果 nums1[m−1]>nums2[k/2−1]nums1[m−1]>nums2[k/2−1]nums1[m−1]>nums2[k/2−1],则 nums2nums2nums2 中的前 k/2k/2k/2 个元素一定都小于等于第 kkk 小数,我们可以将问题归约成在剩下的数中找第 k−⌊k/2⌋k−⌊k/2⌋k−⌊k/2⌋ 小数。
  • 如果 nums1[m−1]≤nums2[k/2−1]nums1[m−1]≤nums2[k/2−1]nums1[m−1]≤nums2[k/2−1],则 nums1nums1nums1 中的所有元素一定都小于等于第 kkk 小数,因此第kkk小数是 nums2[k−m−1]nums2[k−m−1]nums2[k−m−1]。

C++代码

class Solution {
public:double findMedianSortedArrays(vector& nums1, vector& nums2) {// 计算长度总和,从而获得中位数的位置int total = nums1.size() + nums2.size();if (total % 2 == 0) {   // 如果total为偶数,则中位数是中间两个数的均值,位置分别在total/2和total/2+1// 注意:这里的位置均是从1开始的,比如说6个数,下标从1-6,中位数位置为第3和4个数的均值// 0 表示从数组的哪个下标开始搜索int left = findKthNumber(nums1, 0, nums2, 0, total / 2);int right = findKthNumber(nums1, 0, nums2, 0, total / 2 + 1);return (left + right) / 2.0;}else {   // 如果total为奇数,则中位数是位置为第total/2+1的数(这里是整数向下除法)return findKthNumber(nums1, 0, nums2, 0, total / 2 + 1);}}// 该函数作用:下标分别从i和j开始,搜索两个数组第k小的数(这里的第k小是指下标从1开始的第k小)int findKthNumber(vector &nums1, int i, vector &nums2, int j, int k) {   // 这里默认nums1的长度比nums2的长度短,否则就调换一下if (nums1.size() - i > nums2.size() - j) return findKthNumber(nums2, j, nums1, i, k);// 边界情况:nums1为空,则直接返回nums2的第j+k-1位置上的数if (nums1.size() == i) return nums2[j + k - 1];// 边界情况:k==1,即找最小值即可。注意:此时nums1和nums2一定都不为空if (k == 1) return min(nums1[i], nums2[j]);// 一般情况:先确定两个数组第k/2位置在数组中的下标是多少(si和sj)// 注意:由于nums1比较短,因此i+k/2可能会越界,需要和nums1.size()取个最小值int si = min(i + k / 2, int(nums1.size())), sj = j + k / 2;if (nums1[si - 1] > nums2[sj - 1]) {// 如果nums1第si位置上的数大于nums2第sj位置上的数,则可以删除nums2前sj个数// 因此,下一次迭代nums2从sj开始,且搜寻第k-k/2小的数return findKthNumber(nums1, i, nums2, sj, k - k / 2);} else {// 如果nums1第si位置上的数小于nums2第sj位置上的数,则可以删除nums1前si个数// 因此,下一次迭代nums1从si开始,且搜寻第k-(si-i)小的数return findKthNumber(nums1, si, nums2, j, k - (si - i));}}
};

复杂度分析

  • 时间复杂度:O(log(m+n))O(log(m+n))O(log(m+n))。k=(m+n)/2k=(m+n)/2k=(m+n)/2,且每次递归 kkk 的规模都减少一半,因此时间复杂度是 O(log(m+n))O(log(m+n))O(log(m+n)).
  • 空间复杂度:O(1)O(1)O(1)。

思路三:二分查找(O(log(min(m+n)))O(log(min(m+n)))O(log(min(m+n))))

『该算法的处理细节非常繁琐,建议新手直接跳过』

首先,让我们考虑只有一个有序数组的情况:

  • 如果我们将有序数组切分为等长的左右两部分,则 中位数 = (左半边的最大值 + 右半边的最小值) / 2
  • 切分情况有两种,例如:
    • 数组长度是偶数,对于 [2,3,5,7][2,3,5,7][2,3,5,7], 我们在 333 和 555 之间切分:[2,3∣5,7][2,3| 5,7][2,3∣5,7],则 中位数 =(3+5)/2
    • 数组长度是奇数,对于 [2,3,4,5,7][2,3,4,5,7][2,3,4,5,7],我们在 444 的位置切分,且让 444 属于左右两边:[2,3,(4∣4),5,7][2,3,(4 | 4),5,7][2,3,(4∣4),5,7],则 中位数 =(4+4)/2=(4+4)/2;

现在让我们来考虑两个有序数组的情况,类似于上述考虑方式:

  • 我们在两个数组中分别找到一个分割点 (分割点可能在相邻数之间,也可能在数上)。两个分割点左边元素的总个数等于右边元素的总个数,且左边元素的最大值 <= 右边元素的最小值,则该分割点即为所求。

为了同时处理分割点『在两数之间』和『在数上』的情况,我们在数组A1A_1A1​中可能是分割点的位置添加虚拟元素 ‘@’‘@’‘@’,这样我们枚举数组 A1′A'_1A1′​ 的所有元素,即可枚举 A1A_1A1​ 所有可能的分割点:

  • A1:[1,2,3,4,5]=>A1′:[@,1,@,2,@,3,@,4,@,5,@]A_1:[1,2,3,4,5]=>A'_1:[@,1,@,2,@,3,@,4,@,5,@]A1​:[1,2,3,4,5]=>A1′​:[@,1,@,2,@,3,@,4,@,5,@]
  • A2:[1,1,1,1]=>A2′:[@,1,@,1,@,1,@,1,@]A_2:[1,1,1,1]=>A'_2:[@,1,@,1,@,1,@,1,@]A2​:[1,1,1,1]=>A2′​:[@,1,@,1,@,1,@,1,@]

我们将数组 A1A_1A1​ 的长度记为 N1N_1N1​,则 A1′A'_1A1′​ 的长度为 2∗N1+12∗N_1+12∗N1​+1; A2A_2A2​ 的长度记为 N2N_2N2​,则 A2′A'_2A2′​ 的长度为 2∗N2+12∗N_2+12∗N2​+1。因此,总共有 2N1+2N2+22N_1+2N_2+22N1​+2N2​+2 个元素。

假设数组 A1′A'_1A1′​ 的分割点下标是 C1C_1C1​,数组 A2′A'_2A2′​ 的分割点下标是 C2C_2C2​,则 C1C_1C1​ 和 C2C_2C2​ 之间具有如下等式关系:

  • C1+C2=N1+N2C_1+C_2=N_1+N_2C1​+C2​=N1​+N2​
  • C1+C2=N1+N2C_1+C_2=N_1+N_2C1​+C2​=N1​+N2​

证明:除了 C1C_1C1​ 和 C2C_2C2​ 以外,共有 2N1+2N22N_1+2N_22N1​+2N2​ 个元素,要平均分配到左右两边,因此左边共有 N1+N2N_1+N_2N1​+N2​ 个元素。 数组下标从 000 开始,下标为 C1C_1C1​ 的元素左边有 C1C_1C1​ 个元素,下标为 C2C_2C2​ 的元素左边有 C2C_2C2​ 个元素,由此可得上述等式。

为了方便表述,在 A1′A'_1A1′​ 中,C1C_1C1​ 左边(包括C1C_1C1​)的最大值记为 L1L_1L1​,C1C_1C1​ 右边(包括C1C_1C1​)的最小值记为 R1R_1R1​;在 A2′A'_2A2′​ 中,C2C_2C2​ 左边(包括C2C_2C2​)的最大值记为 L2L_2L2​,C2C_2C2​ 右边(包括C2C_2C2​)的最小值记为 R2R_2R2​。

  • L1<=R1&&L1<=R2&&L2<=R1&&L2<=R2L_1<=R_1\ \&\&\ L_1<=R_2\ \&\&\ L_2<=R_1\ \&\&\ L_2<=R_2L1​<=R1​ && L1​<=R2​ && L2​<=R1​ && L2​<=R2​

由于 A1,A2A_1,A_2A1​,A2​ 都是有序的,因此 L1<=R1&&L2<=R2L_1<=R_1\ \&\&\ L_2<=R_2L1​<=R1​ && L2​<=R2​ 一定满足;不满足的情况有两种:

  • 如果 L1>R2L_1>R_2L1​>R2​,表示 A2A_2A2​中在分割点左侧的元素太少,此时我们需要将 C2C_2C2​ 右移;
  • 如果 L2>R1L_2>R_1L2​>R1​,表示 A2A_2A2​中在分割点左侧的元素太多,此时我们需要将 C2C_2C2​ 左移;

符合二分结构。

另外,我们在实际操作中,不需要真的在原数组中插入‘@’‘@’‘@’,只需找出 L1,R1,L2,R2L_1,R_1,L_2,R_2L1​,R1​,L2​,R2​ 和 C1,C2C_1,C_2C1​,C2​ 的关系即可。我们可以列表找规律:

C1C_1C1​L1L_1L1​R1R_1R1​
0INT_MINA1[0]
1A1[0]A1[0]
2A1[0]A1[1]
3A1[1]A1[1]
4A1[1]A1[2]

由此我们可以发现:

  • L1=A1[(C1−1)/2]L_1=A_1[(C_1−1)/2]L1​=A1​[(C1​−1)/2]
  • R1=A1[C1/2]R_1=A_1[C_1/2]R1​=A1​[C1​/2]

类似可得:

  • L2=A2[(C2−1)/2]L_2=A_2[(C_2−1)/2]L2​=A2​[(C2​−1)/2]
  • R2=A2[C2/2]R_2=A_2[C_2/2]R2​=A2​[C2​/2]

最后,还有两点需要注意:

  • 我们只能二分长度较小的数组,因为长度较长的数组中的某些分割点可能不合法,会出现 C1>N1+N2C_1>N_1+N_2C1​>N1​+N2​ 的情况;
  • 我们在数组边界设置两个哨兵,来处理 C1=0C_1=0C1​=0 或 C1=2N1C_1=2N_1C1​=2N1​ 的情况,这样做并不会影响结果,但可以简化代码:
    • A1[−1]=INTMIN,A1[2N1]=INTMAXA_1[−1]=INTMIN,A_1[2N_1]=INTMAXA1​[−1]=INTMIN,A1​[2N1​]=INTMAX

C++代码

class Solution {
public:double findMedianSortedArrays(vector& nums1, vector& nums2) {int N1 = nums1.size();int N2 = nums2.size();if (N1 < N2) return findMedianSortedArrays(nums2, nums1);int lo = 0, hi = N2 * 2;while (lo <= hi) {int mid2 = (lo + hi) / 2;int mid1 = N1 + N2 - mid2;double L1 = (mid1 == 0) ? INT_MIN : nums1[(mid1-1)/2];double L2 = (mid2 == 0) ? INT_MIN : nums2[(mid2-1)/2];double R1 = (mid1 == N1 * 2) ? INT_MAX : nums1[(mid1)/2];double R2 = (mid2 == N2 * 2) ? INT_MAX : nums2[(mid2)/2];if (L1 > R2) lo = mid2 + 1;else if (L2 > R1) hi = mid2 - 1;else return (max(L1,L2) + min(R1, R2)) / 2;}return -1;}
};

复杂度分析

  • 时间复杂度:O(log(min(n,m)))O(log(min(n,m)))O(log(min(n,m)))。二分长度较短的数组,且每次二分的复杂度是 O(1)O(1),所以总复杂度是O(log(min(n,m)))O(log(min(n,m)))O(log(min(n,m)))。
  • 空间复杂度:O(1)O(1)O(1)。

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
怎么样消灭臭虫 臭虫用什么药消... 奔奔虫控|被吸血臭虫骚扰的滋味,你体会过吗?相信被臭虫骚扰过的人都不想再体验一次了吧?那么臭虫应该怎...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
苁蓉的食用方法 鲜苁蓉的吃法有... 肉苁蓉号称“温而不热,补而不峻,暖而不燥,滑而不泄,故有从容之名。”历来也是传统药食同源的滋补佳品。...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...