问题描述:
对于下面的问题给出一个分治算法(divide and conquer):给出两个排好序的列表,两个列表的元素个数分别是m和n,对于每一个列表你被允许有单元时间去访问第i个元素。给出一个时间复杂度是O(lg m+lg n)的算法来计算出两个列表合并之后第k大的元素。(为了简单起见,你可以假定这两个列表中的元素是不同的)。
求第k大的元素,即为求第(m+n-k+1)小的元素
算法设计:
按照下面三种情况进行分析:
(1)两个数组元素的个数均为奇数的情况下:
设a数组的中位数的下标为x,b数组中位数的下标为y,则a数组的长度为2x+1,b数组的长度为2y+1,总的元素个数为2x+2y+1。
不妨先假设a[x]<b[y]:
当k<=x+y+1,也就是说k属于归并数组的前一半时,从下图可以看出黄色区域不包含第k个数,因为b[y]大于等于蓝色部分的数,而蓝色部分的数据即为x+y+1,恰好为全体数的前一半的个数,也就是说黄色区域的数不可能属于归并后的前一半数组,可以排除在外,在剩余的蓝颜色的区域中继续寻找第k大的数。
当k>x+y+1,也即k属于归并后数组的后一半,那么从下图可以看出黄色区域明显不包含第k个数。原因是黄色区域的数必然小于蓝色区域的数,而蓝色区域的数的数目之和即为x+y+1,恰好为归并后数组的和的一半,因此可以将黄色区域排除在外,然后在蓝色区域中寻找第k-x-1大的数(因为前x+1个数已经找过)。
当a[x]>b[y]的时候,将上面的情况反过来。
(2)两个数组的长度均为偶数
我们寻找a数组的上中位下标,记为x,b数组的下中位下标,记为y,如下图所示。这样错开中位数的目的是为了在将来做排除时能够保证像奇数情况下能将第k大数归类到前一半或者后一半。
这个时候,a数组长度为2x+2,b数组长度为2y,因此归并后的数组的长度为2x+2y+1,与奇数情况下相同。
我们还是先假设a[x]<=b[y]:
当k<=x+y+1,也即k属于归并数组的前一半的时候,如下图所示。显然黄色部分可以排除在外,因为黄色部分一定大于等于蓝色部分,而蓝色部分的长度之和为x+y+1,恰好为归并数组的一半。也就是说黄色部分必然位于归并数组的后一半,不可能包含第k个数。之后,我们在蓝色区域中找第k大的数。
当k>x+y+1,也即k属于归并数组的后一半时,如下图所示。显然黄色部分可以排除在外,以为黄色部分一定小于蓝色部分,而蓝色部分的长度之和为x+y+1,恰好为归并数组的一半。也就是说黄色部分必然位于归并数组的前一半,不可能包含第k个数。之后我们在蓝色区域中寻找第k-x-1个数。
当a[x]>b[y]时,情况与上述相反。
(3)当a、b数组的长度为一奇一偶的情况下:
若k=1,结果为min(a[0],b[0])。
若k>1,不妨假设a[0]<b[0],那么a[0]必然是两个数组中最小的数,那么去掉a[0]之后,我们可以在a,b两个数组中去寻找第k-1个数,并且a数组的长度减1,将问题转化为(1)(2)这两种情况。
(4)当a、b数组的长度有一个为0时,直接返回另一个数组的第k个数即可。
我们在每次递归的时候都去除了一个数组中包含一个中位数的一半的元素,直到一个数组被完全排除在外为止,直到取另一个数组的第k个数。上面的过程中由于每次总会取出一个中位数,所以算法是收敛的,并且算法的时间复杂度是O(log m + log n)。
伪代码:
1 GETKth(a,alen,b,blen,k) 2 if a=NULL || alen<1 || b=NULL || blen<1 || k<=0 || k>alen+blen 3 return -1 4 if alen=0 5 return b[k] 6 if blen=0 7 return a[k] 8 if k=1 9 return min(a[1],b[1]) 10 if alen or blen is one odd and one even number 11 if a[1]<b[1] 12 return GETKth(a+1,alen-1,b,blen,k-1) 13 else 14 return GETKth(a,alen,b+1,blen-1,k-1) 15 if alen and blen are both odd number 16 aMid=alen/2 17 bMid=blen/2 18 else 19 aMid=alen/2-1 20 bMid=blen/2 21 if a[aMid]<=b[bMid] 22 if k<=aMid+bMid+1 23 return GETKth(a,alen,b,bMid,k) 24 else 25 return GETKth(a+aMid+1,alen-aMid-1,b,blen,k-aMid-1) 26 else 27 if k<=aMid+bMid+1 28 return GETKth(a,aMid,b,blen,k) 29 else 30 return GETKth(a,alen,n+bMid+1,blen-bMid-1,k-bMid-1) 31 return 0 32
C++源码:
1 #include <iostream> 2 using namespace std; 3 4 int getKth( int a[], int alen, 5 int b[], int blen, 6 int k) 7 { 8 if( NULL == a || alen < 0 || NULL == b || blen < 0 || k <=0 || k > alen + blen ) 9 return -1; 10 11 if( alen == 0 ) 12 return b[ k - 1 ]; 13 if( blen == 0 ) 14 return a[ k - 1 ]; 15 16 if( k == 1 ) 17 return min( a[0], b[0] ); 18 19 if( ( alen & 0x1 ) ^ ( blen &0x1 ) ) // 一奇一偶 20 { 21 if( a[0] < b[0] ) 22 return getKth( a+1, alen -1, b, blen, k-1); 23 else 24 return getKth( a, alen, b+1, blen - 1, k-1); 25 } 26 27 int aMid, bMid; 28 if( alen & 0x1 ) // 两个数组长度为奇数 29 { 30 aMid = alen / 2; 31 bMid = blen / 2; 32 } 33 else 34 { 35 aMid = alen / 2 - 1; 36 bMid = blen / 2; 37 } 38 39 if( a[aMid] <= b[bMid] ) 40 { 41 if( k <= aMid + bMid + 1 ) 42 return getKth( a, alen, b, bMid , k ); 43 else 44 return getKth( a+aMid + 1, alen - aMid - 1, b, blen, k - aMid - 1 ); 45 } 46 else 47 { 48 if( k <= aMid + bMid + 1 ) 49 return getKth( a, aMid, b, blen, k ); 50 else 51 return getKth( a, alen, b+bMid+1, blen - bMid - 1, k - bMid - 1 ); 52 } 53 return 0; 54 } 55 56 int main() 57 { 58 int a[] = {1,3,5,7}; 59 int b[] = {2,4,6,8,10,12}; 60 // for( int i=1; i<=10; ++i ) 61 // cout << getKth( a, 4, b, 6, i ) << endl; 62 cout <<"第3大的数为:" <<getKth( a, 4, b, 6, 3 ) << endl; 63 system("pause"); 64 return 0; 65 }
参考:
https://blog.csdn.net/lqglqglqg/article/details/48845225