双指针常见用法2

来源:互联网 发布:jdbc连接数据库5个步骤 编辑:程序博客网 时间:2024/06/06 09:13

        本文是总结双指针用法的第二篇文章,文章内容主要转载自:http://www.cnblogs.com/titer1/archive/2012/03/30/2425824.html和http://blog.csdn.net/rein07/article/details/6741661及http://www.cnblogs.com/HappyAngel/archive/2011/02/07/1949762.html

       双指针应用之五:本质方法与上篇文章中双指针应用之一中的用法一致。

       例2:这题来自编程之美2.21只考加法的面试题,原题大致意思是写一个程序,对于一个32位整数,输出它所有可能的连续自然数之和的算式,要求是这些连续自然数之和要等于原数。例如3 = 2+1; 9 = 4+5,9 = 2+3+4等。

      这题有两种解法,其中一种便是双指针法,还有一种利用数学方法,简单来说是求出一个公式来。这里先说双指针的解法。

      这里需要一个转化,把求n中所有可能的连续自然数之和归约为在数组{1,2,3,...,n}中找所有连续子序列和等于n的问题。这里同样也是这样一个场景:对有序数组如何遍历来求得符合要求的数据集合?这时的双指针可以不是一头一尾了,而是两个都指向头部,这样可以以高效的顺序遍历我们要找的所有集合。初始设i=j=1,这里同样会出现三种情况:

      sum[i,j] == sum, 直接输出i到j的值,并把i+1,j+1,因为只是i+1肯定是不等的,因为和小了,同样j+1只会使和变大,所以两个都要往前加(注意这里指针不用考虑减小,因为这在以前就考虑过了)

      sum[i,j] < sum,说明偏小,那么提高j来使得和变大才有可能相等

      sum[i,j] > sum,说明偏大,那么提高i来使得和变小才有可能相等

      这样,代码就出来了:

#include<iostream>#include<vector>#include<stdlib.h>#include<time.h>using namespace std;void GetAnswer(int n){int i = 1;int j = 1;while (i <= n / 2 && j <= n){int sum = (j + i)*(j - i + 1) / 2;if (sum == n){for (int k = i; k <= j; k++)cout << k << " ";cout << endl;j++;i++;}else if (sum < n){//sum[i..j]<n,只能提高j以增大sumj++;}else//sum[i..j]>n,只能提高i以减小sumi++;}}int main() {srand((unsigned)time(NULL));int a = rand();//产生随机数acout << a << endl;GetAnswer(a);return 0;}
       这里的思路本质上和上篇文章中的双指针之应用一是相同的,区别在于应用一中的例子是用的头尾指针,这里用的是双头指针。当遇到有序数组或者归约到有序数组时,利用双指针遍历的方法是求得我们需要的数据集合的一种相对比较高效的方法。

       双指针应用之六:排除以减少解空间大小。

     相信这种方法大家都听过,但是实际使用的时候却时常忘了去考虑这种思维模式,我这里举的都是很巧妙的例子,也是我遇到的,感觉绝对值得把这种思考方法总结出来。

      例1 此种解法很值得一说的题目来自编程之美2.3 寻找发帖水王,大致意思是:

某论坛有一个“水王”,经常发帖,据说该“水王”发帖数目超过了帖子总数的一半,那么如何在id发帖列表中快速的查找到这一“水王”?

这道题非常好的体现了排除法的非凡效果,如果直接去求这个水王,方法也不少,例如按发帖数排序,但是这至少是O(nlogn)时间复杂度,实际上最好的算法却是尽可能的去减少解空间,把不可能的去除掉,留下的自然是要求的的解。对于这道题,就是每次去除两个不同id的发帖,由于默认水王发帖超过一半,那么去除任何两个不同id后仍然是超过一半的,why?可以这样想,开始满足的公式是 x>y/2,那么减去两个不同id后,最坏情况,这两个不同id中有一个是水王的id,则(x-1)/(y-2) > (y/2-1)/(y-2) > 1/2,即仍然是大于二分之一的, 所以可以不断的这样做直到最终剩下水王id为止。编程之美上给出了一个非常精妙的程序。

  总结下该程序的大致思想就是:假设每个ID都有可能是水王,那么在遍历时这个水王就要遇到一种挑战,可能自己的帖子数是会增加的,也可能是遇到挑战的,帖子数要减少的。这样遍历下来,只有水王的帖子增加的减去遇到挑战的帖子数会是大于0的。其他任何帖子假设为水王时都是禁不起挑战的。

  步骤:

  1. 可以假设帖子的第一个ID是次数最大的,用candidate记录,次数用nTimes记录。

  2. 遍历下一个ID,如果跟candidate一样,nTimes++,否则,遇到一个挑战,则nTimes--,如果nTimes == 0,下一步就要重复第一步了。

  3.遍历结束,nTimes>0的那个candidate就是水王ID,他是获胜者。

