欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

查找两个有序数组合并后的中位数

程序员文章站 2024-03-16 12:59:16
...

例题 :查找两个有序数组合并后的中位数
【题目】 两个有序数组查找合并之后的中位数。给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出这两个正序数组合在一起之后的中位数,并且要求算法的时间复杂度为 O(log(m + n))。

你可以假设 nums1 和 nums2 不会同时为空,所有的数字全都不相等。还可以再假设,如果数字个数为偶数个,中位数就是中间偏左的那个元素。

例如:nums1 = [1, 3, 5, 7, 9]

nums2 = [2, 4, 8, 12]

输出 5。

【解析】 这个题目是我个人非常喜欢的,原因是,它所有的解法和思路,都隐含在了题目的描述中。如果你具备很强的分析和解决问题的能力,那么一定可以找到最优解法。

我们先看一下复杂度的分析。这里的 nums1 和 nums2 都是有序的,这让我们第一时间就想到了归并排序。方法很简单,我们把两个数组合并,就得到了合在一起后的有序数组。这个动作的时间复杂度是 O(m+n)。接着,我们从数组中就可以直接取出中位数了。很可惜,这并不满足题目的时间复杂度 O(log(m + n)) 的要求。

接着,我们来看一下这个问题的定位。题目中有一个关键字,那就是“找出”。很显然,我们要找的目标就藏在 nums1 或 nums2 中。这明显就是一个查找问题。而在查找问题中,我们学过的知识是分治法下的二分查找。

回想一下,二分查找适用的重要条件就是,原数组有序。恰好,在这个问题中 nums1 和 nums2 分别都是有序的。而且二分查找的时间复杂度是 O(logn),这和题目中给出的时间复杂度 O(log(m + n)) 的要求也是不谋而合。因此,经过分析,我们可以大胆猜测,此题极有可能要用到二分查找。

我们再来看一下数据结构方面。如果要用二分查找,就需要用到若干个指针,去约束查找范围。除此以外,并不需要去定义复杂的数据结构。也就是说,空间复杂度是 O(1) 。

好了,接下来,我们就来看一下二分查找如何能解决这个问题。二分查找需要一个分裂点,去把原来的大问题,拆分成两个部分,并在其中一部分继续执行二分查找。既然是查找中位数,我们不妨先试试以中位数作为切分点,看看会产生什么结果。如下图所示:

经过切分后,两个数组分别被拆分为 3 个部分,合在一起是 6 个部分。二分查找的思路是,需要从这 6 个部分中,剔除掉一些,让查找的范围缩小。那么,我们来思考一个问题,在这 6 个部分中,目标中位数一定不会发生在哪几个部分呢?

中位数有一个重要的特质,那就是比中位数小的数字个数,和比中位数大的数字个数,是相等的。围绕这个性质来看,中位数就一定不会发生在 C 和 D 的区间。

如果中位数在 C 部分,那么在 nums1 中,比中位数小的数字就会更多一些。因为 4 < 5(nums2 的中位数小于 nums1 的中位数),所以在 nums2 中,比中位数小的数字也会更多一些(最不济也就是一样多)。因此,整体来看,中位数不可能在 C 部分。同理,中位数也不会发生在 D 部分。

接下来,我们就可以在查找范围内,剔除掉 C 部分(永远比中位数大的数字)和 D 部分(永远比中位数小的数字),这样我们就成功地完成了一次二分动作,缩小了查找范围。然而这样并没结束。剔除掉了 C 和 D 之后,中位数有可能发生改变。这是因为,C 部分的数字个数和 D 部分数字的个数是不相等的。剔除不相等数量的“小数”和“大数”后,会造成中位数的改变。

为了解决这个问题,我们需要对剔除的策略进行修改。一个可行的方法是,如果 C 部分数字更少为 p 个,则剔除 C 部分;并只剔除 D 部分中的 p 个数字。这样就能保证,经过一次二分后,剔除之后的数组的中位数不变。

应该剔除 C 部分和 D 部分。但 D 部分更少,因此剔除 D 和 C 中的 9。

二分查找还需要考虑终止条件。对于这个题目,终止条件必然是某个数组小到无法继续二分的时候。这是因为,每次二分剔除掉的是更少的那个部分。因此,在终止条件中,查找范围应该是一个大数组和一个只有 1~2 个元素的小数组。这样就需要根据大数组的奇偶性和小数组的数量,拆开 4 个可能性:

可能性一:nums1 奇数个,nums2 只有 1 个元素。例如,nums1 = [a, b, c, d, e],nums2 = [m]。此时,有以下 3 种可能性:

如果 m < b,则结果为 b;

