题目:
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;
}
};