两个有序数组合并后找第k个数(中位数)
2015-09-30 15:03
337 查看
原文链接:http://m.blog.csdn.net/blog/fangkyo/8114784
经常有同学在面试或者笔试的时候遇到两个有序数组(未必等长)找第k个数的问题。归并比较的方法固然可以完成,但是面试官总是期望O(logN)的解决方案。我参考了网上的所有方法都发现有特殊案例无法通过的情况,主要问题集中在边界情况上,比如一个数组只有一个或两个元素时,或者一个数组的长度不足k/2,总之很麻烦。现在我给出一个比较完美和简单的解决方案,希望大家指正,如果可以的话希望广大人民群众不要再被这道题折磨了。
废话少说,进入正题吧,我的方法是基于中位数的比较。为什么使用中位数而不是在两个数组中各取k/2大的数呢,是因为只要数组的长度不为0,那么中位数总是存在的,而第K/2的数就不一定了,数组的长度很有可能小于k/2哦。通过这样的方法我们可以规避一大票边界问题。
我们将两个数组按照3种情况来讨论:
1、两个数组的长度均为奇数。
我们设a数组的中位数下标为x,b数组的中位数下标为y,则如下图所示,a数组的长度为2x+1,b数组的长度为2y+1,总的元素的个数为2x+2y+2。
我们现在需要探讨的就是k是大于x+y+1,还是小于x+y+1的情况,也就是我们想找的第k大的数是在a、b数组归并后的数组中的前一半内,还是在后一半内。不失一般性,我们讨论当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 大数。
当a[x]
> b[y]时,只是上述的情况反一下,读者可以自己试着推一下。
2、两个数组的长度均为偶数
我们寻找a数组的上中位下标,记为x,b数组的下中位下标,记为y,如下图所示。这样错开中位数的目的是为了在将来做排除时能够保证像奇数情况下能将第k大数归类到前一半或者后一半的完美性质。
这样看来,a数组长度为2x + 2,b数组长度为2y,因此归并后的数组长度为2x + 2y + 2,与奇数情况一致。
不失一般性,我们讨论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、2讨论的问题。
4、当a、b数组的长度有一个为0时,直接返回另一个长度非0数组的第k个数即可。
综上所述,我们在每次递归的时候都去除了一个数组中包含一个中位数的一半元素,直到一个数组被完全排除在外为止,直接去另一个数组的第k个数。上面的过程中由于每次总会去除一个中位数,因此算法总能够收敛,不难证明算法的时间复杂度为O(logN)。
我将我的代码贴出来作为参考:
经常有同学在面试或者笔试的时候遇到两个有序数组(未必等长)找第k个数的问题。归并比较的方法固然可以完成,但是面试官总是期望O(logN)的解决方案。我参考了网上的所有方法都发现有特殊案例无法通过的情况,主要问题集中在边界情况上,比如一个数组只有一个或两个元素时,或者一个数组的长度不足k/2,总之很麻烦。现在我给出一个比较完美和简单的解决方案,希望大家指正,如果可以的话希望广大人民群众不要再被这道题折磨了。
废话少说,进入正题吧,我的方法是基于中位数的比较。为什么使用中位数而不是在两个数组中各取k/2大的数呢,是因为只要数组的长度不为0,那么中位数总是存在的,而第K/2的数就不一定了,数组的长度很有可能小于k/2哦。通过这样的方法我们可以规避一大票边界问题。
我们将两个数组按照3种情况来讨论:
1、两个数组的长度均为奇数。
我们设a数组的中位数下标为x,b数组的中位数下标为y,则如下图所示,a数组的长度为2x+1,b数组的长度为2y+1,总的元素的个数为2x+2y+2。
我们现在需要探讨的就是k是大于x+y+1,还是小于x+y+1的情况,也就是我们想找的第k大的数是在a、b数组归并后的数组中的前一半内,还是在后一半内。不失一般性,我们讨论当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 大数。
当a[x]
> b[y]时,只是上述的情况反一下,读者可以自己试着推一下。
2、两个数组的长度均为偶数
我们寻找a数组的上中位下标,记为x,b数组的下中位下标,记为y,如下图所示。这样错开中位数的目的是为了在将来做排除时能够保证像奇数情况下能将第k大数归类到前一半或者后一半的完美性质。
这样看来,a数组长度为2x + 2,b数组长度为2y,因此归并后的数组长度为2x + 2y + 2,与奇数情况一致。
不失一般性,我们讨论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、2讨论的问题。
4、当a、b数组的长度有一个为0时,直接返回另一个长度非0数组的第k个数即可。
综上所述,我们在每次递归的时候都去除了一个数组中包含一个中位数的一半元素,直到一个数组被完全排除在外为止,直接去另一个数组的第k个数。上面的过程中由于每次总会去除一个中位数,因此算法总能够收敛,不难证明算法的时间复杂度为O(logN)。
我将我的代码贴出来作为参考:
#include <iostream> using namespace std; int getKth( int a[], int alen, int b[], int blen, int k) { if( NULL == a || alen < 0 || NULL == b || blen < 0 || k <=0 || k > alen + blen ) return -1; if( alen == 0 ) return b[ k - 1 ]; if( blen == 0 ) return a[ k - 1 ]; if( k == 1 ) return min( a[0], b[0] ); if( ( alen & 0x1 ) ^ ( blen &0x1 ) ) // 一奇一偶 { if( a[0] < b[0] ) return getKth( a+1, alen -1, b, blen, k-1); else return getKth( a, alen, b+1, blen - 1, k-1); } int aMid, bMid; if( alen & 0x1 ) // 两个数组长度为奇数 { aMid = alen / 2; bMid = blen / 2; } else { aMid = alen / 2 - 1; bMid = blen / 2; } if( a[aMid] <= b[bMid] ) { if( k <= aMid + bMid + 1 ) return getKth( a, alen, b, bMid , k ); else return getKth( a+aMid + 1, alen - aMid - 1, b, blen, k - aMid - 1 ); } else { if( k <= aMid + bMid + 1 ) return getKth( a, aMid, b, blen, k ); else return getKth( a, alen, b+bMid+1, blen - bMid - 1, k - bMid - 1 ); } return 0; } int main() { int a[] = {1,3,6,94}; int b[] = {2,4,5,8,10,22}; for( int i=1; i<=10; ++i ) cout << getKth( a, 4, b, 6, i ) << endl; system("pause"); return 0; }
相关文章推荐
- 【Javascript】: for循环中定义的变量在for循环体外也有效
- PHP实现快速排序、插入排序、选择排序
- (IIS8/8.5/Apache)301域名重定向
- 英雄展示代码进一步完善(包括可重用单元格)
- linux mint安装搜狗输入法
- 小马哥---山寨小米平板电脑刷机拆机主板图与开机识别图 界面图 多图展示 精仿版本 购买警惕
- 基于SVG的web页面图形绘制API介绍
- FAT32分区格式
- 我的头马经历
- 源码包安装git
- 九月英语总结——不同凡响
- 浅析Android M新功能Adoptable Storage Devices(适配的存储设备)
- Flash Tool Lite User Manual
- 三、网页授权获取用户信息
- C语言使用正则表达式
- JS正则座机电话和邮箱匹配
- sflow
- [php] 搭建LAMP环境
- eclipse下SVN subclipse插件
- RequireJS入门一之实现第一个例子