《数学与泛型编程:高效编程的奥秘》一3.3 实现该算法并优化其代码

3.3 实现该算法并优化其代码

一开始我们可能认为要用两个数组才能实现该算法,其中一个数组保存有待筛选的数字,另一个数组保存对应的Boolean标志,用以表示相应的数字有没有划掉。然而只要稍微思考一下就会发现,我们实际上不需要存储有待筛选的数字。因为其中的大多数值(也就是那些非素数)根本用不到。如果确实需要用到某个值,那我们可以根据它的位置来计算。由于首个值是3,而且后一个值比前一个值大2,因此第i个值就是2i + 3。
于是,只需要把筛选过程中用到的Boolean标志保存下来就可以了,我们用true表示素数,用false表示合数,并且把划掉非素数的操作称为给筛子做记号(mark the sieve)。下面这个函数可以根据给定的素因子(factor)来标注与之相关的所有非素数:

上面这段代码“声明”了一些带有要求的模板参数,而这些要求则称为概念(concept),
我们将在第10章详细讲解此内容,大家现在可以先参考附录C,来熟悉这个术语。(如果你对C++的模板不太熟悉,那么也请查阅附录C,该附录详细解释了模板机制。)
稍后我们将会看到,在调用上面这个函数时,first参数会指向首个还未划掉的factor倍数,而根据刚才的讨论,大家可以知道,这个值就是factor的平方。对于last参数来说,我们将遵循STL的约定,把刚刚超过表格中最后一个元素的那个迭代器传给它,这样一来,last-first就可以表示元素的数量了。

    • *
      在开始编写筛选所用的代码之前,我们先考虑下面几条引理(lemma):

对于合数c来说,其最小的素因子的平方值,肯定小于等于c。
任何一个比p2小的合数,都会为某个比p小的素数所划掉(也就是说,它是p的倍数)。
如果某一轮是以p为素因子进行筛选的,那么该轮应该从p2开始执行。
如果要找的是小于等于m的所有素数,那么当p2>m时,整个筛选过程就应该停止。
下面这两条公式,用来在筛选的过程中进行计算:
第i个元素的值:value (i) = 3 + 2i = 2i + 3
值为v的元素所对应的下标:index (v) =
对于第i个元素来说,其数值的k倍与k+2倍之间所间隔的元素数量是:

现在,我们可以试着来实现这个素数筛选函数了:

有人可能认为,既然这个筛选函数必须从头开始完整地运用到某个序列上面,那么它就应该接受一个指向某种数据结构的引用,那种数据结构里面含有由Boolean值所构成的序列。但实际上,我们并没有那么做,而是令调用者把一个指向某段范围开始处的迭代器,以及该范围的长度传进来,以便能够应对更多的数据结构。这些数据既可以存放在STL容器里面,也可以存放在内存块里面,我们并不对此做出限定。请注意,该函数的第二个参数指的是表格的大小n,而不是筛选的上限m。
当前这一轮所要划掉的首个数值,就是筛选所用的那个素因子的平方,而该值在表格中的索引则用index_square变量表示。值得注意的是,在每一轮循环时,我们可能都要用
i + i + 3这个表达式,来计算当前这一轮筛选所使用的素因子,而且还要用另外一些表达式来计算其他的量(这些内容在代码中以斜体标出)。为此,我们可以把每轮循环都要用到的那些式子提取到循环的外面。修改之后的代码,用粗体标出了相关的改动:

敏锐的读者可能会发现,修改之后的代码要对factor变量进行运算,这样做的效率实际上比修改之前还要差一些。修改代码之前,我们只需要在if测试为true的时候才去计算i + i + 3的值,而修改完之后则需要在每次执行循环时,都把这个式子计算一遍。但是大家稍后就会看到,单独用一个factor变量来表示这个式子是很有意义的。更大的问题在于,对index_square变量所做的运算,其开销相对来说比较高,因为要执行两次乘法才行。针对这个问题,我们可以从编译器的优化技术中获得灵感。有一种优化技术叫做强度折减(strength reduction),也就是用开销相对较低的运算,来等效地取代开销较高的运算,例如用加法来实现乘法。既然编译器可以执行这样的优化,那我们同样可以采用手动的办法来实现它。
现在就来详细考虑这两个式子。假如我们把下面两种运算:

