leetcode4. 寻找两个正序数组的中位数python_二分查找和递归(困难)

题目

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。算法的时间复杂度应该为 O(log (m+n)) 。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2

示例 2:
输入: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

思路和代码

看到这题,先拼接,再sort,根据长度的奇偶返回中间数或中间两个数的平均,一提交,通过。啊,这题也不难啊。

不对,sort的时间复杂度是O(nlogn),不满足题目要求。

题解太难,看了好多天(真的很难看下去啊),有点理解,但不多。有两种方法,一种是二分查找,时间复杂度是O(log (min(m,n)))(哎呀,这个太优秀了,题目要求是O(log (m+n)) ) 。另一种方法就是递归,时间复杂度刚好是题目要求的O(log (m+n)) 。

先看太优秀的二分查找在这题怎么用。

1.二分查找

要来一条线,把num1和nums2分成左边一部分和右边一部分。
在这里插入图片描述
我这个图画的真的太丑也太形象了。

这条线需要满足什么条件?

1.线左边的元素个数和右边的元素个数差不多,如果两个数组总长度是偶数就两边各半,如果是奇数,左边就多分一个。

2.左边元素的值都小于等于右边元素的值。更具体的,因为对于各自数组来说,左边的必定小于右边的,所以这里需要满足交叉小于等于条件,即nums1[i-1] <= nums2[j] and nums2[j-1] <=nums1[i]。这里的i表示nums1数组位置i前面有i个元素,同理j表示nums2数组位置j前面有j个元素。

怎么利用二分查找去找到这条线?

不必在两个数组都找这条分割线,因为左边的元素个数是知道的,当确定第一个数组的分割线位置,推导一下就可以得到第二个数组的分割线位置。

更具体的,设totalleft为左边分得的元素个数。如果m+n是奇数,左边分的(m+n+1)//2个元素,如果m+n是偶数,左边有(m+n)//2个元素,又因为//是向下取整,当m+n是偶数,(m+n)//2 和 (m+n+1)//2是相等的。故不论奇偶,都可以用totalleft = (m+n+1)//2来表示。

当数组nums1的分割线是i时,分割线左边有i个元素。nums1分割线左边的值是nums1[i-1],右边的值就是nums1[i]。而此时,数组nums2的分割线j = totalleft - i。nums2分割线左边的值是nums2[j-1],右边的值就是nums2[j]。

接下来,我们在较短的数组nums1(开始不是nums1的话,nums1和nums2交换一下)利用二分查找找到合适的分割线。注意,查找范围是[0,m] (左闭右闭,因为分割线可能在num1的最右边)。所以初始时left=0,right=m(而不是right=m-1)

我们要找到满足条件nums1[i-1] <= nums2[j] and nums2[j-1] <=nums1[i]的分割线,对该条件取反,只要其中一个一条不满足即可。

i是中间的数,即i=left+(right-left)//2。我们判断nums1[i-1] > nums2[j],说明线画的靠右了,要往左边找,故right = i-1,否则left=i。

注意,当数组只有两个数的时候[left,right],i=left陷入死循环,所以开始求中间位置的时候写成i=left+(right-left+1)//2 。(高,实在是高,我自己肯定想不出来啊,其实懂都没有太懂,反正这样写就可以避免死循环。)

最后跳出循环,更新一下分割线,i=left,j=totalleft-i 。

这就结束了么?并不是!还有一些特殊情况需要处理。例如下图:
在这里插入图片描述
i和j如果为边界值就会报错,所以当 i=0 的时候,分割线在nums1的最左边。左边求最大值,将nums1的分割线左边值设置为负无穷,同理,j=0时,将nums2的分割线左边值设置为负无穷。而当i = m的时候,分割线在nums1的最右边,右边是求最小值,将nums1分割线右边的值设置为正无穷,同理,j=n的时候,将nums2分割线右边的值设置为正无穷。

最后总长度是奇数返回左边最大值,是偶数返回左边最大值和右边最小值的平均值。

class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        if len(nums1) > len(nums2):
            nums1, nums2 = nums2, nums1
        m, n = len(nums1), len(nums2)
        left, right = 0, m
        totalleft = (m + n + 1) // 2
        while left < right:
            i = left + (right - left + 1)//2
            j = totalleft - i
            if nums1[i-1] > nums2[j]:
                right = i-1
            else:
                left = i
        i = left
        j = totalleft - i
        left1 = float("-inf") if i == 0 else nums1[i-1]
        right1 = float("inf") if i == m else nums1[i]
        left2 = float("-inf") if j == 0 else nums2[j-1]
        right2 = float("inf") if j == n else nums2[j]
        return max(left1,left2) if (m+n)%2 else (max(left1,left2)+min(right1,right2))/2

二分查找的时间复杂度是O(log(min(m,n)))。

2.递归

在两个数组中找第k小的数,比较nums1[k//2]和nums2[k//2],如果nums1[k//2] > nums2[k//2],说明nums2的前k//2中的数都不可能是答案,直接舍去。然后不断舍去一些肯定不是答案的值,直到得到最终结果。大概的思路就是这样。

再说一些细节。

递归三要素,参数,终止条件和单层递归逻辑。

首先参数是两个数组,各自的起始位置,和要求的第k个小的数。
findmin(self,nums1,start1,end1, nums2, start2, end2, k)。

终止条件:

其中两个数组的长度len1,len2分别是end1-start1+1,end2-start2+1。

1.当较短的数组长度为0时,直接返回较长数组的第k小的数。
2.当k为1的时候,取两个数组首位元素的较小值。

单层递归逻辑,为了防止越界,nums1的第k小的数位置为i = start1 + min(len1,k//2)-1,j = start2 +min(len2,k//2)-1

当nums1[i] > nums2[j],舍去nums2的前部分,nums2的起始位置变成了j+1,要求得的第k小的数,变成了求第k-(j-start2)-1小的数。否则,舍去nums1前面的部分,nums1的起始位置变成了i+1,要求得的第k小的数,变成了求第k-(i-start2)-1小的数。

代码:

class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        m, n = len(nums1), len(nums2)
        left = (m+n+1)//2
        right = (m+n+2)//2
        return (self.findkmin(nums1,0,m-1,nums2,0,n-1,left) + self.findkmin(nums1,0,m-1,nums2,0,n-1,right))/2

    def findkmin(self,nums1,start1,end1,nums2,start2,end2,k):
        len1, len2 = end1 - start1 + 1, end2 - start2 + 1
        if len1 > len2:
            return self.findkmin(nums2,start2,end2,nums1,start1,end1,k)
        if len1 == 0:
            return nums2[start2 + k-1]
        if k == 1:
            return min(nums1[start1],nums2[start2])
        i = start1 + min(k//2,len1) -1
        j = start2 + min(k//2,len2) -1
        if nums1[i] > nums2[j]:
            return self.findkmin(nums1,start1,end1,nums2,j+1,end2,k-(j-start2)-1)
        else:
            return self.findkmin(nums1,i+1,end1,nums2,start2,end2,k-(i-start1)-1)

时间复杂度是O(log(m+n))。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值