快速寻找满足条件的两个数字

http://blog.csdn.net/hackbuteer1/article/details/6699642

能否快速找出一个数组中的两个数字,让这两个数字之和等于一个给定的值,为了简化起见,我们假设这个数组中肯定存在至少一组符合要求的解。

     假如有如下的两个数组,如图所示:

    5,6,1,4,7,9,8

    给定Sum= 10

    1,5,6,7,8,9

    给定Sum= 10

   分析与解法

    这个题目不是很难,也很容易理解。但是要得出高效率的解法,还是需要一番思考的。

    解法一

     一个直接的解法就是穷举:从数组中任意取出两个数字,计算两者之和是否为给定的数字。

     显然其时间复杂度为N(N-1)/2即O(N^2)。这个算法很简单,写起来也很容易,但是效率不高。一般在程序设计里面,要尽可能降低算法的时间和空间复杂度,所以需要继续寻找效率更高的解法。

     解法二

     求两个数字之和,假设给定的和为Sum。一个变通的思路,就是对数组中的每个数字arr[i]都判别Sum-arr[i]是否在数组中,这样,就变通成为一个查找的算法。

     在一个无序数组中查找一个数的复杂度是O(N),对于每个数字arr[i],都需要查找对应的Sum-arr[i]在不在数组中,很容易得到时间复杂度还是O(N^2)。这和最原始的方法相比没有改进。但是如果能够提高查找的效率,就能够提高整个算法的效率。怎样提高查找的效率呢?

     学过编程的人都知道,提高查找效率通常可以先将要查找的数组排序,然后用二分查找等方法进行查找,就可以将原来O(N)的查找时间缩短到O(log2N),这样对于每个arr[i],都要花O(log2N)去查找对应的Sum-arr[i]在不在数组中,总的时间复杂度降低为N* log2N。当让将长度为N的数组进行排序本身也需要O(N*log2N)的时间,好在只须要排序一次就够了,所以总的时间复杂度依然是O(N*log2N)。这样,就改进了最原始的方法。

     到这里,有的读者可能会更进一步地想,先排序再二分查找固然可以将时间从O(N^2)缩短到O(N*log2N),但是还有更快的查找方法:hash表。因为给定一个数字,根据hash表映射查找另一个数字是否在数组中,只需要O(1)时间。这样的话,总体的算法复杂度可以降低到O(N),但这种方法需要额外增加O(N)的hash表存储空间。某些情况下,用空间换时间也不失为一个好方法。

     解法三

     还可以换个角度来考虑问题,假设已经有了这个数组的任意两个元素之和的有序数组(长为N^2)。那么利用二分查找法,只需用O(2*log2N)就可以解决这个问题。当然不太可能去计算这个有序数组,因为它需要O(N^2)的时间。但这个思考仍启发我们,可以直接对两个数字的和进行一个有序的遍历,从而降低算法的时间复杂度。

      首先对数组进行排序,时间复杂度为(N*log2N)。

      然后令i = 0,j = n-1,看arr[i] + arr[j] 是否等于Sum,如果是,则结束。如果小于Sum,则i = i + 1;如果大于Sum,则 j = j – 1。这样只需要在排好序的数组上遍历一次,就可以得到最后的结果,时间复杂度为O(N)。两步加起来总的时间复杂度O(N*log2N),下面这个程序就利用了这个思想,代码如下所示:

int getSumNum(int[] arr,int Sum),   //arr为数组,Sum为和 
{
	int i,j;
	for(i = 0, j = n-1; i < j ; )
	{
		if(arr[i] + arr[j] == Sum)
			return ( i , j );
		else if(arr[i] + arr[j] < Sum)
			i++;
		else
			j--;
	}
	return ( -1 , -1 );
}


 

它的时间复杂度是O(N)。

      刚开始一直无法理解这样一定可以找到这个和吗?难道不会漏掉了解的位置。可以这么理解,假如排好序后的数组为1,3,6,a,9,12,17,28,b,35,46  ,那么i最初指向1的位置,j最初指向46的位置,比如所求的是Sum=a+b,a<b,a和b在数组中的某位置上。那么i和j在变化过程中,只考虑i遇到了a或者j遇到了b的时候,必定有一个先遇到,比如i遇到了a,那么这个时候j必定还没指到b,故这是j指到的值比b大,从而j减小直到b位置。同理若j先指到了b位置,那么i必定还没指到a(这是我们的前提),然后i现在指到的值比a小,故i增加,直到a位置。

 

 

扩展问题

1、如果把这个问题中的“两个数字”改成“三个数字”或“任意个数字”时,你的解是什么呢?

三个数字:首先还是先对数组进行排序,然后从i=0到n-1进行遍历,遍历arr[i]时,在调用上面的函数getSumNum(arr , Sum-arr[i])即可。

任意m个数字的想法:

      首先还是先对数组进行排序,然后从i=0到n-1个元素遍历,遍历arr[i]时,在剩下的n-1个元素中调用getSumNum(arr,Sum-arr[i]),此时为求m-1个元素和为Sum-arr[i];接下来,同样的方法,从j=0到n-2个元素遍历,遍历arr[j]时在arr上递归调用getSumNum(arr,Sum-arr[i]-arr[j]),此时为求m-2个元素和为Sum-arr[i]-arr[j];依次递归,直到为求2个元素和为Sum-?-?-?...时为止。

不论是求3个数字还好是m个数字,总是能比较穷举法少一个数量级n,比先排序然后二分查找求Sum-arr[i]也要快。

 

以下是全部的测试代码:

//快速排序
void sort(int a[],int l,int r)
{
	if(l<r)
	{
		int i=l,j=r,x=a[i];
		while(i<j)
		{
			while(a[i]<=a[j]&&i<j)
			{
				j--;
			}
			if(i<j)
			{
				a[i++]=a[j];
			}
			while(a[i]<a[j]&&i<j)
			{
				i++;
			}
			if(i<j)
				a[j--]=a[i];
		}
		a[i]=x;
		sort(a,l,i-1);
		sort(a,i+1,r);
	}
}
int FindTowFen(int a[],int len,int ans)
{
	int i=0;
	int j=len-1;
	int mid;
	while(i<=j)
	{
		mid=(i+j)>>1;
		if(a[mid]==ans)
			return mid;
		else if(a[mid]>ans)
			j=mid-1;
		else
			i=mid+1;
	}
	return -1;
}

void GetSumNum_TwoFen(int a[],int len,int sum)
{
	int i=0,j=len-1;
	for(;i<j;i++)
	{
		int temp=sum-a[i];
		int pos=FindTowFen(a,len,temp);
		if(pos!=-1)
		{
			cout<<a[i]<<"+"<<a[pos]<<endl;
			break;
		}
	}
}


void GetSumNum(int a[],int len,int Sum)
{
	int i=0;
	int j=len-1;
	for(;i<j;)
	{
		if(a[i]+a[j]==Sum)
		{
			cout<<"a[i]:"<<a[i]<<" "<<"a[j]:"<<a[j]<<endl;
			return;
		}
		else if(a[i]+a[j]<Sum)
		{
			i++;
		}
		else
			j--;
	}
}

void GetSumNum_Hash(int a[],int len,int Sum)
{
	int *hashtable=(int*)malloc(sizeof(int)*100);
	memset(hashtable,0,100);
	for(int i=0;i<len;i++)
		hashtable[i]=0;
	for(int i=0;i<len;i++)
	{
		hashtable[a[i]]=1;
	}
	for(int i=0;i<len;i++)
	{
		if(hashtable[Sum-a[i]]==1)
		{
			cout<<a[i]<<"+"<<Sum-a[i]<<" "<<endl;
			break;
		}
	}
	delete[] hashtable;
}

int main()
{
	int a[]={0,9,2,8,5,6,10};
	//sort(a,0,sizeof(a)/sizeof(int)-1);
	//for(int i=0;i<sizeof(a)/sizeof(int);i++)
	//	cout<<a[i]<<" ";
	//cout<<endl;
	//GetSumNum(a,sizeof(a)/sizeof(int),10);
	//GetSumNum_TwoFen(a,sizeof(a)/sizeof(int),10);
	GetSumNum_Hash(a,sizeof(a)/sizeof(int),10);

	//getchar();

	return 0;
}


 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值