08查找满足条件的n个数

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

        题目:输入一个数组和一个数字,在数组中查找两个数,使得它们的和正好是输入的那个数字。要求时间复杂度是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(" "); 

                  /*

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

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

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

                          这与我们在文本编器中按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 ", 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记录从左向右看时的LISb[i]表示,以a[i]结尾的,从a[0]a[i]之间的LIS

       用数组c记录,从右向左看是的LISc[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 ", 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 ", 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 ", 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 ", 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 ", index);

         return len - maxlen + 1;

}

 

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

原文地址:https://www.cnblogs.com/gqtcgq/p/7247179.html