您的位置:首页 > 理论基础 > 数据结构算法

数据结构学习笔记6-寻找和为定值的两个数(二分查找)

2015-08-20 11:20 357 查看
本文参考了文章《寻找和为定值的两个数》

寻找和为定值的两个数

题目描述

输入一个数组和一个数字,在数组中查找两个数,使得它们的和正好是输入的那个数字。

要求时间复杂度是O(N)。如果有多对数字的和等于输入的数字,输出任意一对即可。

例如输入数组1、2、4、7、11、15和数字15。由于4+11=15,因此输出4和11。

分析与解法

咱们试着一步一步解决这个问题(注意阐述中数列有序无序的区别):

直接穷举,从数组中任意选取两个数,判定它们的和是否为输入的那个数字。此举复杂度为O(N^2)。很显然,我们要寻找效率更高的解法

题目相当于,对每个a[i],查找sum-a[i]是否也在原始序列中,每一次要查找的时间都要花费为O(N),这样下来,最终找到两个数还是需要O(N^2)的复杂度。那如何提高查找判断的速度呢?

答案是二分查找,可以将O(N)的查找时间提高到O(log N),这样对于N个a[i],都要花logN的时间去查找相对应的sum-a[i]是否在原始序列中,总的时间复杂度已降为O(N log N),且空间复杂度为O(1)。

(如果有序,直接二分O(N log N),如果无序,先排序后二分,复杂度同样为O(N log N + N log N)= O(N log N),空间复杂度总为O(1))

二分查找

二分查找完整实现代码:

#include<stdio.h>
#include<stdlib.h>
int find_num(int *k,int strlen,int value)//二分查找实现函数
{
int left=0;
int right=strlen-1;//注意,这里有strlen-1和strlen-1的区别
int middle;
while(left<right)
{
middle=left+((right-left)>>1);//还有另外一种写法:middle= (left+right)>>1; 这样的话left与right的值比较大的时候,其和可能溢出。
if(k[middle]>value)
right=middle-1;
else if(k[middle]<value)
left=middle+1;
else
return middle;
}
return -1;//代表异常,没找到结果
}

int main()
{
int k[]={1,2,4,7,11,15};//假设序列已经有序了
int strlen=sizeof(k)/sizeof(int);
int value=15;
int *p=(int *)malloc(sizeof(k));
for(int i=0;i<strlen;i++)
{
*(p+i)=value-k[i];//求出15-k[]对应的数组
}

for(int j=0;j<strlen;j++)
{
int findmiddle;
findmiddle=find_num(k,strlen,*(p+j));//轮询k,看p中的每一个元素是否在k中会出现
if(findmiddle!=-1)
{
printf("%d %d\n",k[findmiddle],k[strlen-findmiddle]);
break;
}

}
return 0;
}


可以继续优化做到时间O(N)么?

解法一

根据前面的分析,a[i]在序列中,如果a[i]+a[k]=sum的话,那么sum-a[i](a[k])也必然在序列中。

举个例子,如下:

原始序列:

- 1、 2、 4、 7、11、15

用输入数字15减一下各个数,得到对应的序列为:

- 14、13、11、8、4、 0

第一个数组以一指针i 从数组最左端开始向右扫描,第二个数组以一指针j 从数组最右端开始向左扫描,如果第一个数组出现了和第二个数组一样的数,即a[*i]=a[*j],就找出这俩个数来了。

如上,i,j最终在第一个,和第二个序列中找到了相同的数4和11,所以符合条件的两个数,即为4+11=15。

怎么样,两端同时查找,时间复杂度瞬间缩短到了O(N),但却同时需要O(N)的空间存储第二个数组。

思路:(参考《程序员编程艺术:第五章、寻找满足和为定值的两个或多个数》

要达到O(N)的复杂度,第一个数组以一指针i 从数组最左端开始向右扫描,第二个数组以一指针j 从数组最右端开始向左扫描,首先初始i指向元素1,j指向元素0,谁指的元素小,谁先移动,由于1(i)>0(j),所以i不动,j向左移动。然后j移动到元素4发现大于元素1,故而停止移动j,开始移动i,直到i指向4,这时,i指向的元素与j指向的元素相等,故而判断4是满足条件的第一个数;然后同时移动i,j再进行判断,直到它们到达边界。

参考代码如下:

#include<stdio.h>
#include<stdlib.h>

int main()
{
int k[]={1,2,4,7,11,15};
int num=15;
int strlen=sizeof(k)/sizeof(int);
int *p=(int *)malloc(sizeof(k));
for(int i=0;i<strlen;i++)
{
*(p+i)=num-k[i];
}

int j=0;
i=0;
while(i<strlen-1 && j<strlen-1)
{
if(k[i]>*(p+strlen-j-1))
j++;
else if(k[i]<*(p+strlen-j-1))
i++;
else
{
printf("%d %d\n",k[i],p[i]);
break;
}
}
free(p);
return 0;

}


解法二

当题目对时间复杂度要求比较严格时,我们可以考虑下用空间换时间,上述解法一即是此思想,此外,构造hash表也是典型的用空间换时间的处理办法。

即给定一个数字,根据hash映射查找另一个数字是否也在数组中,只需用O(1)的时间,前提是经过O(N)时间的预处理,和用O(N)的空间构造hash表。

但能否做到在时间复杂度为O(N)的情况下,空间复杂度能进一步降低达到O(1)呢?

解法三

如果数组是无序的,先排序(N log N),然后用两个指针i,j,各自指向数组的首尾两端,令i=0,j=n-1,然后i++,j–,逐次判断a[i]+a[j]?=sum,

如果某一刻a[i]+a[j] > sum,则要想办法让sum的值减小,所以此刻i不动,j–;

如果某一刻a[i]+a[j] < sum,则要想办法让sum的值增大,所以此刻i++,j不动。

所以,数组无序的时候,时间复杂度最终为O(N log N + N)=O(N log N)。

如果原数组是有序的,则不需要事先的排序,直接用两指针分别从头和尾向中间扫描,O(N)搞定,且空间复杂度还是O(1)。

代码参考如下:

#include<stdio.h>
#include<stdlib.h>

void findnum(int *p,int strlen,int num)
{
int i=0,j=strlen-1;
while(i<strlen-1 && j>0)
{
if(*(p+i)+*(p+j)>num)
j--;
else if(*(p+i)+*(p+j)<num)
i++;
else
{
printf("%d %d\n",*(p+i),*(p+j));

break;
}
}
}

int main()
{
int k[]={1,2,4,7,11,15};
int num=15;
int strlen=sizeof(k)/sizeof(int);
findnum(k,strlen,num);
return 0;

}


参考网址:

《寻找和为定值的两个数》

《程序员编程艺术:第五章、寻找满足和为定值的两个或多个数》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: