LeetCode | Median of Two Sorted Arrays(两个数组的中位数)


题目:

There are two sorted arrays A and B of size m and n respectively. Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).


题目解析:

这道题是让找出两个有序数组A和B中,处于两个中间值的数字。但要求时间复杂度为O(log (m+n))。


方法一:

很常规的做法是:将A和B归并成一个数组,然后找出C[(n+m)/2]的值就行了,时间复杂度为O(n+m)。(对一个无序数组归并排序O(nlogn))

但明显不符合题意,这就迫使我们想其他办法。


方法二:

我们可以发现,现在我们是不需要“排序”这么复杂的操作的,因为我们仅仅需要第k大的元素。我们可以用一个计数器,记录当前已经找到第m大的元素了。同时我们使用两个指针pA和pB,分别指向A和B数组的第一个元素。使用类似于merge sort的原理,如果数组A当前元素小,那么pA++,同时m++。如果数组B当前元素小,那么pB++,同时m++。最终当m等于k的时候,就得到了我们的答案——O(k)时间,O(1)空间。



方法三:(需要再判断A和B为空的情况,A为空,直接返回B的中间值)

看到log(m+n),我们能想到的是二分查找(logn),那么我们就向二分查找这个方向上靠拢。

但两个数组是分别有序的,但是我们要找的中间值的位置是固定的:(n+m)/2。那么我们就假设,在a中的长度为len1,在b中的长度为len2。因为长度一定,对其中一个进行折半,另外一个就通过“前一个相对移动的距离”来改变同样的距离。

1、但是m和n的值相对大小不知道,我们就假定让n<=m,那么在函数中我们就做了相应的对换,并且函数指针也相应改变,保持a指向短数组。

2、因为咱们要找的是(n+m)/2位置的值,那么这个值要么在a[len1]出现,要么在b[len2]出现。

3、假定len2来折半查找,当b[len2]这个值是中间值的时候,必须满足a[len1]<=b[len2]<=a[len1+1]。表明a[len1+1]在模拟数组C[n+m]中不会出现在b[len2]前面;当a[len1]为中间值的时候,必须满足a[len1-1]<=b[len2]<=a[len1]。正因为b[len2]和a[len1]都要在C中前半段出现,就要找其中大的一个,当满足a[len1-1]<=b[len2]时就不用再进行折半查找,已经能确定a[len1]为中间值。

4、当len1为0的时候的特殊比较:如果b[len2]<=a[len1]并且当b[len2+1]<=a[len1]的时候,C的前半段将不含a数组数据,中间值为b[len2+1].

5、当len1为n-1的时候:a[len1]<=b[len2]就已经能保证b[len2]为中间值,就不需要也不能比较b[len2]<=a[len1+1]这个表达式了。

6、不用再判断low和high的关系,因为我们已经保证了n<=m,所以把教研全部都放在了a数组的下标验证中。


代码如下:

#include <stdio.h>
#include <stdlib.h>

