第一节、寻找和为定值的两个数

题目:输入一个数组和一个数字,在数组中查找两个数,使得它们的和正好是输入的那个数字。要求时间复杂度是O(n)。如果有多对数字的和等于输入的数字,输出任意一对即可。

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

思路如下:

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

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

答案是二分查找。二分查找的时间复杂度为O(lg n) ,如果原数组有序,直接二分查找,n个数的总时间为O(n lg n) ;如果原数组无序,则需要先排序后二分,复杂度同样为O(nlgn + nlgn) =O(nlgn) ,空间复杂度为O(1) 。

3:有没有更好的办法呢?可以依据上述思路2的思想,进一步缩小查找时间。如果数组无序,则先在O(nlgn) 时间内排序。

举个例子,如下:

原始序列:1、 2、 4、 7、11、15

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

对应序列:14、13、11、8、4、0

第一个数组以一指针i 从数组最左端开始向右扫描,第二个数组以一指针j 从数组最右端开始向左扫描,谁指的元素小,谁先移动,如果a[*i]=a[*j],就找出这俩个数来了。两端同时查找,时间复杂度瞬间缩短到了O(n),但却同时需要O(n)的空间存储第二个数组。

因此,如果原数组无序,则需要时间O(n +nlgn)
=O(nlgn) ;如果原数组有序,则需要时间O(n)

4:针对上述思路进一步改进,使空间复杂度由O(n)变为O(1)。如果数组是无序的,则先在O(nlgn) 时间内排序,然后用两个指针 i,j,各自指向数组的首尾两端,令i=0,j=n-1,逐次判断a[i]+a[j]?=sum,如果a[i]+a[j]>sum,则要想办法让a[i]+a[j]的值减小,所以此刻i不动,j--,如果某一刻a[i]+a[j]<sum,则要想办法让a[i]+a[j]的值增大,所以此
刻i++,j不动。所以,数组无序的时候,时间复杂度最终为O(n +nlgn) =O(nlgn),若原数组是有序的,则不需要事先的排序,直接O(n)搞定,且空间复杂度还是O(1)
,此思路是相对于上述所有思路的一种改进。

5:还可以构造hash表,正如编程之美上的所述,给定一个数字,根据hash映射查找另一个数字是否也在数组中,只需用O(1) 的时间,这样的话,总体的算法通上述思路3 一样,也能降到O(n)
,但有个缺陷,就是构造hash额外增加了O(n)
的空间,不过,空间换时间,仍不失为在时间要求较严格的情况下的一种好办法。

所以,要想达到时间O(n)
,空间O(1)
的目标,除非原数组是有序的(指针扫描法),不然,当数组无序的话,就只能先排序,后指针扫描法或二分查找(时间O(nlgn)
,空间O(1)
),或映射或hash(时间O(n) ,空间O(n)
)。时间或空间,必须牺牲一个。

二:二分查找

“二分查找可以解决已排序数组的查找问题:只要数组中包含T(即要查找的值),那么通过不断缩小包含T的范围,最终就可以找到它。一开始,范围覆盖整个数组。将数组的中间项与T进行比较,可以排除一半元素,范围缩小一半。就这样反复比较,反复缩小范围,最终就会在数组中找到T,或者确定原以为T所在的范围实际为空。对于包含N个元素的表,整个查找过程大约要经过logN次比较。

多数程序员都觉得只要理解了上面的描述,写出代码就不难了;但事实并非如此。我在贝尔实验室和IBM的时候都出过这道考题。那些专业的程序员有几个小时的时间,可以用他们选择的语言把上面的描述写出来。考试结束后,差不多所有程序员都认为自己写出了正确的程序。于是,我们花了半个钟头来看他们编写的代码经过测试用例验证的结果。几次课,一百多人的结果相差无几:90%的程序员写的程序中有bug(我并不认为没有bug的代码就正确)。

