这是一篇因骂战而起的博文,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. 一不小心把oschina给戒了

    不知怎么回事,逐渐变成一周看一次oschina了.

  2. PHP内核探索之变量(5)- session的基本原理

    这次说说session. session可以说是当前互联网提到的最多的名词之一了.它的含义很宽泛,可以指任何一次完整的事务交互(会话):如发送一次HTTP请求并接受响应,执行一条SQL语句都可以看做一 ...

  3. Android开发跳槽、简历和面试的那些事

    年后不久,就迎来了一年一度的招聘旺季,尤其,对于互联网行业来说,近些年的3月份被视为换工作的最高峰,已经没什么可以争议的了. 至今为止,在小组Android开发招聘这块,已经面试有近30人了.最后得出 ...

  4. Mac OS Git 安装

    一.Git是一个分布式的代码版本管理工具.类似的常用工具还有SVN,CVS.最大的特点也是优点在于提供分布式的代码管理 1.分支代码只有一份! 使用过svn的童鞋想必都知道,当我们要开发一个新功能或者 ...

  5. C#代码实现对HTTP POST参数进行排序

    private static string GetSortedParas(Dictionary<string, string> dic) { dic = dic.OrderBy(key = ...

  6. CSS教程:vlink,alink,link和a:link

    超链接文字的状态可以通过伪类选择符+样式规则来控制. 一组专门的预定义的类称为伪类,主要用来处理超链接的状态.超链接文字的状态可以通过伪类选择符+样式规则来控制.伪类选择符包括: 总: a 表示 超链 ...

  7. console命令详解

    Firebug是网页开发的利器,能够极大地提升工作效率. 但是,它不太容易上手.我曾经翻译过一篇<Firebug入门指南>,介绍了一些基本用法.今天,继续介绍它的高级用法. ======= ...

  8. What is research (1)

    This abstract tells me a lot of stories about itself. Here I want to discuss two stories about it. I ...

  9. Sharepoint学习笔记—习题系列--70-576习题解析 -(Q63-Q65)

    Question 63You are designing a SharePoint 2010 implementation that will be used by a company with a ...

  10. Android无线调试

    方法一: 1. 使用USB数据线连接设备. 2. 命令输入adb tcpip 5555 ( 5555为端口号,可以自由指定). 3. 断开 USB数据,此时可以连接你需要连接的|USB设备. 4. 再 ...