这是一篇因骂战而起的博文,GarbageMan 在该文章回复中不仅对我进行了侮辱,还涉及了我的母校,特写此文用理性的分析和实验予以回击。

在此也劝告 GarbageMan,没什么本事就别在那叫嚣了,还写什么《C语言初学者代码中的常见错误与瑕疵》,误人子弟。

完整的实验代码点这里下载。使用方法见实验环境一节。

本文需要一些基本的数论知识。本人对于数论没有详细而深入的研究,部分表述有可能不严谨或不正确,如有发现,还请指正。

预备知识

素数,又称质数,指除了 1 和该整数自身外,无法被其他正整数整除的正整数。

素数定理表明,从不大于 n 的自然数随机选一个,它是素数的概率大约是 1/ln n 。

Eratosthenes 筛法。给出要筛数值的范围n,找出以内的素数。先用2去筛,即把2留下,把2的倍数剔除掉;再用下一个素数,也就是3筛,把3留下,把3的倍数剔除掉;接下去用下一个素数5筛,把5留下,把5的倍数剔除掉;不断重复下去......。

试除法。尝试从到的整数是否整除。

问题描述

GarbageMan 给出的问题原题在这里。在此对问题的本质进行简述:

任意给定一个正整数 n,求与 n 的差的绝对值最小的素数。若 n 是一个素数,则输出 n 自身;若有两个素数符合条件,则输出其中较大的一个。

根据我和 GarbageMan 在留言中的讨论,还要补充以下几点:

  1. 该题有两种模式,一种是只考虑一次问答的情况,另一种是考虑连续问答。
  2. 题目中会事先给定 n 的取值范围。

GarbageMan 所用算法分析

GarbageMan 在这篇文章中所用的算法本质上是试除法的一个变种,其改变有两点:

  1. 维持一个从到的素数表,在验证一个正整数是否是素数时,只需用素数表中的数进行验证即可,而无需使用从到的所有整数。
  2. 延迟计算。实现并不计算素数表,而是在用到的时候,按需进行计算。这样在单次问答中就不需要准备整张素数表,而只需要计算需要的部分即可。

首先肯定一下,GarbageMan 的思路是正确的:在给定的 n 附近由近及远的进行素数判定,直到找到一个素数为止。

那么我为什么会和 GarbageMan 在留言中吵起来,以至于 GarbageMan 对我进行人身攻击呢?这是因为 GarbageMan 在这个算法中使用延迟计算的方法,将素数表的计算推迟到提问之后按需进行计算。这一优化本来无可厚非,但是问题就出在他这篇文章的副标题是“C语言初学者 代码中的常见错误与瑕疵”。我认为,这样的优化意义不大,并且凭空增加了许多代码复杂性。另一方面,尽管作者声明了“本文讨论的并不是初学者代码中的常见 错误与瑕疵,而是对我自己代码的改进和优化”,但是顶着这样的标题写这样的内容,也有些欠妥。下面分两种情况讨论更好、更常见、代码更简洁易懂的方案。

改进方案分析

该问题有两种模式,一种是只考虑一次问答的情况,另一种是考虑连续问答。两种截然不同的模式会导致不同的算法设计。

如果只考虑一次问答的情况,Rabin-Miller 素数测试会成为一个更好的方案。因为 Rabin-Miller 素数测试可以被用于测试一个非常非常大的整数是否是一个素数,并且还不需要计算素数表,所以对于较大规模的数据,Rabin-Miller 素数测试比朴素的试除法效率更高。

如果考虑多次问答的情况,考虑平均情况,延迟计算最终也会计算出大部分的素数表。与其动态的不断补充素数表,还不如用更有效率的方法直接计算出整张素数表,所以对于这种情况,GarbageMan 的优化是毫无意义的。

下面将分别讨论上面提到的几种算法。

Rabin-Miller 素数测试

Rabin-Miller 素数测试,是一种素数判定法则,利用随机化算法判断一个数是合数还是可能是素数。

