寻找两个有序数组中的中位数

遇到一个比较复杂的算法题,记录一下,内容如下。


1.问题

假设有两个有序数组nums1和nums2,它们的长度分别为n和m。请找出这两个有序数组组成的序列中的中位数,并且整体的时间复杂度不大于log(m+n)。你可以假设这两个数组都不为空。

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

The median is 2.0
复制代码

2.分析

解决这个问题可以采用递归的方法,而寻找中位数的问题,可以理解为:将一个集合划分为两个等长的子集,其中一个子集的任意元素值永远小于等于另一个子集的任意元素值。
首先,我们可以通过随机位置i将数组A划分为两部分(可以将i理解为数组元素之间的间隔位置,间隔i左边有i个元素,右边有m-i个元素):

      left_A             |        right_A
A[0], A[1], ..., A[i-1]  |  A[i], A[i+1], ..., A[m-1]
复制代码

因为A有m个元素,因此共有m+1种切分方式。我们可以得出:len(left_A)=i,len(right_A)=m-i。当i=0时(第一个位置为0),left_A为空,当i=m时,right_A为空。(注:len(left_A)代表left_A部分的元素个数,其他的同理)
同理,我们也可以通过随机位置j将数组B划分为两部分:

      left_B             |        right_B
B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]
复制代码

因此,我们可以将left_A和left_B合并为一个集合,将right_A和right_B合并为一个集合,分别命名为left_part和right_part:

      left_part          |        right_part
A[0], A[1], ..., A[i-1]  |  A[i], A[i+1], ..., A[m-1]
B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]
复制代码

如果我们能够确保(注:max(left_part)代表left_part集合的最大值,min(rigth_part)代表right_part集合的最小值):

1.len(left_part)=len(right_part);
2.max(left_part)<=min(rigth_part)
复制代码

那么我们就将{A,B}集合中的元素划分为具有相同长度的两个子集了,其中一个子集的元素永远大于等于另外一个子集的元素。那么 median(中位数)=(max(left_part)+min(right_part))/2。(以上的讨论基于m,n都是偶数,但不失一般性)

因此为了保证以上的两个条件成立,我们可以保证(当m+n为奇数时,左边部分会比右边部分多一个元素):

在这里,我们假设:
1.为了方便讨论,我们假设当i=0, i=m,j=0, j=n 时,A[i−1], B[j−1], A[i], B[j]的值都存在。我们在最后再讨论这些边界值的情况。
2.假设n≥m,因为我们必须确保j不为负数,当0≤i≤m,j=(m+n+1)/2-i。

因此我们需要做的就是,遍历i(i属于[0,m]),找到一个目标i使得:


我们可以通过以下的步骤进行二分查找:
1.令imin=0,imax=m,从[imin,imax]开始查找。
2.令i=(imin+imax)/2, j=(m+n+1)/2-i。
3.现在我们已经使得len(left_part)=len(right_part)。我们可能会遇到两种情况:

  • B[j−1]≤A[i] 和A[i−1]≤B[j]这意味着我们找到了目标i因此查询结束。
  • B[j−1]>A[i],这意味着A[i]太小了,因此我们需要增大i,使得B[j−1]≤A[i]。因为i增大了j就会减小,A[i]就会增大,B[j−1]就会减小。因此,我们将查询范围调整为[i+1,imax],返回第2步。
  • A[i−1]>B[j]这意味着A[i−1]太大了,因此我们需要减小i,所以我们将查询范围调整为[imin,i-1],返回第2步。

当我们获得目标i时,我们可以得到中位数:
1.当m+n时奇数时,中位数为max(A[i−1],B[j−1])。
2.当m+n时偶数时,中位数为(max(A[i−1],B[j−1])+min(A[i],B[j]))/2

最后,我们来考虑一下边缘值。当i=0, i=m, j=0, j=n时,A[i−1], B[j−1], A[i], B[j]不存在。当i,j不是边缘值时,我们需要判断B[j−1]≤A[i] 和A[i−1]≤B[j]这两个条件,当i, j是边缘值时,这两个条件就不需要都进行判断了,比如,当i=0时,A[i-1]不存在,因此我们就不需要判断A[i−1]≤B[j]这个条件了。所以我们需要做的是,遍历i(i属于[0,m]),找到目标值i使得:

比如j=0 则 A[i−1]≤B[j]必然成立;i=m 则 A[i−1]≤B[j] 必然成立。

因此,总得来说,当我们在循环时,只会碰到三种情况:
第一种情况:找到目标 i,当:

第二种情况:B[j−1]>A[i],这意味着A[i]太小了,因此我们需要增大i;
第三种情况:A[i−1]>B[j]这意味着A[i−1]太大了,因此我们需要减小i。

3.具体代码