那么其中的δfactor和δindex_square,就分别表示factor和index_square变量在前后两轮之间的差值(也就是第i轮和第i+1轮之间的差值)。这两个差值可以这样来计算:

δfactor这一部分很好处理,由于其中的变量i可以消去,因此只需要用常量2来表示它就行了。接下来主要应该考虑的是,怎样对计算δindex_square所需的表达式进行简化。我们对表达式中的各项重新整理之后,可以看出,这条表达式相当于factor(i)与factor(i + 1)的和,其中的factor(i)是当前这一轮已经计算好的,而factor(i + 1)则正是我们紧接着就要计算的。(如果你要在代码中计算多个值,那就应该像本例这样,看看能不能用其中一个值来表示另外的那些值,做到了这一点,或许就可以减少一些计算量。)
用常量2来替换δfactor,并通过变量factor来表示δfactor之后,我们就得到了最终版本的sift函数。这次也用粗体表示其中的改进之处:

习题3.2 用二进制位(以std::vector来实现)、uint8_t、uint16_t、uint32_t及uint64_t等大小不同的数据来衡量素数筛选算法所耗的时间。
习题3.3 通过素数筛选算法来绘制下列函数的图像:
π (n) =小于n的素数个数
n一直取到107,并找出此函数的解析近似(analytic approximation)函数。
如果一个素数的数位按照相反顺序排列之后和原数一样,那么这种素数就叫做回文素数(palindromic prime)。下面列出2至1000之间的素数,并用方框来标注其中的回文素数:

习题3.4 有没有大于1000的回文素数?[1000, 2000]这个区间内,为什么不会出现回文素数?如果我们不用十进制,而是改用十六进制,那么情况是否有所变化?如果改用以任意数n为底的计数方式(也就是n进制)呢?

时间: 2024-09-21 19:57:11

《数学与泛型编程:高效编程的奥秘》一3.3 实现该算法并优化其代码的相关文章

《数学与泛型编程:高效编程的奥秘》一第2章 算法初谈

第2章 算 法 初 谈 摩西很快就学会了算术与几何. --这些知识是他从埃及人那里学来的, 埃及人最重视的研究科目是数学. --亚历山大的斐洛(Philo of Alexandria),<摩西生平>(Life of Moses) 算法(algorithm)是用来完成计算任务的一系列有限步骤.由于算法与计算机编程的关系特别密切,因此很多人或许认为,算法是一个来自计算机科学专业的概念.其实算法这个词已经有几千年历史了.数学中充满了各种各样的算法,有些算法我们天天在用,就连小学生计算加法时所用的竖式

《数学与泛型编程:高效编程的奥秘》一导读

前言 如果将计算机科学与数学分离,那么这两者的发展都会有很大困难.于是,我们就试图通过一些课程,把人类文明早期就有的数学活动与现代才有的计算机活动结合起来.本书正是基于这样一种课程而编写的.能够与友人Dan Rose合作,我深感荣幸.他的管理工作令我们团队能够把泛型编程的原则运用到搜索引擎的设计上来,而且他愿意把我原来那些相当分散的课程内容集结成现在这样一本连贯的书籍.Dan和我都希望读者能够喜欢我们这次合作的成果.--A.A.S. 大家将要阅读的这本书是根据"Algorithmic Journ

《数学与泛型编程:高效编程的奥秘》一1.1 编程与数学

1.1 编程与数学 那么,这种泛型编程的想法是从哪里来的?我们又应该怎样来学习它呢?这种想法是从数学中衍生出来的,尤其与抽象代数(abstract algebra)这个数学分支有关.为了使大家能够理解这种编程方式,本书会对抽象代数做一些介绍,并着重讲解怎样从抽象的运算属性来认识对象.这个话题一般是数学专业的大学生才去研究的,然而笔者认为,它对于我们理解泛型编程会起到关键的作用.实际上,还有很多基本的编程概念也同样来自数学.对这些概念的产生及变化过程加以学习,能够促使我们更好地思考软件的设计问题.

《数学与泛型编程:高效编程的奥秘》一2.1 埃及乘法算法

