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

150 阅读6分钟

原文首发

题目描述

给定两个大小为 mn 的正序(从小到大)数组 nums1nums2

请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为O(log(m + n))

你可以假设 nums1nums2 不会同时为空。

示例1

nums1 = [1, 3]
nums2 = [2]

则中位数是 2.0

示例 2

nums1 = [1, 2]
nums2 = [3, 4]

则中位数是 (2 + 3)/2 = 2.5

题解

题解一:暴力解法

思路

根据题目描述及示例我们可以知道,用 len 表示合并后数组的长度如果是奇数,我们需要知道第 (len+1)/2 个数就可以了,如果遍历的话需要遍历 Math.floor(len/2 ) + 1 次。如果是偶数,我们需要知道第 len/2len/2+1 个数的值,也是需要遍历 len/2+1 次。所以遍历的话,奇数和偶数都是 len/2+1 次。

即:

  • len%2===1中位数就是位于合并后数组Math.floor(len/2)+1的数字。
  • (m+n)%2===0中位数就是位于合并后数组len/2len/2+1的数字的平均数。

所以我们需要做的就是排序就可以了,这里我们采用指针法排序这样最多只需要移动len/2+1就能获得答案。

时间复杂度:遍历 len/2+1 次,len=m+n,所以时间复杂度是 O(m+n)。 空间复杂度:我们申请了常数个变量,也就是mnlenpointer1pointer2newValoldVal 以及 i

总共 8 个变量,所以空间复杂度是 O(1)

但这并不符合题目中要求的时间复杂度O(log(m + n))

代码

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number}
 */
var findMedianSortedArrays = function (nums1, nums2) {
        let m = nums1.length
        let n = nums2.length
        let len = m + n
        let pointer1 = 0, pointer2 = 0
        let newVal, oldVal
        for (let i = 0; i <= len / 2; i++) {
            oldVal = newVal
            if (pointer1 < m && (nums1[pointer1] <= nums2[pointer2] || pointer2 >= n)) {
                newVal = nums1[pointer1++]
            } else {
                newVal = nums2[pointer2++]
            }
        }
        if ((len % 2) === 0) {
            return (newVal + oldVal) / 2
        } else {
            return newVal
        }
    };

题解二:二分法

思路

该思路源自官方题解加以自己的理解

假设两个有序数组的长度分别为 mn 。提议中寻找中位数即 当 (m+n)%2===0 即为寻找两个数组中 第(m+n)>>1小 与 第((m+n)>>1)+1小 的平均数, 当(m+n)%2===1 时即为寻找两个数组中第((m+n)>>1)+1小的值。

那么本题的关键点在于如何取查找两个有序数组第 k 小的值,k我们可以理解为(m+n)>>1((m+n)>>1)+1

基于题目中的要求复杂度 O(log(m+n)) 所以采用二分法查找。这里二分法的使用相对难以理解,如果理解透彻相信会对二分法有着更深地理解。

在上述的暴力解法中我们通过双指针逐个对比两个数组中数值的大小来查找第k小的值。其实二分法查找的原理与之相类似, 也是通过比较两个数组中数值的大小来查找第k小的值来查找第k 小的值,只不过二分法查找在双指针查找的基础上多做了一层逻辑来简化时间复杂度。

假设两个有序数组分别是 AB,要找到第 k 个元素。双指针查找则从A[0]B[0]开始对比,而二分查找我们直接从A[(k>>1)-1]B[(k>>1)-1]开始对比。 那么这样对比有什么优势?为什么可以这么对比?我们举一个🌰

  • 步骤1:图中 AB 两个数组假设我们需要查找他第4小的数即k=4那么(k>>1)-1===1那么就是对比A[1]B[1]。🌰 中A[1]<B[1] 所以A[0]A[(k>>1)-1]k>>1个数中,不可能包含第k小的数。我们理一下A[0]A[(k>>1)-1]B[0]B[(k>>1)-1]共有k个数。 A[(k>>1)-1]<B[(k>>1)-1]是不是就代表着A[(k>>1)-1]最多只有2*((k>>1)-1)k-2个元素比他小,那A[(k>>1)-1]最多是第k-1 个元素,所以A[0]A[(k>>1)-1]k>>1个数中不可能包含第k小的数。

  • 步骤2:排除了A[(k>>1)-1]<B[(k>>1)-1]k>>1个元素后我们应查找剩余元素中第k-k>>1小的值。为了方便理解我们假定l=k-k>>1l=2 所以我们需要比较A[(k>>1)+(l>>1)-1]B[(l>>1)-1],即A[2]B[0]A[2]>B[0]B[0]B[(l>>1)-1]排除。

  • 步骤3:排除了B[0]~B[(l>>1)-1]l>>1个元素后我们应查找剩余元素中第k-(k>>1)-(l>>1)小的值。为了方便理解我们假定j=k-(k>>1)-(l>>1)j=1 所以我们需要比较A[(k>>1)+(l>>1)+(j>>1)-1]B[(l>>1)+(j>>1)-1],即A[2]B[1]。由于j>>1===0等于1A[2]>B[1]B[1]就是我们需要查找的第5小数。

代码实现

敬请期待... (还没写完回头补上)

划分数组.png

题解三:划分数组 + 二分法

思路

该思路源自官方题解加以自己的理解

为了使用划分的方法解决这个问题,需要理解「中位数的作用是什么」。在统计中,中位数被用来:将一个集合划分为两个长度相等的子集,其中一个子集中的元素总是大于另一个子集中的元素。

如何找到这个元素,我们举一个🌰:

  • 首先根据中位数的概念,在任意位置ij将数组A与数组B划分为两个长度相等的部分。即:(m+n)%2===0时满足i+j=m-i+n-j(m+n)%2===1时满足i+j=m-i+n-j+1。总结成代码就是j+i=(m+n+1)>>1,这里如果不理解可以去看看位运算。

划分数组.png

  • 题中提到数组A与数组B是正序数组。那么必然A[i]>=A[i-1]B[j]>=B[j-1]若要满足其中一个子集中的元素总是大于另一个子集中的元素则必须满足A[i]>=B[j-1]B[j]>=A[i-1]

划分数组2.png

划分数组3.png

  • i0 ∼ m递增时,A[i−1]递增,B[j]递减,所以一定存在一个最大的i满足A[i−1]≤B[j]。那么如果i是最大的那么i+1必定不满足对角<=。即A[i]>B[j-1]刚好满足上面提到的A[i]>=B[j-1](ps:为什么可以这样转换?因为由两个子集总长度奇偶决定必须长度相等或相差一,故一边+1另一边则-1),所以我们只需要找到最大的i满足A[i−1]≤B[j]即可准确划分集合。那么就可以根据(m+n)%2判断中位数:
    • (m+n)%2===1中位数为划分前一部分元素中的最大值。
    • (m+n)%2===0中位数为划分前一部分元素中的最大值与划分后一部分元素中的最小值的平均值。
  • 这里我们可以用二分法快速查找i0 ∼ m区间找到最大的i满足A[i−1]≤B[j],即:满足A[i]>B[j-1]A[i−1]≤B[j]也就找到了中位数。

代码实现

敬请期待... (还没写完回头补上)