4. 寻找两个正序数组的中位数

题目

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

算法的时间复杂度应该为 O ( l o g ( m + n ) ) O(log (m+n)) O(log(m+n))

示例

  1. 输入: n u m s 1 = [ 1 , 3 ] , n u m s 2 = [ 2 ] nums1 = [1,3], nums2 = [2] nums1=[1,3],nums2=[2]
    输出: 2.00000 2.00000 2.00000
    解释:合并数组 = [ 1 , 2 , 3 ] [1,2,3] [1,2,3] ,中位数 2 2 2

  2. 输入: n u m s 1 = [ 1 , 2 ] , n u m s 2 = [ 3 , 4 ] nums1 = [1,2], nums2 = [3,4] nums1=[1,2],nums2=[3,4]
    输出: 2.50000 2.50000 2.50000
    解释:合并数组 = [ 1 , 2 , 3 , 4 ] [1,2,3,4] [1,2,3,4] ,中位数 ( 2 + 3 ) / 2 = 2.5 (2 + 3) / 2 = 2.5 (2+3)/2=2.5

思路

1 暴力解法

简单地将这两个数组合并,然后重新排序,再找出中间值。

  • 时间复杂度: O ( ( m + n ) l o g ( m + n ) ) O((m+n)log(m+n)) O((m+n)log(m+n))
  • 空间复杂度: O ( m + n ) O(m+n) O(m+n)
  • 缺点:没有使用到题目中数组有序这一条件
