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

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


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个元素):bash

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划分为两部分:ui

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:spa

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集合的最小值):code

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都是偶数,但不失通常性)cdn

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

在这里,咱们假设:
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)。

相关文章
相关标签/搜索