2.1 埃及乘法算法 与所有的古文明一样,埃及的计数系统也没有按位置计数这一概念,而且无法表示0.于是,乘法计算起来就特别困难,只有少数受过训练的专家才会做.(你可以想象一下:如果自己只能像使用罗马数字那样来做运算,而且要计算的数字又很大,那么算起来是相当困难的.)怎样来定义乘法呢?宽泛地说,我们可以认为乘法就是"把某物多次加到它自己上面",如果说得严谨一些,那么可以分两种情况来定义:一种情况是乘以1,另一种情况是乘以一个大于1的数.我们将乘以1的乘法运算,定义如下:1a = a(2.

《数学与泛型编程:高效编程的奥秘》一第1章 内容提要

第1章 内 容 提 要 不懂数学,就无法了解世界. --罗吉尔·培根(Roger Bacon),<大著作>(Opus Majus) 这是一本谈编程的书,但是它与大多数的编程书都不太一样,因为除了算法和代码之外,本书还会给出数学证明和一些讲述从古代到20世纪各种数学发现的历史材料. 另一个更为具体的特色在于:这是一本谈论泛型编程(generic programming)的书.泛型编程是出现于20世纪80年代的编程方法,在20世纪90年代随着C++标准模板库(Standard Template L

《数学与泛型编程:高效编程的奥秘》一2.2 改进该算法

2.2 改进该算法 从加法的执行次数上面来看,multiply1函数确实做得不错,但它毕竟执行了?log n?次递归调用.由于函数调用的开销很大,因此我们想把程序中的递归调用去掉,以避免此类开销. 在对函数进行转化时,我们所秉持的一项理念是:通用的工作实现起来经常比具体的工作还要简单(It is often easier to do more work rather than less.).就本例来说,我们要按照下面这种通用的方式进行计算: r + na 其中的r是一个会在运算过程中持续更新的值

《数学与泛型编程:高效编程的奥秘》一1.3 阅读准备

1.3 阅读准备 由于书中的很多内容都和数学有关,因此你可能担心自己必须先具备丰富的数学知识,然后才能看懂这本书.其实你只要有逻辑思考能力就行(程序员应该很擅长逻辑思考),笔者并不会要求大家具备中学代数与中学几何之外的其他数学知识.某些章节可能会运用向量(vector)与矩阵(matrix)等线性代数(linear algebra)方面的概念,如果从前没有看过这方面的资料,那么把这些内容跳过去就可以了.若是对本书所用的记法不够熟悉,则请参考附录A. 数学中有一个很重要的部分就是对命题给出形式化证

《数学与泛型编程:高效编程的奥秘》一3.1 整数的几何属性

3.1 整数的几何属性 毕达哥拉斯(Pythagoras)这个名字,大多数人都是从同名的定理中听说的.这位古希腊的数学家和哲学家曾经提出一个理念:要想理解世界,就必须先懂得数学.他还发现了数字的很多奇妙性质,而且认为这些发现无论有没有实际的用途,其本身都具备巨大的价值.亚里士多德的学生亚里士多塞诺斯(Aristoxenus)曾经说道:"他极其重视算术研究工作,并把这些研究从实用的商业领域中划分出来,单独加以推进." 毕达哥拉斯(Pythagoras,约公元前570-公元前490) 毕达

《数学与泛型编程:高效编程的奥秘》一1.2 从历史的角度来讲解

1.2 从历史的角度来讲解 如果能把某个话题融入故事中,那么我们学起来就会更容易一些,而且也会觉得更为有趣.我们想要知道:在某个时间发生了什么事件?都有谁与该事件相关?这些人是怎样产生某种想法的?他们是要在别人的基础之上进行研究,还是要驳斥别人的结论?为此,本书在介绍数学概念时,还会讲一些与此有关的故事,并谈一下提出这些概念的人.笔者通常用一份简要的传记来描述身为故事主角的数学家,这些小传不会像百科全书里面写得那样详尽,它只是想令你对这位数学家的情况有所了解而已. 尽管我们会从历史的角度来谈论某

《数学与泛型编程:高效编程的奥秘》一3.7 本章要点

3.7 本章要点 古希腊人对数字的"形状"以及其他一些属性(例如是不是素数,是不是完美数等)很着迷,这就给数论这一数学领域打下了基础.他们所提出的某些算法(例如埃拉托斯特尼筛法)即便在今天来看,也是相当优雅的,只不过我们还可以通过某些现代的优化技术来继续提升其效率. * 读完本章之后,大家已经看到了两种能够证明是无理数的方法,一种是几何方法,另一种是代数方法.能够针对同一个数学现象提出两种完全不同的证明,这是相当好的结果,而且数学家实际上也必须像这样去寻找同一个数学现象的多种证法,以增