void Swap(int *a,int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

int MedianOfArray(int arr1[],int arr2[],int n,int m)
{
    printf("%d %d\n",n,m);
    int *a,*b;
    if(n > m){  //交换两个参数,使得n <= m 并且a指向短数组,b指向长数组
        Swap(&n,&m);
        a = arr2;
        b = arr1;
    }else{
        a = arr1;
        b = arr2;
    }

    int low = 0;            //以下三个参数用于数组b中
    int high;   //(n+m)/2表示要在b中找到多少个元素,转化成数组下标
    //对于{1,2,3,4,5},{6}来说,high=(n+m)/2的时候,high=3,mid=1指向了2,但是len=1却超出了范围
    high = (n+m)/2 - 1;     //减一是为了转化成数组下标;
        //high = (n+m)/2 - 1 -1;再减一是为了表示a数组先给一个元素指标,剩下的high为在b数组中的个数,
                        //但如果n+m=7时,high为1,也就是2个,算上a中的一共才3个,与中间值4不相符
    int mid = high;         //
    int len = 0;   //用于数组a中,high-mid为个数,减一是转化成下标
    int temp,increase,decrease;

    //n和m一样长也要考虑
    while(1){
        printf("len = %d,mid = %d,a[len] = %d,b[mid] = %d\n",len,mid,a[len],b[mid]);
        if(a[len] < b[mid]){
            if(len+1 >= n && n == m)  //处理右边界情况,当n==m的时候。由于前面保证了n<=m所以,要么在a最右边界,要么在b中
                return a[len];
            if( (len+1)>=n || a[len+1] >= b[mid])    //当len为边界的时候,或者b[mid]介于a[len]和a[len+1]之间
                return b[mid];                                      //或者当low和high已经不能再移动的时候

            high = mid;                 //调整high的值,重新界定mid和len的位置
            temp = (low + high)/2;      //但必须要注意防止len的值超过n,所以就需要if语句判断

            if(high - temp > n - len - 1)
                increase = n - len - 1;
            else
                increase = high - temp;
            mid = high - increase;
            len = len + increase;

        }else{
            if(len == 0){   //处理左边界情况。
                if(a[len] >= b[mid+1])  //如果a[len]大于等于b[mid+1]证明,中值已经中值一下的元素全在b中
                    return b[mid+1];
                return a[len];  //如果不是上述那样,则a[len]就是中值!
            }
            if(a[len-1] <= b[mid])  //如果b[mid]介于a[len]和a[len-1]之间,取较大值a[len]
                return a[len];
            if(low == high)
                return b[mid];
            low = mid;
            temp = (low+high)/2;

            if(temp - low > len)
                decrease = len;
            else
                decrease = temp - low;
            mid = low + decrease;
            len = len - decrease;  //必须为mid-low,b数组mid增加减少多少,a数组len相应改变多少!
        }
    }
}

int main(void)
{
    int arr1[] = {1,2,3,4,5,6,7};
    int arr2[] = {1,2,3,4,5,6,7};

    printf("the median is :%d",MedianOfArray(arr1,arr2,sizeof(arr1)/sizeof(int),sizeof(arr2)/sizeof(int)));
    return 0;
}

方法四:

其实和思路二差不多,但是利用递归的方法实现的:寻找第k个数据。

该方法的核心是将原问题转变成一个寻找第k小数的问题(假设两个原序列升序排列),这样中位数实际上是第(m+n)/2小的数。所以只要解决了第k小数的问题,原问题也得以解决。

首先假设数组A和B的元素个数都大于k/2,我们比较A[k/2-1]和B[k/2-1]两个元素,这两个元素分别表示A的第k/2小的元素和B的第k/2小的元素。这两个元素比较共有三种情况:>、<和=。如果A[k/2-1]<B[k/2-1],这表示A[0]到A[k/2-1]的元素都在A和B合并之后的前k小的元素中。换句话说,A[k/2-1]不可能大于两数组合并之后的第k小值,所以我们可以将其抛弃。下一次我们再A[k/2…n]和B[0…m]之间找k-k/2小的数据,直到寻找第k小的数据。

当A[k/2-1]>B[k/2-1]时存在类似的结论。

当A[k/2-1]=B[k/2-1]时,我们已经找到了第k小的数,也即这个相等的元素,我们将其记为m。由于在A和B中分别有k/2-1个元素小于m,所以m即是第k小的数。(这里可能有人会有疑问,如果k为奇数,则m不是中位数。这里是进行了理想化考虑,在实际代码中略有不同,是先求k/2,然后利用k-k/2获得另一个数。)

通过上面的分析,我们即可以采用递归的方式实现寻找第k小的数。此外我们还需要考虑几个边界条件
如果A或者B为空,则直接返回B[k-1]或者A[k-1];
如果k为1,我们只需要返回A[0]和B[0]中的较小值;
如果A[k/2-1]=B[k/2-1],返回其中一个;

最终实现的代码为:

double findKth(int a[], int m, int b[], int n, int k)
{
	//always assume that m is equal or smaller than n
	if (m > n)
		return findKth(b, n, a, m, k);
	if (m == 0)
		return b[k - 1];
	if (k == 1)
		return min(a[0], b[0]);
	//divide k into two parts
	int pa = min(k / 2, m), pb = k - pa;
	if (a[pa - 1] < b[pb - 1])
		return findKth(a + pa, m - pa, b, n, k - pa);
	else if (a[pa - 1] > b[pb - 1])
		return findKth(a, m, b + pb, n - pb, k - pb);
	else
		return a[pa - 1];
}

class Solution
{
public:
	double findMedianSortedArrays(int A[], int m, int B[], int n)
	{
		int total = m + n;
		if (total & 0x1)
			return findKth(A, m, B, n, total / 2 + 1);
		else
			return (findKth(A, m, B, n, total / 2)
					+ findKth(A, m, B, n, total / 2 + 1)) / 2;
	}
};

点赞