如果 b < m < c,则结果为 m;

如果 m > c,则结果为 c。

这 3 个情况,可以利用 "A?B:C" 合并为一个表达式,即 m < b ? b : (m < c ? m : c)。

可能性二:nums1 偶数个,nums2 只有 1 个元素。例如,nums1 = [a, b, c, d, e, f],nums2 = [m]。此时,有以下 3 种可能性:

如果 m < c,则结果为 c;

如果 c < m < d,则结果为 m;

如果m > d,则结果为 d。

这 3 个情况,可以利用"A?B:C"合并为一个表达式,即 m < c ? c : (m < d? m : d)。

可能性三:nums1 奇数个,nums2 有 2 个元素。例如,nums1 = [a, b, c, d, e],nums2 = [m,n]。此时,有以下 6 种可能性:

如果 n < b,则结果为 b;

如果 b < n < c,则结果为 n;

如果 c < n < d,则结果为 max(c,m);

如果 n > d,m < c,则结果为 c;

如果 n > d,c < m < d,则结果为 m;

如果 n > d,m > d,则结果为 d。

其中,4~6 可以合并为,如果 n > d,则返回 m < c ? c : (m < d ? m : d)。

可能性四:nums1 偶数个,nums2 有 2 个元素。例如,nums1 = [a, b, c, d, e, f],nums2 = [m,n]。此时,有以下 6 种可能性:

如果 n < b,则结果为 b;

如果 b < n < c,则结果为 n;

如果 c < n < d,则结果为 max(c,m);

如果 n > d,m < c,则结果为 c;

如果 n > d,c < m < d,则结果为 m;

如果 n > d,m > d,则结果为 d。与可能性 3 完全一致。

不难发现,终止条件都是 if 和 else 的判断,虽然逻辑有点复杂,但时间复杂度是 O(1) 。为了简便,我们可以假定,nums1 的数字数量永远是不少于 nums2 的数字数量。

因此,我们可以编写如下的代码:

#include<stdio.h>

int find(int* a,int begina,int enda,int* b,int beginb,int endb){
	
	int lengtha = enda - begina + 1;
	int lengthb = endb - beginb + 1;
	int mid = (enda + begina) / 2;
	//如果 a, b都只剩一个元素
	if(lengtha == 1 ){
		return a[begina]<b[beginb]?a[begina]:b[beginb];
	} 
	//如果a,b都剩两个元素
	if(lengtha == 2 ){
		if(a[begina] < b[beginb]){
			return a[enda] < b[beginb] ? a[enda] : b[beginb];
		}else{
			return b[endb] < a[begina] ? b[endb] : a[begina];
		}
	} 
	//假定a的元素个数大于b的元素个数
	//当b的个数为1时 a的个数为偶数时 
	if((lengtha % 2 == 0 )&& (lengthb == 1)){
		int m = b[beginb];
		int c = a[mid];
		int d = a[mid + 1];
		return m < c ? c : (m < d ? m : d);
	}
	//当b的个数为1时, a的个数为奇数时 
	if((lengtha % 2 == 1) && (lengthb == 1)){
		int bb = a[mid - 1];
		int c = a[mid];
		int m = b[beginb];
		return m < bb ? bb : (m < c ? m : c);
	
	}
	//当b的个数为2时,a的个数为奇数或偶数的判断情况是一样的 
	if(lengthb == 2){
		int bb = a[mid - 1];
		int c = a[mid];
		int d = a[mid + 1];
		
		int m = b[beginb];
		int n = b[endb];
		if(n < bb){
			return bb;
		}else if(n < c){
			return n;
		}else if(n < d){
			return c > m ? c : m;
		}else{
			return m < c ? c : (m < d ? m : d);
		}
	}
	
	
	
	//划分
	int mida = (begina + enda) / 2;
	int midb = (beginb + endb) / 2;
	
	//划分原则 中位数比一组中间数大的小(在左边), 比一组中间数小的大(在右边),两组增删一致,按b的即元素少的那一组 
	if( a[mida] < b[midb]){
		int steps = endb - beginb;
		return find(a,begina + steps,enda,b,beginb,endb - steps);
	} else{
		int steps = midb - beginb;
		return find(a,begina,enda - steps,b,beginb + steps,endb);
	}
}

int main(){
	int a[] = {1,2,3,4,5};
	int b[] = {6,7,8,9};
	int lengtha = sizeof(a) / sizeof(a[0]);
	int lengthb = sizeof(b) / sizeof(b[0]);
	printf("两组有序数列的合并数列中位数为%d\n",find(a,0,lengtha - 1,b,0,lengthb - 1));
	return 0;
}

 

相关标签: c语言算法