【Leetcode】4. Median of Two Sorted Arrays 双有序数组找中位数

一、 描述

给两个有序数组,长度分别为m和n,找他们的中位数,要求时间复杂度O(log(m+n))。

二、分析

咋一看是很简单的题,但是坑了我两个星期。其中既有两年没做题的因素在,但是更重要的是本就菜逼的我更加菜了。这让我十分焦虑。
我分析了好久,但是从第一步开始就错了,实际上不应该叫错,要是硬着头皮做应该还是做得出的,但那得多头铁啊,反正我没这么铁。
先来分析一下这道题,咋一看时间复杂度要求log,那肯定是二分啊。有序数组,那就对数组做二分呗。但是,求中位数咋才能和对数组二分联系起来呢?
我想到这么一招:我设第一个数组的中位数为mid_1,第二个数组的中位数为mid_2,那么我们就可以根据中位数把这俩数组分为四段:
******* mid_1 *******
******* mid_2 *******
那么如果我们通过比较,有mid_1 > mid_2,那么就有mid_1的右边那一段要大于mid_1左边那一段,同时又有mid_1右边那一段大于mid_2左边那一段。所以mid_1右边那一段肯定不在中位数的左边。这样我们就能砍下去一段。
啊对,能砍下去一段,然后呢?
然后我们就一直砍,砍到…
砍到什么时候?
我这时候发现这个终点不好定义。砍到什么时候为止呢?
首先,很直观的,如果俩数组的长度加起来为原来总长度的一半,那么俩数组的最后一个值里面较大的那个就是中位数。这一点得明白,中位数就是第“总长度的一半”那么大的数。所以我如果砍到最后就剩“总长度的一半”那么多个数了,那最后一个就是中位数。“总长度的一半”说起来太麻烦 ,我还是用k来代替。如果砍到剩k个元素,就能找出来最后一个。
真不错,这就完了?试试呗。submit一下。不出所料报错了。
啥错呢?[1,3],[2]处理不了。
为啥处理不了?因为我在设置mid_1和mid_2的时候,当数组中元素个数为偶数的时候,我们让中间偏后的那一个作为mid
因为mid_1就是3,它砍不下去,它右边没有数了。然而我们的k又是2,现在总长度为3,一直循环了。
所以不行。那咋办呢?问题出在哪呢?出在当中位数右边没有元素的时候,这个递归就走不下去了。想个法子干他一炮。咋办呢?我们来看看左边能不能搞一搞。
之前咱们说到,中位数较大的那个的右边那段可以砍下去,因为我们最终结果肯定不在这段里。那么反过来,中位数较小的那个的左边那段同样可以砍下去,我们的最终结果肯额定也不在这一段。但是同时我们也需要知道,这个砍完了,那最小的元素就不是第k个了,要减去砍掉的那些。好。来试一试吧。
不对。有问题。[1,2,5],[3,4,6]处理不了。先考虑选择第3大的元素,k=3。正确结果应该是3。
我们来看第一步,选择的是2和4,那么砍掉6;[1,2,5],[3,4],然后继续砍,4右边没有,砍2的左边,砍掉1,那么现在要找第二大的数
;然后剩下[2,5],[3,4],选择的是5和4,好,又出现5的右边没得砍,那就砍4的左边,砍掉3。注意这里我们把正确答案砍掉了。这是为什么呢?不是说正确答案不会在里面吗?推论是错的?
这个推论是有一个前提的。
我们刚刚砍掉“较大的中位数右边的那一段”所用到的推论,有一个重要的前提,就是:当数组内元素个数为偶数个的时候,我们选择两个中位数中的后一个作为mid。这是为什么我们能够安全的把最大的那一个砍掉。我们来举一个例子:假设两个数组长度分别为2m和2n,那么我们选择的中位数就是m+1和n+1。
这样一来,如果m+1对应的元素的值大于n+1那个,那么我们就有推论,m+1后面m-1个元素要大于前面m+1个元素,同样要大于另一个数组的前n+1个元素。即后m+1个元素前面至少有m+n+2个元素,而总共只有2m+2n个元素,所以我们可以确定,砍最大的那段是合理的。
但是砍最小的那段呢?假设m+1对应的元素的值大于n+1那个,那么我们就有推论,
n+1前面n个元素要小于后面n个元素,同时也要小于从m+1开始的m个元素。因此。前面n个元素后面要有m+n个元素,注意这句话,只能保证有m+n个元素大于这n个元素,但是别忘了,我们是偶数,要找俩元素呢,那你只保证m+n是不够的,这就会把边缘的那一个给删掉了。
那怎么找呢?难不成我们还把中位数改成较小的那个?那不行,那样就没法砍较大的那段了。
正是因为我们引入了“两个数组各自的中位数”这个思路,从而导致了这样一个砍数组出现bug的问题。
为啥我们会有这个思路呢?因为要二分啊,二分数组看上去不是挺好的。
好个球。你看看这出来的一堆bug。
那咋办呢?我们往回回溯一下思路:我们为啥要砍小的那段?因为大的没得砍。那我们让中位数为较小的那个,那大的那段不就一直有的砍?你是不是傻,中位数为较小的那个的话,砍大的那段本身就变成了错误的。所以这条路走不通。
那该咋办呢???
好吧,让我们来看一下正确思路:
将找中位数问题转化为找第k大的数的问题,这个思路是没错的。
看见log,但不要对数组做二分,对k做二分
把k平分成两部分,m和n,要是k是奇数,那就m小n大,那也就差1。然后两个数组,让比较短的那个取前m个,比较长的那个取前n个,要是然后看“取出来的部分谁的更小”,比如第m个小于第n个,那么就能安全砍掉这m个。
欸欸欸,为啥你就这么就砍掉了?我第k个就不能是第m个吗?
傻逼吧你,要是第k个就是第m个,那我们的前提是第m个小于第n个,那第n个至少是第k+1个,但是他妈m+n一共才是k个,你怎么冒出来k+1的。
这么砍是肯定安全的,只要能保证第m个小于第n个,那这m个元素里面肯定没有第k大的,直接砍掉就完事。
那还有一个问题,要是比较短的那个数组比m都小怎么办?那就让m等于比较短的这个数组的长度,比较长的那个取k-短数组长度这么多元素。反正是砍,一次砍没一个数组不是更好。
那啥时候结束呢?
很直观啊,要是有一个数组被砍没了,直接返回另外一个的第k-1个;要是k=1,那就没法分成两份,这时候还分个屁,直接找俩数组头,选小的那个返回。
那要是第m个等于第n个呢?傻啊,那不是直接返回就完事了。
为啥呢?
你得明白为啥把k分成两份然后砍掉一份,为啥是这个思路:因为这样对k二分也是log,而且砍到最后就肯定有结果,那为啥相等时候就直接返回呢?
来来来我问你:你现在拿这俩选出来的子数组,拼在一起,是不是k个元素?是。那这k个元素里最大的那个是不是就是你要找的最后结果?是啊。
是个屁是,我说什么你都说是。
我就随便分别选了m个和n个元素,你咋知道最后结果不是第m+1个,这次没选上来呢?
啊这也对。
那为啥相等就可以直接返回了呢?
本来我们只能知道,我们随手选的这m个和n个元素里各自最大的是哪个(分别记为m_max和n_max),以及这m+n个元素里最大的是哪个(m_max和n_max里最大的那个呗,我们假设n_max更大)。但是我们不排除,在没选的元素里面有大于m_max但小于n_max的,这就是为啥不能直接把n_max当最后结果。
上面这句话,就“没选的元素里面有大于m_max但小于n_max的”,这句咋来的?
我们来捋一捋啊,就m+n等于k对吧,那m_max能保证有m-1个比它小的,n_max能保证有m+n-1个比它小的。那要是有一个比这m+n-1个元素里面最大的大一点,但是又比n_max小一点的,那n_max就不是第k个
我还是拿具体例子来说吧:
俩数组分别是
1,3,5,7,9
2,4,6,8,10
然后要找k=5第5大的。m=2,n=3。
我们选出来1,3和2,4,6。我们不能说6就是我们要的第5大的,我们只能说,存在4个元素比6小,要是有一个元素,比这个四个里最大的那个(4)还大,但是比6要小,那这个就比6更合适,它才是第5大的。很不幸,确实有这个。所以我们说,要是“有一个比这m+n-1个元素里面最大的大一点,但是又比n_max小一点的,那n_max就不是第k个”。那要是n_max和m_max一边大,那就说明,剩下的m+n-1个元素里面最大的,也还是n_max,就不存在那样一个数,比这m+n-1个元素里面最大的大一点,但是又比n_max小一点。所以我们可以直接返回。
TMD这也太复杂了。
综上所述,按照二分k的方法,我们可以相对简单的找到最后的中位数。
已经把找中位数化为找第k个值的问题了,就不要再给自己找麻烦再去找中位数了。不但麻烦,而且还不对。

三、代码

from typing import List

class Solution:
    def find_kth(self, nums1, nums2, k):
        if len(nums2) < len(nums1):
            nums1, nums2 = nums2, nums1
        if len(nums1) == 0:
            return nums2[k - 1]
        if k == 1:
            return min(nums1[0], nums2[0])
        a = min(k // 2, len(nums1))
        b = k - a
        if nums1[a - 1] < nums2[b - 1]:
            return self.find_kth(nums1[a:], nums2, k - a)
        elif nums1[a - 1] > nums2[b - 1]:
            return self.find_kth(nums1, nums2[b:], k - b)
        else:
            return nums1[a - 1]

    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        if (len(nums1) + len(nums2)) % 2 == 0:
            a = self.find_kth(nums1, nums2, (len(nums1) + len(nums2)) // 2)
            b = self.find_kth(nums1, nums2, (len(nums1) + len(nums2)) // 2 + 1)
            print(a, b)
            return (a + b) / 2
        else:
            a = self.find_kth(nums1, nums2, (len(nums1) + len(nums2)) // 2 + 1)
            print(a)
            return a
        
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值