我很惊讶:在足够的时间内,只有大约10%的专业程序员可以把这个小程序写对。但写不对这个小程序的还不止这些人:高德纳在《计算机程序设计的艺术 第3卷 排序和查找》第6.2.1节的“历史与参考文献”部分指出,虽然早在1946年就有人将二分查找的方法公诸于世,但直到1962年才有人写出没有bug的二分查找程序。”——乔恩·本特利,《编程珠玑(第1版)》第35-36页。

二分查找算法的边界,一般来说分两种情况,一种是左闭右开区间,类似于[left, right),一种是左闭右闭区间,类似于[left, right].需要注意的是, 循环体外的初始化条件,与循环体内的迭代步骤, 都必须遵守一致的区间规则,也就是说,如果循环体外初始化时,是以左闭右开区间为边界的,那么循环体内部的迭代也应该如此.如果两者不一致,会造成程序的错误.比如下面就是错误的二分查找算法:

left = 0, right = n;

while (left < right)

{

middle = (left + right) / 2;

if (array[middle] > v)

{

right = middle - 1;

}

else if (array[middle] < v)

{

left = middle + 1;

}

else

{

return middle;

}

}

这个算法的错误在于, 在循环初始化的时候,初始化right=n,也就是采用的是左闭右开区间,而当满足array[middle]> v的条件是, v如果存在的话应该在[left,middle)区间中,但是这里却把right赋值为middle - 1了,这样,如果恰巧middle-1就是查找的元素,而且middle-1=left,那么就会找不到这个元素。

下面给出两个算法, 分别是正确的左闭右闭和左闭右开区间算法,可以与上面的进行比较:

int search2(int array[], int n, int v)

{

int left, right, middle;

left = 0, right = n - 1;

while (left <= right)

{

middle = (left + right) / 2;

if (array[middle] > v)

{

right = middle - 1;

}

else if (array[middle] < v)

{

left = middle + 1;

}

else

{

return middle;

}

}

return -1;

}

int search3(int array[], int n, int v)

{

int left, right, middle;

left = 0, right = n;

while (left < right)

{

middle = (left + right) / 2;

if (array[middle] > v)

{

right = middle;

}

else if (array[middle] < v)

{

left = middle + 1;

}

else

{

return middle;

}

}

return -1;

}

另外:在循环体内,计算中间位置的时候,使用的是这个表达式:

middle = (left + right) / 2;

假如,left与right之和超过了所在类型的表示范围的话,那么middle就不会得到正确的值.所以,更稳妥的做法应该是这样的:

middle = left + (right - left) / 2;

