LeetCode_4_困难_寻找两个正序数组的中位数

文章目录

  • 1. 题目
  • 2. 思路及代码实现(Python)
    • 2.1 二分查找
    • 2.2 划分数组


1. 题目

给定两个大小分别为 m m m n n n 的正序(从小到大)数组 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


提示

  • n u m s 1. l e n g t h = = m nums1.length == m nums1.length==m
  • n u m s 2. l e n g t h = = n nums2.length == n nums2.length==n
  • 0 < = m < = 1000 0 <= m <= 1000 0<=m<=1000
  • 0 < = n < = 1000 0 <= n <= 1000 0<=n<=1000
  • 1 < = m + n < = 2000 1 <= m + n <= 2000 1<=m+n<=2000
  • − 1 0 6 < = n u m s 1 [ i ] , n u m s 2 [ i ] < = 1 0 6 -10^6 <= nums1[i], nums2[i] <= 10^6 106<=nums1[i],nums2[i]<=106

2. 思路及代码实现(Python)

2.1 二分查找

对于查找两个数组的中位数,我们很自然想到中位数的寻找方式,就是将数组由小到大排序,然后根据数组长度,计算出中位数的索引位置,找到相应位置的数即可。现在的问题是,对于拆成两部分的数组,我们如何找到合并后的数组的某个索引位置的值呢?

一个简单的方式是直接把两个数组按顺序合并成一个大数组进行索引,这个过程的时间复杂度为 O ( m + n ) O(m+n) O(m+n),空间复杂度也为 O ( m + n ) O(m+n) O(m+n);由于题目给出的两个数组本身就是有序的,因此还有另一个直观的方法是,对两个数组分别各维护一个指针,共同移动指针的过程判断是否到达中位数的索引位置,该方法的时间复杂度为 O ( m + n ) O(m+n) O(m+n),空间复杂度为 O ( 1 ) O(1) O(1)。然而,题目要求的是让时间复杂度降为 O ( l o g ( m + n ) ) O(log(m+n)) O(log(m+n)),这相当于就是明示,需要用到二分查找之类的方法。

基于上述的逻辑,我们其实根据索引值就能找到中位数的值,因此二分查找用在如何加快搜索该索引值下的值上,已知有两个数组,长度分别为 m , n m,n m,n,若 m + n m+n m+n 是奇数,则中位数的索引为 ( m + n ) / / 2 (m+n)//2 (m+n)//2,若为偶数,则中位数取索引为 ( m + n ) / / 2 , ( m + n ) / / 2 + 1 (m+n)//2, (m+n)//2+1 (m+n)//2,(m+n)//2+1的两个数的平均值。那如何加速搜索第 k ( k = ( m + n ) / / 2 或 k = ( m + n ) / / 2 + 1 ) k(k=(m+n)//2 或 k=(m+n)//2+1) kk=(m+n)//2k=(m+n)//2+1 个值呢?就需要利用到两个数组也都是有序的这个特点。当我们在已知两数组长度后,计算得到 k k k 值,然后想办法将 k k k 拆开,用以每次比较能排除 k / 2 k/2 k/2 的值,因此,我们需要每次比较两个数组的索引为 k / 2 − 1 k/2-1 k/21 的值,这样,当比较小的值的那方涉及到的 k / 2 k/2 k/2 个值可以被一次排除。存在特殊边界条件,当 k = 1 k=1 k=1,说明要查找的是两个数组中最小的数,取两个数组首元素进行比较即可。