听起来像是一个不靠谱的算法,但是该算法可以以任意给定的准确率给出可能正 确的答案。当这个准确率足够大时,我们可以近似的认为这个算法给出的答案正确。(这一点遭到了 GarbageMan 的疯狂嘲讽,我猜他不知道为什么无穷大的倒数等于 0)对此算法仍然不放心的话,已经有结论表明,如果 n < 4,759,123,141 的话,只需验证 a = 2, 7, and 61 的情况,就可以确定性的给出 n 是否是一个素数。该算法的伪代码如下:

Input: n > 3, an odd integer to be tested for primality;
Input: k, a parameter that determines the accuracy of the test
Output: composite if n is composite, otherwise probably prime
write n − 1 as 2s·d with d odd by factoring powers of 2 from n − 1
WitnessLoop: repeat k times:
pick a random integer a in the range [2, n − 2]
x ← ad mod n
if x = 1 or x = n − 1 then do next WitnessLoop
repeat s − 1 times:
x ← x2 mod n
if x = 1 then return composite
if x = n − 1 then do next WitnessLoop
return composite
return probably prime

有关 Rabin-Miller 素数测试是否真的比通过试除法检验素数快,我们暂且将这一问题留待实验结果说明。下面我们讨论多次问答情况下的算法设计。

积极计算

GarbageMan 自鸣得意的一个优化就是延迟计算素数表。在我看来,这是一个完全没有必要的,并且极大地增加了代码复杂度的优化。在多次问答的情况下,最坏情况下如论如何 都需要计算整张素数表用于之后的试除法检验素数。这样,延迟计算所节省的计算量,完全抵不过复杂代码所带来的负面效果。

我的建议是,使用初始化方法预先计算从 [2, log MAX_N] 之间的素数,然后再用 GarbageMan 的 get_nearest 方法进行计算。在问答量比较大时,这种方法甚至会比 GarbageMan 优化过的算法还要快。

素数筛法

素数筛法是用于计算素数表的快速方法,其效率比朴素的通过试除法发现素数然后再添加到素数表中,要快得多。素数筛法已经十分先进了,甚至有亚线性时间复杂度的算法,因此,在实现生成素数表的情况下,没有理由不选择素数筛法。

由于这样一个事实——素数的个数随着数量级的增大而变得越来越稀疏(参考预备知识中的素数定理),当题目中给定的 n 非常大时,在 n 的附近寻找一个素数将变得越来越没有效率。对此,我建议在计算素数表的时候,直接计算到比 n 的上限还要大的一个素数之后再停止生成素数表,然后通过二分查找,直接确定给定的 n 在素数表中的位置,从而找到距离 n 最近的素数。

算法的思路至此介绍完毕,下面将设计实验来验证我的想法是否正确。

实验设计

由于题目分为两种情况,我的算法设计也分别针对这两种情况,因此在做实验进行对比的时候,也将分别设计实验。

实验的思路如下:

  1. 生成在 [1, MAX_N] 上均匀分布的若干随机数,并记录下来备用;
  2. 将这些随机数使用 GarbageMan 的方法进行计算,收集答案,并且统计用时;
  3. 将这些随机数使用我上面提出的几种方法进行计算,收集答案,并且统计用时;
  4. 验证这些不同算法答案是否相同,即确保算法的正确性;
  5. 比对不同算法的用时。

严密的验证需要多次重复试验不同的数据规模,并且排除其他因素的干扰。由于实验环境的限制,我将只进行一种中等数据规模的测试,得出的实验结果不严密,但是足以说明问题。

实验分别比较在单次问答模式下,GarbageMan 所用算法和 Rabin-Miller 算法的用时;在多次问答模式下,GarbageMan 所用算法和其他算法的用时。其中,在多次问答模式下,需要对 GarbageMan 所用的算法进行一些微调,以保证测试的公平性和正确性:

  1. get_nearest 方法中的 Node * head = NULL; 语句挪到 get_nearest 方法外面,并加上 static 标识符;
  2. 删除掉 get_nearest 方法中的 my_free(head); 一句;
  3. 修改 get_remainder 方法中的 if 语句判断条件为 if ((x != p->prime) && (x % p->prime == 0))