(http://www.cppblog.com/converse/archive/2009/10/05/97905.html)

三:编程求解

输入两个整数 n 和 m,从数列1,2,3.......n 中 随意取几个数,使其和等于 m ,要求将其中所有的可能组合列出来。

分析:用f(m, n)表示从1到n的序列中,和等于m的所有可能的组合,分几种情况:

1:如果m<n,则m+1, m+2,..n这些数不可能出现在任何组合中,所以:

f(m, n) = f(m, m)。

2:如果m=n,则单独的n是组合之一,所以f(m, n) = 。

3:如果m>n,则可以根据组合中是否包含n进行区分,所以:

f(m, n) =

所以,该题目的解法可以用递归实现,考虑到要保存中间结果以便打印出最终所有的组合,所以可以借用栈实现,下面的代码直接使用list来模拟栈的操作:

void findsum(int sum, intn)

{

static list<int> indexStack;

if(sum < 1)return;

if(n < 1)return;

if(sum < n)

{

return findsum(sum, sum);

}

else if(sum == n)

{

cout << sum <<"+";

for(list<int>::iteratoriter = indexStack.begin();iter!=indexStack.end();++iter)

{

cout<<*iter<<"+";

}

printf("\b \n");

/*

\b是退格符,显示的时候是将光标退回前一个字符,

但不会删除光标位置的字符,

如果后边有新的字符,将覆盖退回的那个字符,

这与我们在文本编器中按Backspace的效果不一样。  
         */

return findsum(sum, sum-1);

}

else

{

indexStack.push_front(n);

findsum(sum-n, n-1);

indexStack.pop_front();

findsum(sum, n-1);

}

}

四:最长递增子序列(LongestIncreasing Subsequence,LIS问题)

给定一个长度为n的数组,找出一个最长的单调递增子序列(不一定连续)。 更正式的定义是:

设L=<a1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<ak1,ak2,…,akm>,其中k1<k2<…<km且ak1<ak2<…<akm。求最大的m值。

比如数组A 为{10, 11, 12, 13, 1,2, 3, 15},那么最长递增子序列为{10,11,12,13,15}。

1:动态规划(http://blog.chinaunix.net/uid-26548237-id-3757779.html)

对于数组a[n], 假设LIS[i]表示:以a[i]为结尾的,最长递增子序列的长度。

初始情况下,LIS[i]=1,因为最长递增子序列只包含a[i];

如果对于j,0<=j<i,假设LIS[j]已经求解出来了,则可以根据LIS[j]来计算LIS[i]:对于所有j(0<=j<i),如果a[i]>a[j],则LIS[j] + 1的最大值。也就是:

LIS[i]= 。所以,代码如下:

for(i = 0; i<len; i++)

{

LIS[i] = 1;

}

for(i = 0; i < len; i++)

{

for(j = 0; j < i; j++)

{

if(a[i] > a[j] &&LIS[i]<LIS[j]+1)

{

LIS[i] = LIS[j]+1;

}

}

}

max = LIS[0];

for(i = 1; i < len; i++)

{

printf("the LIS[%d] is %d\n", i, LIS[i]);

if(max < LIS[i])

{

max = LIS[i];

}

}

2:二分查找法(http://www.felix021.com/blog/read.php?1587)

   假设存在一个序列d[1..9]= 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。

下面一步一步试着找出它。

我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。B[i]中存储的是:对于长度为i的递增子序列的中的最大元素,其中的最小值。也就是长度为i的递增子序列的最小末尾。

定义LEN记录数组B中,已经得到的LIS的值。

首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1

然后,读取d[2],因d[2] < B[1],所以令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1

接着,d[3]= 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2]= 1, 5,Len=2

再来,d[4]= 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2]= 1, 3,Len =2

继续,d[5]= 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3]= 6, 这时B[1..3]= 1, 3, 6,还是很容易理解吧? Len= 3 了噢。

第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3]= 4。B[1..3] = 1, 3, 4, Len继续等于3

第7个, d[7] = 8,它很大,比4大,嗯。于是B[4]= 8。Len变成4了

第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。

最后一个,d[9] = 7,它在B[3]= 4和B[4] = 8之间,所以我们知道,最新的B[4]=7,B[1..5] = 1, 3,4, 7, 9,Len =5。

于是我们知道了LIS的长度为5。之所以B数组中记录递增子序列的最小末尾,是为了遍历后续数组的时候,尽可能的增加递增子序列的长度。比如某一时刻B[1]= 1, B[2]=5,说明目前的长度为2的递增子序列中,最小末尾是5,之后碰到6,7,8,...等数的时候才会增加递增子序列的长度。如果碰见了一个数3,因3<5,所以可以用3替换5:B[2]=3。这样比如后续碰到了4,则就能增加递增子序列的长度了。

然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(nlogn)~!代码如下:

int BIN_LIS(int *a, intlen)

{

int *b = malloc((len+2) * sizeof(int));

int i;

int max = 0;

int left, right, mid;

b[1] = a[0];

max = 1;

for(i = 1; i < len; i++)

{

if(a[i] > b[max])

{

max++;

b[max] = a[i];

continue;

}

left = 1;

right = max;

while(left <= right)

{      
//注意下面的边界条件

mid = left + (right -left)/2;

if(b[mid] < a[i])

{

left = mid +1;

}

else

{

right = mid -1;

}

}

b[left]= a[i];

}

return max;

}

