寻找两个正序数组的中位数 二分查找

题目描述:

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

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

示例 3:

输入:nums1 = [0,0], nums2 = [0,0]

输出:0.00000

示例 4:

输入:nums1 = [], nums2 = [1]

输出:1.00000

示例 5:

输入:nums1 = [2], nums2 = []

输出:2.00000

进阶:你能设计一个时间复杂度为 O(log (m+n)) 的算法解决此问题吗?

思路分析

       若不考虑进阶要求,大多数人包括我自己第一时间想到的做法应该是将两个列表合并,排序再取中位数。Python可以通过list1+list2的友好方式合并两个链表,但排序不管是直接用sort还是自己写(面试的话最好是自己写而不是调包)复杂度都至少是NlogN。复杂度是高了些,不过这样起码是可以做出来的,就不知道能否满足面试官的要求了。

       要真正满足面试官的要求,自然是要考虑进阶的需求,设计一个复杂度为O(log (m+n)) 的算法啦。实际上我们接触到的各类算法模型中,复杂度为logN的并不多,其中最具代表性的就是:二分查找算法

       再回来看看这一题,是不是可以转换成如下这个问题:从两个列表中,利用二分查找去找到中间的那个数。问题就在于,我们平时用二分查找都只是针对一个数组或者列表,两个列表如何去找呢。

       接下来可能需要一点“领悟”了,先来一个小结论:假设我们要从有序列表list1和有序列表list2中去找第k个数(从小到大),那么我们要分别取list1[k//2]和list2[k//2](假设这两个数都能取到,如果取不到就属于base case,要额外处理,目前我们就先确定好这个常规情况),我们比较list1[k//2]和list2[k//2]的大小:假如前者list1[k//2]小,则目标结果一定在list2中,即list1的前k//2个可以删除、忽略;假如list2[k//2]小,则目标结果就在list1中,那么list2中的前k//2个就可以删除、忽略。

       如果不太能理解上面的小结论的话,可以结合一个例子,比如我们有[1,2,3,4]四个数,我们随机把它分成两个分别包含两个元素的列表。现在我们要找第4个数,显而易见这个数必然是4,你可以用上面的结论尝试看看是否真是如此。比如在列表A[1,3]和列表B[2,4]中找第4个数,比较列表A的第4//2个值和列表b的4//2个值即3和4的大小,列表A的3小,结果一定在列表B中,没毛病。

      我们要找中位数,实际上k就等于得到两个列表的总长度后减半就是了。但这里还有个小trick:在我们找中位数时,往往离不开奇偶个数的问题,奇数情况中位数就是恰好中间的数,偶数情况则需要找到两个值再做平均。这里有一个一步到位的解决办法:得到数组nums的总长度为lenth,分别得到nums[(lenth-1)//2]和nums[lenth//2]的值,对这两个数相加除以2就可以稳稳得到中位数了——原理很简单,对于奇数个数的数组来说,得到的这两个数是相同的,对于偶数个数的数组来说,这两个数就是要求的最中间的两个数。这里默认是下标以0开始,后面代码可能是以1开始计的,会有个加一减一的区别,但只要明白道理就不需纠结这些细枝末节了,自己尝试写写就可以得到想要的表达。

代码:

class Solution(object):
    def findMedianSortedArrays(self, nums1, nums2):
        """
        :type nums1: List[int]
        :type nums2: List[int]
        :rtype: float 
        """
        lenth1 =len(nums1)
        lenth2 = len(nums2)
        first = (lenth1+lenth2+1)//2
        second=(lenth1+lenth2+2)//2 
        #first和second就是按小trick得到的两个数   
        #定义函数findKth,作用就是从num1、起始位置为i和num2、起始位置为j的两个数组中找到第k个数返回
        def findKth(num1,i,num2,j,k):
            lenth1 =len(num1)
            lenth2 = len(num2)
            #定义base case
            if i>=lenth1:#如果i超过了num1的长度,则直接返回num2中相应的数
                return num2[j+k-1]
            if j>=lenth2:#反之同理
                return num1[i+k-1] 
            if k==1:#如果k等于1,则返回较小的那个值,这也是一个base case
                return min(num1[i],num2[j])#写i和j 不是写0
            
            #base case处理完毕 开始正常操作
            k1=0#初始化k1和k2
            k2=0
            if i+k//2-1>=lenth1:#如果从i起始加上k//2的位置超过范围
                k1=float('inf')#则k1置为正无穷,此时必然是k2小,后续也会缩小列表2
            else:
                k1 = num1[i+k//2-1]#如果未超范围,则按部就班的取到数
            if j+k//2-1>=lenth2:#如果列表2超范围了
                k2=float('inf')#则k2置为正无穷,后续k1小,缩小k1的范围
            else:
                k2 = num2[j+k//2-1]
            if k1<k2:
            #现在得到k1 k2了
            #进行比较,谁小谁需要缩小范围,因为答案必然在拥有较大数的那个列表中
                return findKth(num1,i+k//2,num2,j,k-k//2)
            else:#若k2小,则缩小num2范围,起始位置j+k//2
                return findKth(num1,i,num2,j+k//2,k-k//2)
        #最后利用小trick得到两个数再取平均即可
        return (findKth(nums1,0,nums2,0,first)+findKth(nums1,0,nums2,0,second))/2.0

小结:

       这种题第一次做肯定不容易想到最优方法。重点是将问题转换为利用二分查找从两个有序列表中寻找第k个数。解题过程中的一次次缩小范围,起始就是变向的实现二分查找的过程。

      其实用这种递归的方式实现二分查找的过程还是有一点绕的。我自己做这个题其实也是结合一半理解一半记忆来做的,因此刚开始接触感觉有点困难很正常,只能是在下次不会的时候再次复习,熟能生巧而已。

posted @   JunanP  阅读(8)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示