前两条修改意在重用已经计算好的素数表;第三条修改是在判断素数表中已有素数时,会产生错误的结果。

实验环境

  • CPU : Intel Core Duo P7450 2.13GHz
  • Windows 7 64bit
  • Visual Studio 2013

编译选项 /STACK:10485760,1048576 /O2

完整的实验代码点这里下载。

给出的测试代码依赖于 C++11 标准中提供的随机数生成函数,因此只能在 Visual Studio 2013 和较新版本的 g++,clang++ 上不需要修改的通过编译。使用较低版本的编译器编译时,可以结合 boost 库提供的支持,进行有限的修改后通过编译。 如果使用 g++ 或者 clang++ 进行编译的话,请使用参数 -O2 -std=c++11

实验结果

当设定 n 的范围在 [1, 10^6 - 10],且生成 50,000 个随机数时,多次问答模式下的测试结果如下:

Elapsed : 3900005ms    // GarbageMan 的方法
Elapsed : 500001ms // 积极计算的方法
Elapsed : 2890109ms // Rabin-Miller 算法
Elapsed : 270015ms // 素数筛法和二分查找

再测一次:

Elapsed : 3920017ms    // GarbageMan 的方法
Elapsed : 500001ms // 积极计算的方法
Elapsed : 2800004ms // Rabin-Miller 算法
Elapsed : 300001ms // 素数筛法和二分查找

大数定律可知,GarbageMan 的算法在平均情况下运行效率较低,并且低于积极计算的方法,可见不仅白优化了,还起到了负面效果。

由于实验结果显示,在多次问答模式下 GarbageMan 的算法运行效率仍然低于为单次问答模式所设计的 Rabin-Miller 算法,因此没有必要再进行单次问答模式的实验。

GarbageMan 多次对我进行人身攻击,并且侮辱我的母校。在此我要说一句,GarbageMan 你真是人如其名——渣男,人品渣,技术也渣。

结论

延迟计算是一个重要的概念,有着很多有趣的用法,尤其是 Haskell 语言中内建支持延迟计算,有很多利用这一特性的优美代码。

但是应该知道,延迟计算是有代价的,如果不能通过延迟计算节省足够的计算量,使用延迟计算在运行速度上就得不偿失。尤其是使用不“天然”支持延迟计 算特性的语言时(比如说 C 语言),更加需要谨慎的使用延迟计算特性,否则不仅达不到优化的目的,反而使代码变得复杂难以理解,运行效率降低。

参考资料

