★引子

  前天,俺在《俺的招聘经验[4]:通过笔试答题能看出啥?》一文,以"求质数"作为例子,介绍了一些考察应聘者的经验。由于本文没有政治敏感内容,顺便就转贴到俺在CSDN的镜像博客。
  昨天,某个CSDN网友在留言中写道:

老实说,这个程序并不好写,除非你背过这段代码
如果只在纸上让别人写程序,很多人都会出错
但是如果给一台电脑,大多数人都会把这个程序调试正确
出这个题目没啥意义
只能让别人觉得你出题水平低

  首先,这位网友看帖可能不太仔细。俺在文中已经专门强调过了,评判笔试答题,"思路和想法"远远比"对错"更重要,而他/她依然纠结于对错;其次,这位网友居然觉得这道题目没啥意义,这让俺情何以堪啊?!看来,有相当一部分网友完全没有领略到此中之奥妙啊!
  算了,俺今天就豁出去了,给大伙儿抖一抖这道题目的包袱。当然,抖包袱的后果就是:从今天开始,就得把"求质数"这道题从俺公司的笔试题中去掉,然后换上另外一道全然不同的。这么好的一道题要拿掉,真是于心不忍啊 :-(

★题目

  好,言归正传。下面俺就由浅入深,从各种角度来剖析这道题目的奥妙。
  为了避免被人指责为"玩文字游戏"(有些同学自己审题不细,却抱怨出题的人玩文字游戏),在介绍各种境界之前,再明确一下题意。
  前一个帖子已经介绍过,求质数可以有如下2种玩法。

◇需求1

请实现一个函数,对于给定的整型参数 N,该函数能够把自然数中,小于 N 的质数,从小到大打印出来。
比如,当 N = 10,则打印出
2 3 5 7

◇需求2

请实现一个函数,对于给定的整型参数 N,该函数能够从小到大,依次打印出自然数中最小的 N 个质数。
比如,当 N = 10,则打印出
2 3 5 7 11 13 17 19 23 29

★试除法

  首先要介绍的,当然非"试除法"莫属啦。考虑到有些读者是菜鸟,稍微解释一下。
  "试除",顾名思义,就是不断地尝试能否整除。比如要判断自然数 x 是否质数,就不断尝试小于 x 且大于1的自然数,只要有一个能整除,则 x 是合数;否则,x 是质数。
  显然,试除法是最容易想到的思路。不客气地说,也是最平庸的思路。不过捏,这个最平庸的思路,居然也有好多种境界。大伙儿请看:

◇境界1

  在试除法中,最最土的做法,就是:
  假设要判断 x 是否为质数,就从 2 一直尝试到 x-1。这种做法,其效率应该是最差的。如果这道题目有10分,按照这种方式做出的代码,即便正确无误,俺也只给1分。

◇境界2

  稍微聪明一点点的程序猿,会想:x 如果有(除了自身以外的)质因数,那肯定会小于等于 x/2,所以捏,他们就从 2 一直尝试到 x/2 即可。
  这一下子就少了一半的工作量哦,但依然是很笨的办法。打分的话,即便代码正确也只有2分

◇境界3

  再稍微聪明一点的程序猿,会想了:除了2以外,所有可能的质因数都是奇数。所以,他们就先尝试 2,然后再尝试从 3 开始一直到 x/2 的所有奇数。
  这一下子,工作量又少了一半哦。但是,俺不得不说,依然很土。就算代码完全正确也只能得3分。

◇境界4

  比前3种程序猿更聪明的,就会发现:其实只要从 2 一直尝试到√x,就可以了。估计有些网友想不通了,为什么只要到√x 即可?
  简单解释一下:因数都是成对出现的。比如,100的因数有:1和100,2和50,4和25,5和20,10和10。看出来没有?成对的因数,其中一个必然小于等于100的开平方,另一个大于等于100的开平方。至于严密的数学证明,用小学数学知识就可以搞定,俺就不啰嗦了。

◇境界5

  那么,如果先尝试2,然后再针对 3 到√x 的所有奇数进行试除,是不是就足够优了捏?答案显然是否定的嘛?写到这里,才刚开始热身哦。
  一些更加聪明的程序猿,会发现一个问题:尝试从 3 到√x 的所有奇数,还是有些浪费。比如要判断101是否质数,101的根号取整后是10,那么,按照境界4,需要尝试的奇数分别是:3,5,7,9。但是你发现没有,对9的尝试是多余的。不能被3整除,必然不能被9整除......顺着这个思路走下去,这些程序猿就会发现:其实,只要尝试小于√x 的质数即可。而这些质数,恰好前面已经算出来了(是不是觉得很妙?)。
  所以,处于这种境界的程序猿,会把已经算出的质数,先保存起来,然后用于后续的试除,效率就大大提高了。
  顺便说一下,这就是算法理论中经常提到的:以空间换时间

◇补充说明

  开头的4种境界,基本上是依次递进的。不过,境界5跟境界4,是平级的。在俺考察过的应聘者中,有人想到了境界4但没有想到境界5;反之,也有人想到境界5但没想到境界4。通常,这两种境界只要能想到其中之一,俺会给5-7分;如果两种都想到了,俺会给8-10分。
  对于俺要招的"初级软件工程师"的岗位,能同时想到境界4和境界5,应该就可以了。如果你对自己要求不高,仅仅满足于浅尝辄止。那么,看到这儿,你就可以打住了,无需再看后续的内容;反之,如果你比较好奇或者希望再多学点东西,请接着往下看。

★筛法

  说完"试除法",再来说说筛法(维基百科的解释在"这里")。俺不妨揣测一下:本文的读者,应该有2/3以上,从来没有听说过筛法。所以捏,顺便跟大伙儿扯扯蛋,聊一下筛法的渊源。
  这个筛法啊,真的是一个既巧妙又快速的求质数方法。其发明人是公元前250年左右的一位希腊大牛——埃拉托斯特尼。为啥说他是大牛捏?因为他本人精通多个学科和领域,至少包括:数学、天文学、地理学(地理学这个词汇,就是他创立的)、历史学、文学(他是一个诗人)。真的堪称"跨领域的大牛"。
  他最让俺佩服的是:仅仅用简单的几何方法,测量出了地球的周长、地球与月亮的距离、地球与太阳的距离、赤道与黄道的夹角......而且,这些计算结果跟当代科学家测出的,相差无几。要知道他生活的年代,大概相当于中国的春秋战国。而咱们的老祖宗,一直到明朝还顽固地坚信:天是圆的、地是方的、月亮会被天狗给吃喽......
  好了,扯蛋完毕,言归正传。
  估计很多人把筛法仅仅看成是一种具体的方法。其实,筛法还是一种很普适的思想。在处理很多复杂问题的时候,都可以看到筛法的影子。那么,筛法如何求质数捏,说起来很简单:
  首先,2是公认最小的质数,所以,先把所有2的倍数去掉;然后剩下的那些大于2的数里面,最小的是3,所以3也是质数;然后把所有3的倍数都去掉,剩下的那些大于3的数里面,最小的是5,所以5也是质数......
  上述过程不断重复,就可以把某个范围内的合数全都除去(就像被筛子筛掉一样),剩下的就是质数了。维基百科上有一张很形象的动画,能直观地体现出筛法的工作过程。

  明白了"筛法"的原理,大伙儿应该看出,筛法在速度上是明显优于"试除法"的。当然,筛法的程序实现也分为不同的境界。而且,筛法可讲究的门道更多了。下面,俺分别从不同角度,聊一聊筛法都有哪些讲究。

◇如何确定质数的分布范围?

  这是采用筛法首先会碰到的问题。文本开头给出的那两种需求,其处理的方式完全不同,俺分别说一下。

需求1
  对于需求1,这个自然不是问题。因为在需求1中,质数的分布范围就是 N,已经给出了,很好办。

需求2
  但是对于需求2,就难办了。因为需求2给出的 N,表示需要打印的质数的个数,那么这 N 个质数会分布在多大的范围捏?这可是个头疼的问题啊。
  但是,来应聘的程序猿如果足够牛的话,当然不会被这个问题难倒。因为素数的分布,是有规律可循滴——这就是大名鼎鼎的素数定理
  稍微懂点数学的,应该知道素数的分布是越往后越稀疏。或者说,素数的密度是越来越低。而素数定理,说白了就是数学家找到了一些公式,用来估计某个范围内的素数,大概有几个。在这些公式中,最简洁的就是 x/ln(x),公式中的 ln 表示自然对数(估计很多同学已经忘了啥叫自然对数)。假设要估计1,000,000以内有多少质数,用该公式算出是72,382个,而实际有78,498个,误差约8个百分点。该公式的特点是:估算的范围越大,偏差率越小。
  有了素数定理,就可以根据要打印的质数个数,反推出这些质数分布在多大的范围内。因为这个质数分布公式有一定的误差(通常小于15%)。为了保险起见,把反推出的素数分布范围再稍微扩大15%,应该就足够了。

  可能有同学会质疑俺:谁有这么好的记性,能够在笔试过程中背出这些质数分布公式捏?
  俺觉得:背不出来是正常滴。但是,对于有一定数学功底的应聘者,假如他/她知道质数分布公式,即便不能完整写出来,只要在答题中体现出:"此处通过质数分布公式推算范围",那么俺也是认可滴。
  再啰嗦一次:关键是看idea!

◇如何设计存储容器?

  知道了分布范围,接下来就得构造一个容器,来存储该范围内的所有自然数;然后在筛的过程中,把合数筛掉。那么,这个容器该如何设计捏?不同层次的程序猿,自然设计出来的容器也不同啦。

境界1
  照例先说说最土的搞法——直接构造一个整型的容器。在筛的过程中把发现的合数删除掉,最后容器中就只剩下质数了。
  为啥说这种搞法最土捏?
  首先,整型的容器,浪费内存空间。比方说,你用的是32位的C/C++或者是Java,那么每个 int 都至少用掉4个字节的内存。当 N 很大时,内存开销就成问题了。
  其次,当 N 很大时,频繁地对一个大的容器进行删除操作可能会导致频繁的内存分配和释放(具体取决于容器的实现方式);而频繁的内存分配/释放,会导致明显的CPU占用并可能造成内存碎片。

境界2
  为了避免境界1导致的弊端,更聪明的程序猿会构造一个定长的布尔型容器(通常用数组)。比方说,质数的分布范围是1,000,000,那么就构造一个包含1,000,000个布尔值的数组。然后把所有元素都初始化为 true。在筛的过程中,一旦发现某个自然数是合数,就以该自然数为下标,把对应的布尔值改为 false。
  全部筛完之后,遍历数组,找到那些值为 true 的元素,把他们的下标打印出来即可。
  此种境界的好处在于:其一,由于容器是定长的,运算过程中避免了频繁的内存分配/释放;其二,在某些语言中,布尔型占用的空间比整型要小。比如C++的 bool 仅用1字节
注:C++标准(ISO/IEC 14882)没有硬性规定 sizeof(bool)==1,但大多数编译器都实现为一字节。

境界3
  虽然境界2解决了境界1的弊端,但还是有很大的优化空间。有些程序猿会想出按位(bit)存储的思路。这其实是在境界2的基础上,优化了空间性能。俺觉得:C/C++出身的或者是玩过汇编语言的,比较容易往这方面想。
  以C++为例。一个bool占用1字节内存。而1个字节有8个比特,每个比特可以表示0或1。所以,当你使用按位存储的方式,一个字节可以拿来当8个布尔型使用。所以,达到此境界的程序猿,会构造一个定长的byte数组,数组的每个byte存储8个布尔值。空间性能相比境界2,提高8倍(对于C++而言)。如果某种语言使用4字节表示布尔型,那么境界3比境界2,空间利用率提高32倍。

★总结

  看到俺写"总结"二字,很多网友心想:总算看完了,知道该怎么求质数才是最优的了。
  其实,你们又错了,本文才写了不到一半。考虑到篇幅已经有点长,而且俺打了这么多字,也有点累了,暂时刹住话匣子,下次接着聊。
  希望看了今天这个介绍,大伙儿应该明白一个道理:山外有山、天外有天。每一个技术领域里面的每一个细小的分支,深究下去都有很多的门道与奥妙。在你深究的过程中,必然会学到很多东西。深究的过程也就是你能力提高的过程。
  本文后续的内容,会介绍刚才提到的按位存储法还有哪些缺陷,该如何解决。另外,还会介绍其它一些求质数的方法。

版权声明
本博客所有的原创文章,作者皆保留版权。转载必须包含本声明,保持本文完整,并以超链接形式注明作者编程随想和本文原始地址:
http://program-think.blogspot.com/2011/12/prime-algorithm-1.html

(转)求质数算法的N种境界[1] - 试除法和初级筛法的更多相关文章

  1. 求质数算法的N种境界[1] - 试除法和初级筛法

    ★引子 前天,俺在<俺的招聘经验[4]:通过笔试答题能看出啥?>一文,以"求质数"作为例子,介绍了一些考察应聘者的经验.由于本文没有政治敏感内容,顺便就转贴到俺在CSD ...

  2. 【转】求质数算法的N种境界

    原文地址:http://blog.csdn.net/program_think/article/details/7032600/ ★引子 前天,俺在<俺的招聘经验[4]:通过笔试答题能看出啥?& ...

  3. 算法之求质数(Java语言)

    质数(Prime number) 又称素数,指在的自然数中,除了1和该数自身外,无法被其他自然数整除的数(也可定义为只有1与该数本身两个因数的数). 算法原理 验证一个数字 n 是否为素数的一种简单但 ...

  4. 谈谈"求线段交点"的几种算法(js实现,完整版)

    "求线段交点"是一种非常基础的几何计算, 在很多游戏中都会被使用到. 下面我就现学现卖的把最近才学会的一些"求线段交点"的算法总结一下, 希望对大家有所帮助.  ...

  5. [经典算法] Eratosthenes筛选求质数

    题目说明: 除了自身之外,无法被其它整数整除的数称之为质数,要求质数很简单,但如何快速的求出质数则一直是程式设计人员与数学家努力的课题,在这边介绍一个著名的 Eratosthenes求质数方法. 题目 ...

  6. 【转】从框架看PHP的五种境界及各自的薪资待遇

    无意中看到这篇文章,有些触动,作为博客开篇,用来激励自己. 原文地址:点击打开 在撰写此文前首先必须申明的是本人不鄙视任何一种框架,也无意于挑起PHP框架间的战争,更没有贬低某个框架使用者的用意,本文 ...

  7. 转:从框架看PHP的五种境界及各自的薪资待遇(仅限于二三线城市,一线除外)

    在撰写此文前首先必须申明的是本人不鄙视任何一种框架,也无意于挑起PHP框架间的战争,更没有贬低某个框架使用者的用意,本文纯粹个人的看法.你可以认为我无知也好,或者装逼也好,请不要试着在任何情况下,随便 ...

  8. 求1+2+…+n,要求不能使用乘除法、for、while、if、else、s witch、case 等关键字以及条件判断语句(A?B:C)和不用循环/goto/递归输出1~100的10种写法

    来源:据说是某一年某个公司的面试题 题目:求1+2+…+n, 要求不能使用乘除法.for.while.if.else.s witch.case 等关键字以及条件判断语句(A?B:C) 分析:这题本来很 ...

  9. 一步一步实现基于GPU的pathtracer(二):求交算法

    不管是哪种全局光照算法,最根本的都要落实到光线与物体的求交.主要分为光线与参数曲面和非参数曲面的求交,典型的参数曲面有球.盒.圆柱等基本体及基本体的组合体,以及一些更为复杂的参数曲面.非参数曲面就是所 ...

随机推荐

  1. 安装显卡后蓝屏0x00000116解决办法

    1. 亲自遇到蓝屏的问题. 2. 进入安全模式后重装系统,成功. 3. 安装驱动,软件没毛病.安装显卡驱动后,蓝屏. 4. 查看蓝屏错误代码0x00000116, 百度后查看到是显卡驱动的问题. 5. ...

  2. Tomcat 7下如何利用 catalina.properties 部署公用类

    Tomcat 有很多配置文件,其中一个是  catalina.properties ,本文介绍catalina.properties 中的设置项. 一.组成   catalina.properties ...

  3. SpringBoot jar包不支持jsp

    官方原文如下: When running a Spring Boot application that uses an embedded servlet container (and is packa ...

  4. [LA3135]node形式的优先队列

    n个触发器,每个触发器每period秒就产生一个编号为qnum的事件,求前k个事件. n<=1000  k<=10000 node形式的优先队列 主要在于重载小于号,确定优先顺序. #in ...

  5. JS语句循环(100以内奇偶数、100以内与7先关的数、100以内整数的和、10以内阶乘、乘法口诀、篮球弹起高度、64格子放东西)

    3.循环 循环是操作某一个功能(执行某段代码). ①循环四要素: a 循环初始值 b 循环的条件 c 循环状态 d 循环体 ②for循环 a 穷举:把所有的可能性的都一一列出来. b 迭代:每次循环都 ...

  6. TensorFlow非线性拟合

    1.心得: 在使用TensorFlow做非线性拟合的时候注意的一点就是输出层不能使用激活函数,这样就会把整个区间映射到激活函数的值域范围内无法收敛. # coding:utf-8 import ten ...

  7. bzoj 1050 并查集

    先按边长排序,假设s与t连通,那么我们可以枚举s与t的路径中最短的一条边,通过类似与kruskal的方法找到s与t的路径在当前最小边权情况下尽量小的最大边权,用这个比值更新答案. 特别的,我们对于某一 ...

  8. 浅谈C语言中的强符号、弱符号、强引用和弱引用【转】

    转自:http://www.jb51.net/article/56924.htm 首先我表示很悲剧,在看<程序员的自我修养--链接.装载与库>之前我竟不知道C有强符号.弱符号.强引用和弱引 ...

  9. device tree source file position

    android/kernel/msm-4.9/arch/arm64/boot/dts/qcom/

  10. ftrace 的使用【转】

    转自:http://blog.csdn.net/wang6077160/article/details/7814279 ftrace 的使用 ftrace 在内核态工作,用户通过 debugfs 接口 ...