【二分查找】——两个正序数组的中位数

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

一、问题描述

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。要求找出并返回这两个正序数组的中位数。算法的时间复杂度应该为 O ( l o g ( m + n ) ) O(log (m+n)) 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 (2 + 3) / 2 = 2.5 (2+3)/2=2.5

三、提示信息

  1. nums1.length == m:表示数组 nums1 的长度为 m
  2. nums2.length == n:表示数组 nums2 的长度为 n
  3. 0 <= m <= 1000:数组 nums1 的长度范围。
  4. 0 <= n <= 1000:数组 nums2 的长度范围。
  5. 1 <= m + n <= 2000:两个数组长度之和的范围。
  6. -106 <= nums1[i], nums2[i] <= 106:数组中元素的取值范围。

四、解题思路

  1. 为了满足时间复杂度为 O ( l o g ( m + n ) ) O(log (m+n)) O(log(m+n)),可以使用二分查找的思想。
  2. 假设两个数组的总长度为 len,如果len是奇数,则中位数是第 k 个数(如果 len 是偶数,中位数是第 k 和第 k + 1 数的平均值)。
  3. 以上都是基于假设将两个数组按照正序结合,实际上如果真的结合,这一步的时间复杂度就已经超了。怎么不用真的结合来达成目的?
  4. 通过在两个数组中分别进行二分查找,确定一个划分位置,使得左边的元素个数之和为 k,并且左边的所有元素都小于等于右边的最小元素。
  5. 根据划分位置判断中位数在哪个部分,并调整二分查找的范围,直到找到中位数。

以上思路的123都非常清楚,但是具体执行是45,如何实现这个思路?

  • 首先确定,我们的整体思路是不断收缩两个数组
    的范围来找到第k小的数,从而确定中位数(这个根据判断len奇偶很好实现)

下面我们来实现这个逻辑,判断总长度是奇是偶,判断要返回k是第 (total_length // 2 + 1) 小的数,还是(self.findKthSmallest(nums1, nums2, total_length // 2) + self.findKthSmallest(nums1, nums2, total_length // 2 + 1)) / 2

class Solution:
    # 处理奇偶数判断的部分,找第`K`小的数的部分交给下一个函数逻辑
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        # 获取两个数组的长度
        m = len(nums1)
        n = len(nums2)
        total_length = m + n
        # 如果总长度是奇数,中位数就是第 (total_length // 2 + 1) 小的数
        if total_length % 2 == 1: # 奇数
            return self.findKthSmallest(nums1, nums2, total_length // 2 + 1)
        # 如果总长度是偶数,中位数是第 total_length // 2 和第 total_length // 2 + 1 小的数的平均值
        else:  # 偶数
            return (self.findKthSmallest(nums1, nums2, total_length // 2) + self.findKthSmallest(nums1, nums2, total_length // 2 + 1)) / 2

下面来面临最难的一部分,实现findKthSmallest逻辑:

findKthSmallest函数的逻辑是在两个有序数组中找到第k小的数。

具体步骤如下:

  1. 边界情况处理

    • 如果nums1为空数组,那么直接返回nums2中第k - 1个元素,因为此时只需要在nums2中找第k小的数。
    • 同理,如果nums2为空数组,返回nums1中第k - 1个元素。
    • 如果k为 1,意味着要找最小的数,比较nums1nums2的第一个元素,返回较小的那个。
  2. 确定比较位置

    • 分别取两个数组中第k//2位置的元素,但要确保不超出数组的长度。如果超出了数组的长度,就取该数组的最后一个元素。这里取k//2位置的元素是为了将问题规模缩小一半。
  3. 比较并缩小范围

    • 如果nums1中第k//2位置的元素小于nums2中第k//2位置的元素,说明nums1的前k//2个元素都不可能是第k小的数,可以把问题转化为在nums1[k//2:]nums2中找第k - k//2小的数。
    • 反之,如果nums2中第k//2位置的元素小于等于nums1中第k//2位置的元素,说明nums2的前k//2个元素都不可能是第k小的数,可以把问题转化为在nums1nums2[k//2:]中找第k - k//2小的数。

通过不断地缩小范围,重复这个过程,直到找到第k小的数或者满足边界条件。这种方法利用了两个数组都是有序的性质,每次都能将问题规模减半,从而实现了时间复杂度为 O ( l o g ( m + n ) ) O(log(m + n)) O(log(m+n)),其中mn分别是两个数组的长度。

    # k是根据上一个函数,根据总长度奇偶来传入的,(奇数)第 k = len // 2 + 1
    def findKthSmallest(self, nums1: List[int], nums2: List[int], k: int) -> int:
        # 如果第一个数组为空,直接返回第二个数组中第 k 小的数
        if not nums1:
            return nums2[k - 1]
        # 如果第二个数组为空,直接返回第一个数组中第 k 小的数
        if not nums2:
            return nums1[k - 1]
        # 如果 k 为 1,比较两个数组的第一个元素,返回较小的那个
        if k == 1:
            return min(nums1[0], nums2[0])

一个通俗解释

我们可以把这个问题想象成一个游戏。

假设有两个队伍,队伍 1 是 nums1,队伍 2 是 nums2。我们的目标是从这两个队伍中找出第 k 个最厉害的人(也就是第 k 小的数)。

一、边界情况

  1. 如果队伍 1 没人了(nums1为空数组),那我们就直接从队伍 2 里找第 k 个厉害的人,也就是返回 nums2[k - 1]
    • 就好比队伍 1 弃权了,那我们只需要在队伍 2 中继续比赛,找第 k 个选手就行。
  2. 同理,如果队伍 2 没人了,就从队伍 1 里找第 k 个厉害的人,返回 nums1[k - 1]
  3. 如果我们要找的是最厉害的那个人(k = 1),那就比较两个队伍的第一个人,谁更厉害就选谁,也就是返回 min(nums1[0], nums2[0])
    • 因为是找最厉害的,所以只需要比较两个队伍的第一个人就行。

二、一般情况

我们想个办法来缩小搜索范围。想象我们把两个队伍分别分成两部分,然后猜测第 k 个厉害的人可能在哪个部分。

  1. 我们分别从两个队伍中挑出大约 k//2 个人(如果队伍人数不够 k//2,就有多少算多少)。
    • 比如队伍 1 有 10 个人,队伍 2 有 8 个人,我们要找第 7 个厉害的人,那么就从队伍 1 中挑出前 3 个人(因为 7//2 = 3),从队伍 2 中挑出前 3 个人。
  2. 然后比较这两部分人里最厉害的人。
    • 假设队伍 1 挑出的这部分人中最厉害的人不如队伍 2 挑出的这部分人中最厉害的人。
  3. 那就说明队伍 1 中我们挑出的这部分人以及更不厉害的人都不可能是第 k 个厉害的人,所以我们可以把队伍 1 的这部分人排除掉,只在队伍 1 的剩下部分和队伍 2 中继续找第 k - k//2 个厉害的人。
    • 比如我们要找第 7 个厉害的人,现在排除了队伍 1 的前 3 个人,那就变成在队伍 1 的后 7 个人和队伍 2 的 8 个人中找第 7 - 3 = 4 个厉害的人。

不断重复这个过程,直到找到第 k 个厉害的人。

而找中位数的时候,先判断总长度的奇偶性确定要找第几个数,然后调用找第 k 小的数的方法就可以了。如果总长度是奇数,就找中间那个数;如果是偶数,就找中间两个数求平均。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值