寻找两个正序数组的中位数 二分查找
题目描述:
给定两个大小分别为 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个数。解题过程中的一次次缩小范围,起始就是变向的实现二分查找的过程。
其实用这种递归的方式实现二分查找的过程还是有一点绕的。我自己做这个题其实也是结合一半理解一半记忆来做的,因此刚开始接触感觉有点困难很正常,只能是在下次不会的时候再次复习,熟能生巧而已。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了