class Solution {
    public double findMedianSortedArrays(int[] A, int[] B) {
        int m = A.length;
        int n = B.length;
        if (m > n) { // to ensure m<=n
            int[] temp = A; A = B; B = temp;
            int tmp = m; m = n; n = tmp;
        }
        int iMin = 0, iMax = m, halfLen = (m + n + 1) / 2;//i
        while (iMin <= iMax) {//每次循环长度都变为原来的1/2
            int i = (iMin + iMax) / 2;
            int j = halfLen - i;
            //调整i
            if (i < iMax && B[j-1] > A[i]){
                iMin = i + 1; // i is too small
            }
            else if (i > iMin && A[i-1] > B[j]) {
                iMax = i - 1; // i is too big
            }
            else { // i is perfect
                int maxLeft = 0;//得到左边部分的最大值
                if (i == 0) { maxLeft = B[j-1]; }
                else if (j == 0) { maxLeft = A[i-1]; }
                else { maxLeft = Math.max(A[i-1], B[j-1]); }
                //当两个数组总长度为奇数时,只需返回左边的最大值即可
                if ( (m + n) % 2 == 1 ) { return maxLeft; }

                int minRight = 0;//得到右边部分的最小值
                if (i == m) { minRight = B[j]; }
                else if (j == n) { minRight = A[i]; }
                else { minRight = Math.min(B[j], A[i]); }
                 //当两个数组总长度为偶数时,只需返回左右两边最大值的平均值
                return (maxLeft + minRight) / 2.0;
            }
        }
        return 0.0;
    }
}
复制代码

4.复杂度分析

1.时间复杂度:O(log(min(m,n)))。在最开始查找范围是[0,m],每次查询时查找的范围都会减少为原来的一半,因此我们需要log(m)次循环,而每次循环的时间都是常数,因此时间复杂度为:O(log(m))。因为m≤n,所以时间复杂度为O(log(min(m,n)))。
2.空间复杂度:O(1)。我们只需要固定的内存去存储9个本地变量,因此时间复杂度为O(1)。

5.其他:

我们可以将这两个有序数组进行归并,找到中位数,但这种方法的时间复杂度为O(n+m)。

题目描述: 给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。 请找出这两个有序数组中位数,并且要求算法的间复杂度为 O(log(m+n))。 你可以假设 nums1 和 nums2 不会同为空。 示例 1: nums1 = [1, 3] nums2 = [2] 则中位数是 2.0 示例 2: nums1 = [1, 2] nums2 = [3, 4] 则中位数是 (2 + 3)/2 = 2.5 解题思路: 题目要求间复杂度为 O(log(m+n)),很明显是要用到二分查找的思想。 首先,我们需要确定中位数的位置。对于两个长度分别为 m 和 n 的有序数组,它们的中位数位置为 (m+n+1)/2 和 (m+n+2)/2,因为当 m+n 为奇数,这两个位置的值是相同的;当 m+n 为偶数,这两个位置的值分别为两个数。 然后,我们需要在两个数组分别找到第 k/2 个数(k 为中位数位置),比较它们的大小,如果 nums1[k/2-1] < nums2[k/2-1],则说明中位数位于 nums1 的右半部分和 nums2 的左半部分之间,此可以舍弃 nums1 的左半部分,将 k 减去 nums1 的左半部分的长度,继续在 nums1 的右半部分和 nums2 的左半部分寻找第 k/2 个数;反之,如果 nums1[k/2-1] >= nums2[k/2-1],则说明中位数位于 nums1 的左半部分和 nums2 的右半部分之间,此可以舍弃 nums2 的左半部分,将 k 减去 nums2 的左半部分的长度,继续在 nums1 的左半部分和 nums2 的右半部分寻找第 k/2 个数。 当 k=1 中位数两个数组的最小值。 Java代码实现: class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int m = nums1.length; int n = nums2.length; int k = (m + n + 1) / 2; double median = findKth(nums1, 0, m - 1, nums2, 0, n - 1, k); if ((m + n) % 2 == 0) { int k2 = k + 1; double median2 = findKth(nums1, 0, m - 1, nums2, 0, n - 1, k2); median = (median + median2) / 2; } return median; } private double findKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) { int len1 = end1 - start1 + 1; int len2 = end2 - start2 + 1; if (len1 > len2) { return findKth(nums2, start2, end2, nums1, start1, end1, k); } if (len1 == 0) { return nums2[start2 + k - 1]; } if (k == 1) { return Math.min(nums1[start1], nums2[start2]); } int i = start1 + Math.min(len1, k / 2) - 1; int j = start2 + Math.min(len2, k / 2) - 1; if (nums1[i] > nums2[j]) { return findKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1)); } else { return findKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1)); } } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值