class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        # 将nums2添加到nums1的末尾
        nums1.extend(nums2)
        # 对nums1进行排序。python中的sort排序是快速排序,复杂度为O(nlog(n))
        nums1.sort()
        # 计算合并后nums1的长度,判断长度为奇数还是偶数
        n = len(nums1)
        # 如果长度除以2余1为奇数,中位数只有一个,直接返回中间值
        if n % 2 == 1:
            return nums1[n//2]
        # 否则为偶数,中位数有两个,返回最中间两个数的平均数
        return (nums1[n//2]+nums1[n//2-1])/2

2 归并排序

和暴力排序一样的思路,只是在排序的使用归并排序。

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

如果不太了解归并排序的可以查看这篇文章归并排序

		# 两个数组的长度
        n = len(nums1)
        m = len(nums2)
        # p1和p2分别指向两个数组的头部
        p1, p2 = 0, 0
        # 创建一个新的数组用于存储排序之后的数组
        new = []
        # 指针不可以超出数组长度
        while p1 < n or p2 < m:
        # 当指针p1超出数组nums1,将nums2中剩余的数字都添加到新的数组中
            if p1 == n:
                new.append(nums2[p2])
                p2 += 1
                # 指针p2同上
            elif p2 == m:
                new.append(nums1[p1])
                p1 += 1
             # 比较两个数组元素的大小,谁大存谁
            elif nums1[p1] < nums2[p2]:
                new.append(nums1[p1])
                p1 += 1
            else:
                new.append(nums2[p2])
                p2 += 1
		# 奇偶判断
        if (m + n) % 2 == 1:
            return new[(m + n) // 2]
        else:
            return (new[(m + n) // 2] + new[(m + n) // 2 - 1]) / 2

下面是官方的归并排序解法,它将可以融合的条件融合在了一起,并且不需要产生新的数组。

        n = len(nums1)
        m = len(nums2)
        l = m + n
        left, right = -1, -1
        p1, p2 = 0, 0
        for i in range(l // 2 + 1):
        # 如果还能继续执行说明右指针还可以右移,那么让左指针等于右指针
            left = right
            # 当p1小于nums数组长度并且nums1的元素小于nums2的元素,或者nums1还存在但nums2已经遍历完成,让右指针指向偏小的数
            if n < p1 and (p2 >= m or nums1[p1] < nums2[p2]):
                right = nums1[p1]
                p1 += 1
            else:
                right = nums2[p2]
                p2 += 1
        if l % 2 == 1:
            return right
        return (left + right) / 2

3 二分查找

首先先熟悉一下二分查找算法,我们举一个用Python实现二分查找的例子:
在该函数中,我们首先初始化左右两个指针,分别指向数组的首尾元素。然后进入循环,每次找到中间位置 mid,判断目标元素是否等于 arr[mid],如果是则返回 mid,如果不是,则根据目标元素与 arr[mid] 的大小关系,移动左指针或右指针,继续进行二分查找。如果最终没有找到目标元素,则返回-1。
需要注意的是,该函数要求输入的数组必须是已经排序好的。如果输入的数组没有排序,则需要先调用 sort() 函数对数组进行排序。

def binary_search(arr, target):
    """
    在已排序的数组 arr 中查找目标元素 target
    返回目标元素的索引值,如果目标元素不存在则返回-1
    """
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1
  • 中位数:在只有一个有序数组的时候,中位数可以把数组划分为两个部分。

  • 根据定义,按数组长度为奇数和偶数讨论:

    如果数组的长度为偶数的话,中位线将数组划分为两个子数组,左数组和右数组长度相等,它的中位数为左边数组最大值和右边数组最小值的平均值。

    如果数组的长度为奇数的话,中位线将数组划分为俩个不等长度的子数组,可以左边数组比右边数组多一个数,也可以右边数组比左边数组多一个数。

  • 解决过程

  1. 题目中有两个有序数组,并且要求的时间复杂度为 O ( l o g ( m + n ) ) O(log(m+n)) O(log(m+n)),因此想到使用二分查找来做,二分查找不需要合并两个数组。

  2. 根据一个有序数组的情况,在两个有序数组的情况下,用一条分割线也可以将两个数组划分为两个部分。那么中位数一定与在分割线两边的元素(也就是图中这四个元素)有关,那么我们的目标就是确定这个分割线的位置,确定这个位置我们需要使用二分查找算法。
    划分两个数组
    这条分割线应该有以下两个条件:

    • 这两个部分的元素个数应该相等,或者一个部分的个数比另一个部分的个数多一个。
    • 分割线左边的所有数值 < = <= <= 分割线右边的所有数值。
  3. 首先满足第一个条件

    • 如果两个有序数组长度之和为偶数的话,红线左边和右边部分的个数相等,即 m + n 2 \frac{m+n}{2} \quad 2m+n它的中位数为左边部分的最大值和右边部分的最小值的平均数。

    • 如果两个有序数组之和为奇数的话,我们规定红线左边部分的个数比右边部分的个数多一个(当然你也可以反着)。因此左边部分的元素个数为: m + n + 1 2 \frac{m+n+1}{2}\quad 2m+n+1它的中位数为左边部分的最大值。

    • 因为是向下取整,合并这两个公式为 k = m + n + 1 2 k=\frac{m+n+1}{2}\quad k=2m+n+1那么如果从第一个数组左侧取 a a a 个数,那么第二个数组左侧就取 b = k − a b=k-a b=ka 个数。

  4. 接下来满足第二个条件

    • 在同一数组内,因为是都是有序数组,所以一定满足分割线左边的元素小于等于右边的元素。
    • 在不同数组之间,应该保证元素 n u m s 1 [ a ] > = n u m s 2 [ b − 1 ] nums1[a]>=nums2[b-1] nums1[a]>=nums2[b1] n u m s 1 [ a − 1 ] < = n u m s 2 [ b ] nums1[a-1]<=nums2[b] nums1[a1]<=nums2[b] n u m s 1 [ a ] nums1[a] nums1[a] n u m s 2 [ b ] nums2[b] nums2[b] 分别为第一和第二个数组分割线右侧的元素,因此 n u m s 1 [ a ] nums1[a] nums1[a] 之前有 a a a 个数, n u m s 2 [ b ] nums2[b] nums2[b] 之前有 b = k − a b=k-a b=ka 个数,只要存在 − 1 -1 1 则为分割线左边的元素。
  5. 知道这两个条件后,我们通过二分查找来确定分割线 a a a b = k − a b=k-a b=ka 的值

    • 首先在第一个数组 n u m s 1 nums1 nums1 的区间 [ 0 , m ] [0,m] [0,m] 中查找 a a a,让 l e f t left left 指向它的头部位置 0 0 0,让 r i g h t right right 指向它的尾部位置 m m m(这里为什么不是和上面二分查找的举例一样指向 n − 1 n-1 n1,而是指向了 n n n,在文章的最后我们会解释)
    • 每次取最中间的分割线作为 a a a,则 b = k − a b=k-a b=ka,然后判断第二个条件,如果 n u m s 1 [ a ] < n u m s 2 [ b − 1 ] nums1[a]<nums2[b-1] nums1[a]<nums2[b1] 则将分割线 a a a 右移,在 [ a + 1 , r i g h t ] [a+1, right] [a+1,right]这个区间查找元素;反之,我们让最右侧等于分割线 a a a ,t t取它的相反区间 [ l e f t , m ] [left, m] [left,m]
    • 那么通过上述操作,极容易出现数组下标越界的两种情况。其中第一种发生在两个数组长度不一致的时候为:
      较短数组左边右边没有数字
      我们可以看到为了使另一个长度较长的数组的分割线两侧都有元素,我们需要在较短的数组上确定分割线的位置,这样就不会出现访问数组下标越界的情况。
      第二种情况为数组长度相等的时候:
      在这里插入图片描述
      这个在下面代码也有讨论。
  6. 找到分割线后,根据两个数组长度之和的奇偶性来计算对应的中位数。

    • 如果为奇数的话 ,中位数为两个数组分割线左侧的两个数 n u m s 1 [ a − 1 ] nums1[a-1] nums1[a1] n u m s 2 [ b − 1 ] nums2[b-1] nums2[b1]中的最大值。
    • 如果为偶数的话,中位数为分割线右侧部分的最小值(即 n u m s 1 [ a ] nums1[a] nums1[a] n u m s 2 [ b ] nums2[b] nums2[b]二者之一)和左侧部分的最大值(即 n u m s 1 [ a − 1 ] nums1[a-1] nums1[a1] n u m s 2 [ b − 1 ] nums2[b-1] nums2[b1]二者之一)。
class Solution
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        n = len(nums1)
        m = len(nums2)
        # 为了使第二个数组分割线左右两侧都有元素,不会出现访问数组下标越界的情况,保持nums1数组长度较短,nums2长度较长的状态
        if n > m:
            return self.findMedianSortedArrays(nums2, nums1)
        # 合并后左侧应该有的数组长度
        k = (n + m + 1) // 2
        # 在区间[0,n]寻找分割线
        # left指向开头
        left = 0
        # right指向nums1的末尾
        right = n 
        while left < right:
        	# nums1分割线a
            a = (left + right) // 2
            # nums2分割线b
            b = k - a
            # 看到nums2[b-1]这种情况,我们就要考虑下标越不越界这种情况了,因为在取中位线的时候我们是上取整的,因此在[0,n]区间,我们是取不了n的,因此不用担心以上4种图的最后一种情况,在前三种中,b-1都不会造成下标越界。
            if nums1[a] < nums2[b-1]:
                left = a + 1
            else:
                right = a

        a = left
        b = k - a
		# 取左侧最大值 float("-inf")为防止越界的操作
        result1 = max(nums1[a-1] if a > 0 else float("-inf"), nums2[b-1] if b > 0 else float("-inf"))
        if (m + n) % 2 == 1:
            return result1
        # 取右侧最小值
        result2 = min(nums1[a] if a < n else float("inf"), nums2[b] if b < m else float("inf"))
        return (result1 + result2) / 2

解释 r i g h t = n right=n right=n

在查找一个数的时候,我们用 r i g h t = n − 1 right=n-1 right=n1,这里不管是 l e f t left left 还是 r i g h t right right 都是指向列表中的数字,最后返回的也是列表中的数字。
但是在本题中,我们寻找的是分割线的位置,而不是数字。假如第一行数组长度为偶数,用 r i g h t = n right = n right=n第一次遍历,如下图,整个数组的不同位置的分割线有 5 5 5条, 4 4 4个数字平均分为 2 2 2组,前后各两个,说明分割线在 b b b后面。
但是如果我们用 r i g h t = n − 1 right = n-1 right=n1 ( 0 + 3 ) / / 2 = 1 (0+3)//2=1 (0+3)//2=1,如果是查找数字它指向了字母 b b b,但本题是分割数组,说明它将这 4 4 4个数字分成了左侧 1 1 1个数字和右侧 3 3 3个数字两部分,并不是左右侧各两个,那么说明我们并没有实现真正的二分法,它并没有实现二分查找,如果要用这种方法,可能第二次分割线才会右移,变到 b b b的后面,那这样我们就多遍历了一次。
在这里插入图片描述

class Solution:
    def findMedianSortedArrays(self, nums1: list[int], nums2: list[int]) -> float:
        n = len(nums1)
        m = len(nums2)
        if n > m:
            return self.findMedianSortedArrays(nums2, nums1)

        k = (n + m + 1) // 2
        left = 0
        right = n - 1
        # 和上面代码不一样的地方:left<=right 
        while left <= right:
            a = (left + right ) // 2
            b = k - a
            if nums1[a] < nums2[b - 1]:
                left = a + 1
            else:
            # 和上面一样都要-1,right会左移,因此可能会出现left=right的情况
                right = a - 1

        a = left
        b = k - a

        result1 = max(nums1[a - 1] if a > 0 else float("-inf"), nums2[b - 1] if b > 0 else float("-inf"))
        if (m + n) % 2 == 1:
            return result1
        result2 = min(nums1[a] if a < n else float("inf"), nums2[b] if b < m else float("inf"))
        return (result1 + result2) / 2

最后,以上这种会多出遍历次数,复杂度升高,但是我们可以调整分割线的计算方式,使 a = ( l e f t + r i g h t ) / / 2 a = (left + right ) // 2 a=(left+right)//2 变为 a = ( l e f t + r i g h t + 1 ) / / 2 a = (left + right + 1) // 2 a=(left+right+1)//2,其实就是把它变为了向上取整,这样分割线还是居中的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值