进阶实验1-3.1 两个有序序列的中位数

 已知有两个等长的非降序序列S1, S2, 设计函数求S1与S2并集的中位数。有序序列A​0​​,A​1​​,⋯,A​N−1​​的中位数指A​(N−1)/2​​的值,即第⌊(N+1)/2⌋个数(A​0​​为第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  

题目:进阶实验1-3.1 两个有序序列的中位数 (25分)

算法分析

 看完题目第一反应是两个集合求并集,再排个序输出中间的数就好了。但是看到数据量10,0000个数,时间限制是200ms。快排时间复杂度是O(nlogn),一定是会超时的。
 所以一定有一个更好的算法。
 接下来留意到题目中的序列是非降序序列,想到取各自的中位数然后比较。通过比较缩小问题规模的办法。如果办法有效,算法的时间复杂度应该是O(logn),满足评分要求。
 那么下面开始验证这个想法。

猜想与验证

基于数学进行猜想

 我们首先取序列S1的中位数设为mida,再取序列S2的中位数设为midb
 由于序列S1、S2都是升序排列的。故S1mida左边的数都小于mida,右边的数都大于mida。序列S2同理。
 此时比较midamidb
 由于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},即两个数中更小的那个。


基于测试用例验证

 我们来模拟一下这个过程。

image.png

 这是序列S1,此时mida=5。

image.png

 这是序列S2,此时midb=4。

 由于mida > midb,故此时U=S1∪S2被分成了两个集合,A={1,3,5}∪{4,5,6}(蓝色)及∁UA(白色)。

image.png

中位数必然在集合A中。因为中位数是排序后位于数列中间,所以它应该在两个升序子序列的中位数的中间。

 此时问题就变成了在集合A中取得中位数。白色的∁UA可以直接抛弃。

 递归上述操作,我们可以逐步迭代集合A。

直到这一步,我们会遇到一个问题,也是笔者遇到的一个大坑。

image.png

 此时两个序列中的数字个数都为偶数数,中位数为俩数中小的那个也就是前面那个。若继续按这种方式迭代,接下来的集合会变成这个。

image.png

由于{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}中的midamidb

 正确的集合A如下。

image.png

 最终筛选出中位数为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);
}

 运行情况如下。

image.png

 看了下最快耗时是25ms左右。并没有数量级上的差距。如果把递归改成循环,缓冲输入改成快速输入应该能有差不多的时间耗时,说明此算法应该是目前为止最快的了。

小结

 本次题目难度不大,主要锻炼了下写代码态度QAQ。毕竟好久没写代码了。对于边界条件的掌握还是有些生疏,希望能够更加严谨。
 朋友们有什么问题的也欢迎跟我交流~


满眼星辰
67 声望8 粉丝

计算机硕士研究生。