class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        def getKthElement(k):
            index1, index2 = 0, 0
            while True:
                # 特殊情况
                if index1 == m:
                    return nums2[index2 + k - 1]
                if index2 == n:
                    return nums1[index1 + k - 1]
                if k == 1:
                    return min(nums1[index1], nums2[index2])

                # 正常情况
                newIndex1 = min(index1 + k // 2 - 1, m - 1)
                newIndex2 = min(index2 + k // 2 - 1, n - 1)
                pivot1, pivot2 = nums1[newIndex1], nums2[newIndex2]
                if pivot1 <= pivot2:
                    k -= newIndex1 - index1 + 1
                    index1 = newIndex1 + 1
                else:
                    k -= newIndex2 - index2 + 1
                    index2 = newIndex2 + 1
        
        m, n = len(nums1), len(nums2)
        totalLength = m + n
        if totalLength % 2 == 1:
            return getKthElement((totalLength + 1) // 2)
        else:
            return (getKthElement(totalLength // 2) + getKthElement(totalLength // 2 + 1)) / 2

执行用时:44 ms
消耗内存:17.11 MB

参考来源:力扣官方题解

2.2 划分数组

上一个方法通过两个数组的长度计算出中位数的索引,通过维护两个指针来不断更新中位数的索引最终找到中位数。另一个方法是划分数组,我们知道,当 m + n m+n m+n 为偶数时,可以将两个数组的数值划分为长度相等的两部分,其中,数值较小部分的最大值,不大于数值较大部分的最小值;同理,若 m + n m+n m+n 长度为奇数,则较小部分的长度比较大部分长度多1(哪边多1都一样),中位数就是较小部分数组中的最大值。

举个例子: [ 2 , 4 , 5 , 7 , 9 , 11 ] [2,4,5,7,9,11] [2,4,5,7,9,11],在这个数组中,长度为偶数,可以分为 [ 2 , 4 , 5 ] , [ 7 , 9 , 11 ] [2,4,5], [7,9,11] [2,4,5],[7,9,11] 两个数组,而左边数组的最大值,不大于右边数组的最小值。因此,现在题目中有两个数组,假设为 A , B A,B A,B,长度分别为 m , n m,n m,n,则可以通过 0 ≤ i ≤ m 0\leq i\leq m 0im A A A分为两部分 A [ 0 ] , A [ 1 ] , . . . A [ i − 1 ] 和 A [ i ] , . . . , A [ m − 1 ] A[0],A[1],...A[i-1]和A[i],...,A[m-1] A[0],A[1],...A[i1]A[i],...,A[m1],同理,通过 j j j 也可以将 B B B分成两部分 B [ 0 ] , B [ 1 ] , . . . B [ j − 1 ] 和 B [ j ] , . . . , B [ n − 1 ] B[0],B[1],...B[j-1]和B[j],...,B[n-1] B[0],B[1],...B[j1]B[j],...,B[n1],其中,若 m + n m+n m+n 为偶数,则 i + j = ( m + n ) / 2 i+j=(m+n)/2 i+j=(m+n)/2,若为奇数,则 i + j = ( m + n + 1 ) / 2 i+j=(m+n+1)/2 i+j=(m+n+1)/2。因此在已知两个数组长度时,确定了 i i i 的值,即可确定 j j j 的值。(这里有个小坑,就是我们以 i i i 去计算 j j j 的值时,需保证 ( m + n ) / 2 或者 ( m + n + 1 ) / 2 减 i (m+n)/2或者(m+n+1)/2减i (m+n)/2或者(m+n+1)/2i的值要大于等于0,因此需要保证 m ≤ n m\leq n mn,如果 n ≤ m n\leq m nm,则调换一下位置即可)。

关键的地方来了,当我们遍历 A A A 的值时, i i i 递增, j j j 会递减,我们总能找到一个使得 A [ i − 1 ] ≤ B [ j ] A[i-1]\leq B[j] A[i1]B[j]的最大的 i i i,此时,由于 i i i 是临界值,所以 A [ i ] > B [ j ] A[i] \gt B[j] A[i]>B[j],且 B [ j ] ≥ B [ j − 1 ] B[j] \geq B[j-1] B[j]B[j1],所以我们只需搜索 m m m,找到满足 A [ i − 1 ] ≤ B [ j ] A[i-1]\leq B[j] A[i1]B[j] 的最大 i i i 即可,这个搜索过程可以用二分搜索进一步加速。

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

        infinty = 2**40
        m, n = len(nums1), len(nums2)
        left, right = 0, m
        # median1:前一部分的最大值
        # median2:后一部分的最小值
        median1, median2 = 0, 0

        while left <= right:
            # 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
            # // 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
            i = (left + right) // 2
            j = (m + n + 1) // 2 - i

            # nums_im1, nums_i, nums_jm1, nums_j 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
            nums_im1 = (-infinty if i == 0 else nums1[i - 1])
            nums_i = (infinty if i == m else nums1[i])
            nums_jm1 = (-infinty if j == 0 else nums2[j - 1])
            nums_j = (infinty if j == n else nums2[j])

            if nums_im1 <= nums_j:
                median1, median2 = max(nums_im1, nums_jm1), min(nums_i, nums_j)
                left = i + 1
            else:
                right = i - 1

        return (median1 + median2) / 2 if (m + n) % 2 == 0 else median1

执行用时:40 ms
消耗内存:17.14 MB

你可能感兴趣的:(LeetCode进阶之路,leetcode,算法)