驳 GarbageMan 的《一个超复杂的简介递归》——对延迟计算的实验和思考的更多相关文章

  1. 一个超复杂的间接递归——C语言初学者代码中的常见错误与瑕疵(6)

    问题: 问题出处见 C语言初学者代码中的常见错误与瑕疵(5) . 在该文的最后,曾提到完成的代码还有进一步改进的余地.本文完成了这个改进.所以本文讨论的并不是初学者代码中的常见错误与瑕疵,而是对我自己 ...

  2. 腾讯出品的一个超棒的 Android UI 库

    腾讯出品的一个超棒的 Android UI 库 相信做 Android 久了大家都会有种体会,那就是 Android 开发相对于前端开发来说统一的 UI 开源库比较少.造成这种现象的原因一方面是大多数 ...

  3. Python 黑客 004 用Python构建一个SSH僵尸网络 01 简介

    用Python构建一个SSH僵尸网络 01 简介 一. 构建一个SSH僵尸网络的流程图: Created with Raphaël 2.1.0手动操作,实现通过SSH连接目标服务器(手动)用 Pexp ...

  4. 搭建一个超好用的 cmdb 系统

    10 分钟为你搭建一个超好用的 cmdb 系统 CMDB 是什么,作为 IT 工程师的你想必已经听说过了,或者已经烂熟了,容我再介绍一下,以防有读者还不知道.CMDB 的全称是 Configurati ...

  5. 吴裕雄 Bootstrap 前端框架开发——Bootstrap 按钮:制作一个超小按钮

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  6. 实验四 (1):定义一个形状类(Shape)方法:计算周长,计算面积

    (1)定义一个形状类(Shape)方法:计算周长,计算面积子类:矩形类(Rectangle) :额外的方法:differ() 计算长宽差圆形类(Circle)三角形类(Triangle)正方形类(Sq ...

  7. Java初学者作业——编写Java程序,输入一个数字,实现该数字阶乘的计算。

    返回本章节 返回作业目录 需求说明: 编写Java程序,输入一个数字,实现该数字阶乘的计算.一个数字的阶乘是所有小于及等于该数的正整数的积,自然数n的阶乘写作n! .例如,5的阶乘等于1*2*3*4* ...

  8. 【转载】制作一个超精简的WIN7.gho

    首先说明一点,这个Resource不是我制作的,Google搜了下GHO镜像文件制作,挺复杂的.如果要从头到尾自己制作GHO文件可以参考: http://baike.so.com/doc/674790 ...

  9. 打造支持apk下载和html5缓存的 IIS(配合一个超简单的android APP使用)具体解释

    为什么要做这个看起来不靠谱的东西呢? 由于刚学android开发,还不能非常好的熟练控制android界面的编辑和操作,所以我的一个急着要的运用就改为html5版本号了,反正这个运用也是须要从serv ...

随机推荐

  1. iOs 自定义UIView 日历的实现 Swift2.1

    学习Swift有一个月了,动手写一个UIView吧. 所有源代码在最后,直接用就可以了,第一次写Swift,和C#,Java还是有区别的 (博客园可以考虑在代码插入中添加Swift的着色了) 1  函 ...

  2. .net用SQL Server进行session会话管理

    保存在内存中的session经常由于dll的变动导致丢失.有一种方法可以长期保存session,那就是session的SqlServer模式. ASP.NET允许将会话数据存储到一个数据库服务器中,方 ...

  3. android实现无限轮播

    1 在ViewPager的适配器中的getCount()长度设置无限大Integer.MAX_VALUE 2  明白当前currentIten 为position % images.length; 3 ...

  4. java.lang.NoSuchMethodError: org.apache.xerces.impl.xs.XMLSchemaLoader.loadGrammar

    今天在服务器部署的时候,发生了这个问题,明明在本机上使用的时候,没有发生错误,但是发布到服务器上的时候却发生了这个错误,百度了好久,发现遇到这个问题的人很多,但是却没有一个比较满意的答案,后来还是通过 ...

  5. 2016 长春东北赛---Coconuts(离散化+DFS)

    题目链接 http://acm.hdu.edu.cn/showproblem.php?pid=5925 Problem Description TanBig, a friend of Mr. Frog ...

  6. js中对象使用

    简单记录javascript中对象的使用 一.创建对象 //创建一个空对象 var o={}; //创建一个含有两个属性的对象,x.y var o2={x:12,y:'12',name:'JS'}; ...

  7. C# RSA 分段加解密

    RSA加解密: 1024位的证书,加密时最大支持117个字节,解密时为128:2048位的证书,加密时最大支持245个字节,解密时为256. 加密时支持的最大字节数:证书位数/8 -11(比如:204 ...

  8. IO复用_select函数

    select函数: #include <sys/select.h> #include <time.h> #include <sys/types.h> #includ ...

  9. 用Microsoft.Solver.Foundation进行线性规划,为WPF应用添加智能

    在管理信息系统的开发过程中,往往会涉及到一些线性规划数学模型,例如资源配置优化.微软的Microsoft.Solver.Foundation是一个数学库,可以很好的对线性规划问题进行求解.关于它的细节 ...

  10. [DeviceOne开发]-地区选择

    一.简介 该demo主要通过do_ComboBox和do_Picker的selectChanged事件,实现省市县三级联动的功能 二.效果图 三.源码地址 https://github.com/do- ...