进阶实验1-3.1 两个有序序列的中位数
已知有两个等长的非降序序列S1, S2, 设计函数求S1与S2并集的中位数。有序序列A0,A1,⋯,AN−1的中位数指A(N−1)/2的值,即第⌊(N+1)/2⌋个数(A0为第1个数)。
输入样例1:
5 1 3 5 7 9 2 3 4 5 6
输出样例1:
4
输入样例2:
6 -100 -10 1 1 1 1 -50 0 2 3 4 5
输出样例2:
1
算法分析
看完题目第一反应是两个集合求并集,再排个序输出中间的数就好了。但是看到数据量10,0000个数,时间限制是200ms。快排时间复杂度是O(nlogn),一定是会超时的。
所以一定有一个更好的算法。
接下来留意到题目中的序列是非降序序列,想到取各自的中位数然后比较。通过比较缩小问题规模的办法。如果办法有效,算法的时间复杂度应该是O(logn),满足评分要求。
那么下面开始验证这个想法。
猜想与验证
基于数学进行猜想
我们首先取序列S1的中位数设为mida,再取序列S2的中位数设为midb。
由于序列S1、S2都是升序排列的。故S1mida左边的数都小于mida,右边的数都大于mida。序列S2同理。
此时比较mida和midb。
由于mida是S1的中位数,midb是S2的中位数。故集合U=S1∪S2
中,大于MAX{mida,midb}
的所有数都不可能是中位数。同理可得,集合中小于MIN{mida,midb}
的所有数也都不可能是中位数。
通过比较mida和midb的大小,我们把集合U划分成了两个区间.
即A=[MIN{mida,midb}的右区间, MAX{mida,midb}的左区间]
和∁UA
。
此时问题就被简化成了求集合A的中位数。
而后通过不断的二分查找,A最后一定会变成一个只有2个数的集合。那么根据中位数的定义,此时中位数必然是min{A}
,即两个数中更小的那个。
基于测试用例验证
我们来模拟一下这个过程。
这是序列S1,此时mida=5。
这是序列S2,此时midb=4。
由于mida > midb,故此时U=S1∪S2
被分成了两个集合,A={1,3,5}∪{4,5,6}
(蓝色)及∁UA
(白色)。
中位数必然在集合A中。因为中位数是排序后位于数列中间,所以它应该在两个升序子序列的中位数的中间。
此时问题就变成了在集合A中取得中位数。白色的∁UA
可以直接抛弃。
递归上述操作,我们可以逐步迭代集合A。
直到这一步,我们会遇到一个问题,也是笔者遇到的一个大坑。
此时两个序列中的数字个数都为偶数数,中位数为俩数中小的那个也就是前面那个。若继续按这种方式迭代,接下来的集合会变成这个。
由于{3,5}中,中位数为3,小于4,那么接下来应该取它右边的序列。此时会发现此序列取右边的序列还是{3,5}!它会造成无限递归或者死循环!
所以分析到这一步我们发现,应该是要分辨集合中数字的个数为奇数还是偶数来分别取子序列。最终我们发现,除了0以外,自然数中最小的偶数是2。在序列长度为2且升序的情况下,中位数直接就是前面那个。
把它扩展到4,那么我们发现只要抛掉首位两个数,情况就退化成了上述情况。即又一次迭代了集合A。换作到题目中,即直接抛掉偶数序列中的边界数即可。即mida或midb(两个子序列都是偶数则兼有之)。
所以{4,5,6,1,3,5}
之后的迭代出的{4,5,3,5}
是不正确的!
正确迭代方式应该抛掉{4,5,6,1,3,5}
中的mida和midb。
正确的集合A如下。
最终筛选出中位数为4。
代码
下面给出笔者的代码。由于最近在复习C语言,写的是尾递归的版本。
#include <stdio.h>
#define MAX_N 100000
/* 二分查找函数声明 aleft a数组左下标,aright a数组右下标*/
int bin_search(int a[], int aleft, int aright, int b[], int bleft, int bright);
int main()
{
int n = 0, a[MAX_N] = {0}, b[MAX_N] = {0};
scanf("%d", &n);
for(int i = 0;i<n;i++){
scanf("%d", &a[i]);
}
for(int i=0;i<n;i++){
scanf("%d", &b[i]);
}
int mid = bin_search(a, 0, n-1, b, 0, n-1);
printf("%d\n", mid);
return 0;
}
int bin_search(int a[], int aleft, int aright, int b[], int bleft, int bright){
int al=0, ar=0, bl=0, br=0; /* 下一步递归的a,b数组下标 */
/* indexa a数组中位数下标 mida a数组中位数的值*/
int indexa = (aleft+aright)/2, indexb = (bleft+bright)/2, mida = a[indexa], midb = b[indexb];
/* 如果俩数组中位数相等 则必是解 */
if(mida == midb){
return mida;
}
/*如果待查找区间内只有一个数,则小的那个为解*/
if(aleft >= aright && bleft >= bright){
return mida<midb?mida:midb;
}
if(mida > midb){
bl = indexb; /* 小的取右区间 */
br = bright;
ar = indexa; /* 大的取左区间 */
al = aleft;
if( (aright-aleft+1) % 2 == 0){ /*偶数个数缩小范围时抛掉当前中位数*/
bl = indexb+1;
}
}else if(mida < midb){
al=indexa;
ar = aright;
bl=bleft;
br = indexb;
if((bright-bleft+1) % 2 == 0){
al=indexa+1;
}
}
return bin_search(a, al, ar, b, bl, br);
}
运行情况如下。
看了下最快耗时是25ms左右。并没有数量级上的差距。如果把递归改成循环,缓冲输入改成快速输入应该能有差不多的时间耗时,说明此算法应该是目前为止最快的了。
小结
本次题目难度不大,主要锻炼了下写代码态度QAQ。毕竟好久没写代码了。对于边界条件的掌握还是有些生疏,希望能够更加严谨。
朋友们有什么问题的也欢迎跟我交流~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。