Type Find(Type* ID, int N)  {      Type candidate;      int nTimes, i;      for(i = nTimes = 0; i < N; i++)      {          if(nTimes == 0)          {               candidate = ID[i], nTimes = 1;          }          else          {              if(candidate == ID[i])                  nTimes++;              else                  nTimes--;            }        }      return candidate;  }  

  扩展问题:

     随着Tango的发展,管理员发现,“超级水王”没有了。统计结果表明,有3个发帖很多的ID,他们的发帖数目都超过了帖子总数目N的1/4。你能从发帖ID列表中快速找出他们的ID吗?

     这个扩展问题还是上题所述的”对手”问题,不过这次是三个ID同时应战,但是这三个ID之间并不对战。所以问题很快得到解决。

void Find(Type* ID, int N,Type candidate[3])  {      Type ID_NULL;//定义一个不存在的ID      int nTimes[3], i;      nTimes[0]=nTimes[1]=nTimes[2]=0;      candidate[0]=candidate[1]=candidate[2]=ID_NULL;      for(i = 0; i < N; i++)      {          if(ID[i]==candidate[0])          {               nTimes[0]++;          }          else if(ID[i]==candidate[1])          {               nTimes[1]++;          }          else if(ID[i]==candidate[2])          {               nTimes[2]++;          }          else if(nTimes[0]==0)          {               nTimes[0]=1;               candidate[0]=ID[i];          }          else if(nTimes[1]==0)          {               nTimes[1]=1;               candidate[1]=ID[i];          }          else if(nTimes[2]==0)          {               nTimes[2]=1;               candidate[2]=ID[i];          }          else          {               nTimes[0]--;               nTimes[1]--;               nTimes[2]--;           }      }      return;}  

      实际上这类思想的应用场景可以认为如果看到要求的东西占总体数量一半以上情况的时候,可以考虑排除法。当然还有其他情况,例如正面求解屡试不行的时候,也可以考虑这样的方法。

        双指针应用之七:蓄水池抽样求概率模型

   问题定义可以简化如下:在不知道文件总行数的情况下,如何从文件中随机的抽取一行?

  首先想到的是我们做过类似的题目吗?当然,在知道文件行数的情况下,我们可以很容易的用C运行库的rand函数随机的获得一个行数,从而随机的取出一行,但是,当前的情况是不知道行数,这样如何求呢?我们需要一个概念来帮助我们做出猜想,来使得对每一行取出的概率相等,也即随机。这个概念即蓄水池抽样(Reservoir Sampling)

  有了这个概念,我们便有了这样一个解决方案:定义取出的行号为choice,第一次直接以第一行作为取出行choice,而后第二次以二分之一概率决定是否用第二行替换choice,第三次以三分之一的概率决定是否以第三行替换choice……,以此类推,可用伪代码描述如下:

i = 0while more input lines           with probability 1.0/++i                   choice = this input lineprint choice
  证明如下:

   回顾这个问题,我们可以对其进行扩展,即如何从未知或者很大样本空间随机地取k个数?

   类比下即可得到答案,即先把前k个数放入蓄水池,对第k+1,我们以k/(k+1)概率决定是否要把它换入蓄水池,换入时随机的选取一个作为替换项,这样一直做下去,对于任意的样本空间n,对每个数的选取概率都为k/n。也就是说对每个数选取概率相等。

   伪代码如下:

Init : a reservoir with the size: kfor i= k+1 to N    M=random(1, i);    if( M < k)     SWAP the Mth value and ith valueend for 

  蓄水池抽样问题是一类问题,在这里总结一下,并由衷的感叹这种方法之巧妙,不过对于这种思想产生的源头还是发觉不够,如果能够知道为什么以及怎么样想到这个解决方法的,定会更加有意义。