五:双端LIS问题

问题描述:从一列数中筛除尽可能少的数使得从左往右看,这些数是从小到大再从大到小的。

思路:这是一个典型的双端LIS问题。

对于数组a,用数组b记录从左向右看时的LIS,b[i]表示,以a[i]结尾的,从a[0]到a[i]之间的LIS。

用数组c记录,从右向左看是的LIS,c[i]表示,以a[i]结尾的,从a[n-1]到a[i]之间的LIS。

所以,b[i] + c[i] - 1就表示以i为“顶点”的,满足题目要求的最长序列。所以n-max{b[i] + c[i] -1}就是题目的解。

这个问题同样需要辅助数组LIS, LIS[i]中存储的是:对于长度为i的递增子序列的中的最大元素,其中的最小值。也就是递增子序列的最小末尾。每当扫描到数组元素a[i]时,同LIS中的二分查找一样,需要找到在LIS中合适的位置,这个位置,也就是b[i]或者c[i]的值。

代码如下(自己写的,JULY的程序有问题):

int Double_LIS(int *a,int len)

{

int *Lis = malloc((len+2) * sizeof(int));

int *b = malloc(len * sizeof(int));

int *c = malloc(len * sizeof(int));

int i;

int max = 0;

int left, right, mid;

int maxlen = 0;

int index;

Lis[1] = a[0];

max = 1;

b[0] = max;

for(i = 1; i < len; i++)

{

if(a[i] > Lis[max])

{

max++;

Lis[max] = a[i];

b[i] = max;

printf("b[%d] is %d\n", i, b[i]);

continue;

}

left = 1;

right = max;

while(left <= right)

{

mid = left + (right - left)/2;

if(Lis[mid] < a[i])

{

left = mid + 1;

}

else

{

right = mid - 1;

}

}

Lis[left] = a[i];

b[i] = left;

printf("b[%d] is %d\n", i, b[i]);

}

memset(Lis, 0, (len+2) * sizeof(int));

Lis[1] = a[len-1];

max = 1;

c[len-1] = max;

for(i = len - 2; i >= 0; i--)

{

if(a[i] > Lis[max])

{

max++;

Lis[max] = a[i];

c[i] = max;

printf("c[%d] is %d\n", i, c[i]);

continue;

}

left = 1;

right = max;

while(left <= right)

{

mid = left + (right - left)/2;

if(Lis[mid] < a[i])

{

left = mid + 1;

}

else

{

right = mid - 1;

}

}

Lis[left] = a[i];

c[i] = left;

printf("c[%d] is %d\n", i, c[i]);

}

maxlen = 0;

for(i = 0; i < len; i++)

{

if(b[i]+c[i] > maxlen)

{

maxlen = b[i]+c[i];

index = i;

}

}

printf("index is %d\n", index);

return len - maxlen + 1;

}

