题目:输入一个数组和一个数字,在数组中查找两个数,使得它们的和正好是输入的那个数字。
要求时间复杂度是O(n)。如果有多对数字的和等于输入的数字,输出任意一对即可。例如输入数组1、2、4、7、11、15和数字15。由于4+11=15,因此输出4和11。
方法一:暴力枚举法。
从数组中任意取出两个数字,计算两者之和是否为给定的数字。显然时间复杂度为N(N-1)/2,代码如下:
//暴力枚举,需要比较N(N-1)/2次
pair<int,int> findTwoNum(int Arr[] , int arrLen ,int sum)
{
for(int i=0 ; i<arrLen ; i++)
for(int j=i+1 ; j<arrLen ; j++){
if(sum == Arr[i]+Arr[j]){
return make_pair(Arr[i],Arr[j]);
}
}
return make_pair(-1,-1);
}
方法二:变为查找数组中sum-Arr[i]是否存在。
对每个Arr[i]都查找sum-Arr[i]是否存在,如果采用直接查询需要遍历一次数组。查询需要O(N),于是该算法总共需要O(N^2);如果先排序后查询,查询采用二分法。总的复杂度为O(NlogN)。
当然还有更快的查询方法,hash表。因为给定一个数字,根据hash映射查找一个数字是否在数组中,只需要O(1)的时间。这样的话总体算法复杂度降低到O(N),但这种方法需要额外增加O(N)的hash表存储空间。某些情况下,用空间换时间不失为一个好方法。下面给出算法的实现代码:
//先快速排序O(NlogN)后二分查找sum-Arr[i]是否存在
//总复杂度O(NlogN)
void swap2Num(int &a , int &b)
{
int temp = a;
a=b;
b=temp;
}
int Partition(int Arr[] , int low , int high){
srand((unsigned)time(0));
//注意:此处容易犯的错误是随机在数组取数与Arr[0]交换而不是Arr[low]
swap2Num(Arr[low] , Arr[rand()%(high-low+1)+low]);//随机主元,分割更加均匀
int pivot = Arr[low];
while(low < high){
while(low<high && Arr[high]>=pivot)
high--;
if(low<high)
Arr[low] = Arr[high];
while(low<high && Arr[low]<=pivot)
low++;
if(low<high)
Arr[high] = Arr[low];
}
Arr[high] = pivot;
return high;
}
void fastSort(int Arr[] , int low , int high)
{
//递归截止条件
if(high <= low)
return;
int mid = Partition(Arr , low , high);
fastSort(Arr , low , mid-1);
fastSort(Arr , mid+1 , high);
}
//返回数num在数组中下标,找不到返回-1
int bSearchNum(int Arr[] , int low , int high , int num)
{
if(high < low){//递归到叶子节点后
return -1;
}
int mid = low + (high-low)/2;//防止上溢
if(Arr[mid] == num)
return mid;
if(Arr[mid] > num)
bSearchNum(Arr , low , mid-1 , num);
else
bSearchNum(Arr , mid+1 , high ,num);
}
//
pair<int,int> findTwoNum(int Arr[] , int arrLen ,int sum)
{
//---先快速排序O(NlogN)
fastSort(Arr , 0 , arrLen-1);
//---二分查找O(NlogN)
int i;
for(i=0 ; i<arrLen ; i++){
if(-1 != bSearchNum(Arr , 0 , arrLen-1 , sum-Arr[i])){
return make_pair(Arr[i] , sum-Arr[i]);
}
}
return pair<int,int>(-1,-1);
}
方法三:先排序,后首尾相加与sum比较
排序需要O(NlogN),然后令i=0,j=N-1,看arr[i]和arr[j]与sum的关系,如果大于sum,说明j太大了(数组排了序),于是j--;否则小于sum说明i太小了,于是i++。i某个时刻等于j时停止查找,于是只需要遍历一次O(N)就能得到最后结果。两步加起来时间复杂度O(NlogN)。下面是实现代码:
//使用首尾相加的方法判定
pair<int,int> findTwoNum(int Arr[] , int arrLen ,int sum)
{
//---先快速排序O(NlogN)
fastSort(Arr , 0 , arrLen-1);
//---再指针移位O(N)
int i=0 ;
int j=arrLen-1;
while(i<j){
if(Arr[i]+Arr[j] == sum)
return make_pair(Arr[i] , Arr[j]);
if(Arr[i]+Arr[j] > sum)
j--;
else
i++;
}
return make_pair(-1,-1);
}
int main()
{
int Arr[9] = {2,5,6,8,1,9,50,33,12};
pair <int,int>p = findTwoNum(Arr , 9 , 45);
cout<<p.first<<" "<<p.second<<endl;
return 0;
}
扩展问题:
1.如果把这个问题中“两个数字”改成“三个数字”或“任意个数字”时,你的解是多少呢?
答:对于ai+aj+ak = sum,由于sum-ak有N个,而对于ai+aj=subSum这样的问题可以采用前面的方法三,双指针法可以在O(N)复杂度内得到相应的ai与aj,由于外循环从N个sum-ak中取一个,内循环采用双指针法。因此总复杂度O(NlogN)+O(N)*O(N) = O(N^2),代码如下:
//使用双指针法查找三个数之和是否存在,不存在返回-1,-1,-1
struct result{
result(int a,int b,int c):a1(a),a2(b),a3(c){}
result(){}
int a1;
int a2;
int a3;
};
result findTwoNum(int Arr[] , int arrLen ,int sum)
{
//---先快速排序O(NlogN)
fastSort(Arr , 0 , arrLen-1);
//---再对每个sum-ak指针移位找subSum-aj
//注意要排除已用过的ak
int subSum;
int low,high;
for(int i=0 ; i<arrLen ; i++){
subSum = sum-Arr[i];
low = 0;
high = arrLen-1;
while(low<high){
if(low == i)
low++;
if(high == i)
high--;
if(Arr[low]+Arr[high] == subSum)
return result(Arr[i],Arr[low],Arr[high]);
else if(Arr[low]+Arr[high] > subSum)
high--;
else
low++;
}
}
return result(-1,-1,-1);
}
int main()
{
int Arr[9] = {2,5,6,8,1,9,50,33,12};
result res;
res = findTwoNum(Arr , 9 , 13);
cout<<res.a1<<" "<<res.a2<<" "<<res.a3<<endl;
return 0;
}
2.如果完全相等的一堆数字找不到,能否找出和最接近解?
答:如果找不到结果,可以保存作差的结果,取最小的哪一堆就行了。