(http://blog.csdn.net/v_JULY_v/article/details/6419466)

08查找满足条件的n个数的更多相关文章

  1. Linux 在文档中查找满足条件的行并输出到文件:

    Linux 在文档中查找满足条件的行并输出到文件: 文件名称: dlog.log    输出文件: out.log 1.满足一个条件(包含  “TJ”  )的语句: grep  “TJ”  dlog. ...

  2. Java8 使用 stream().filter()过滤List对象(查找符合条件的对象集合)

    内容简介 本文主要说明在Java8及以上版本中,使用stream().filter()来过滤一个List对象,查找符合条件的对象集合. List对象类(StudentInfo) public clas ...

  3. 【转载】 C#中List集合使用First方法查找符合条件的第一个元素

    在C#的List集合相关操作中,很多时候需要从List集合中查找出符合条件的第一个元素对象,如果确认在List集合中一定存在符合条件的元素,则可以使用First方法来查找,First方法调用格式为Fi ...

  4. 查找2-n之间素数的个数

    题目描述 查找2-n之间素数的个数.n为用户输入值.素数:一个大于1的正整数,如果除了1和它本身以外,不能被其他正整数整除,就叫素数.如2,3,5,7,11,13,17…. 输入 整数n 输出 2-n ...

  5. R语言-查找满足条件的数并获取索引

    1.在R语言中,怎样找到满足条件的数呢? 比如给定一个向量c2.要求找到数值大于0的数: > c2  [1] 0.00 0.00 0.00 0.00 0.00 0.00 0.06 0.09 0. ...

  6. 划分树---hdu4417---区间查找(不)大于h的个数

    http://acm.hdu.edu.cn/showproblem.php?pid=4417 Super Mario Time Limit: 2000/1000 MS (Java/Others)    ...

  7. linux查找符合条件的文件并删除

    找到根目录下所有的以test开头的文件并把查找结果当做参数传给rm -rf命令进行删除: 1.find / -name “test*” |xargs rm -rf 2.find / -name “te ...

  8. JavaScript 数组中查找符合条件的值

    数组实例的find方法,用于找出第一个符合条件的数组成员.它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员.如果没有符合条件的成员,则返回u ...

  9. python算法 - 快速寻找满足条件的两个数-乾颐堂

    题目前提是一定存在这样两个数 解法一就不写了...一般想不到吧 一开始想到的是解法二最后的用hash表 (其实是想到创建一个跟target一样大的数组啦..存在就写入index,但是要全部找出,那得二 ...

随机推荐

  1. 基础篇-1.5Java的数组

    1 引言 每一种编程语言都有其自身的数组概念,大同小异,都是为了存储一堆数据,而Java的数组是用来存储相同类型的数据,如声明一个arr[10]数组,可以用来代替声明10个变量. 2 声明和创建数组 ...

  2. MySQL数据库的全局锁和表锁

    1.概念 数据库锁设计的初衷是处理并发问题.作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则.而锁就是用来实现这些访问规则的重要数据结构. 2.锁的分类 根据加锁的范围, ...

  3. OSGi教程:Class Space Consistency

    此教程基于OSGi Core Release 7 OSGi类空间的一致性 详细内容上面英文教程有详细解答 下面主要是一些个人见解,若有不当之处,欢迎指出: "Class space cons ...

  4. Calendar to julian date format

    1.JULIAN DATE 定义 2.示例: 定义枚举: public enum JulianDateType    {        /// <summary>        /// J ...

  5. python进程间通信 queue pipe

    python进程间通信 1 python提供了多种进程通信的方式,主要Queue和Pipe这两种方式,Queue用于多个进程间实现通信,Pipe是两个进程的通信 1.1 Queue有两个方法: Put ...

  6. Bigdecimal 相加结果为0的解决

    之前很少使用这样的一个对象BigDecimal,今天在改需求的时候遇到了,结果坑爹的怎么相加最后都为零. 代码如下: BigDecimal totalAmount = new BigDecimal(0 ...

  7. oracle 写存储过程有返回值时 注意在loop循环处添加返回值:=

    例子: create or replace procedure p_xl is v_count NUMBER(10); begin for rs in(select yhbh from dbyh) l ...

  8. 【转载】【软件安装】Source Insight 4.0常用设置

    1.Source Insight简介 Source Insight是一个面向软件开发的代码编辑器和浏览器,它拥有内置的对C/C++, C#和Java等源码的分析,创建并动态维护符号数据库,并自动显示有 ...

  9. php-imagick扩展

    介绍 magick 是用 ImageMagic API 来创建和修改图像的PHP官方扩展.ImageMagick® 是用来创建,编辑,合并位图图像的一套组件. 它能够用于读取,转换,写入多种不同格式的 ...

  10. 七.Deque的应用案例-回文检查

    - 回文检测:设计程序,检测一个字符串是否为回文. - 回文:回文是一个字符串,读取首尾相同的字符,例如,radar toot madam. - 分析:该问题的解决方案将使用 deque 来存储字符串 ...