目录
一,埃及乘法、加法链
1,什么是泛型编程
泛型编程是一种专注于对算法及数据结构进行设计的编程方式,它使得这些算法及数据结构能够在不损失效率的前提下,运用到最为通用的环境中。
2,泛型的思想来源
这种想法是从个数学中衍生出来的,尤其与抽象代数这个数学分支有关。
抽象代数里面的抽象很大程度上是从另外一个数学分支的具体成果中得出的,那个分支比抽象代数更为古老,它叫做数论。
要想成为优秀的程序员,就必须理解泛型编程的原则;要想理解泛型编程的原则,就必须学会抽象;要想学会抽象,就必须知道它所依据的数学基础。
3,算法的起源 VS 数学的起源
算法(algorithm)是用来完成计算任务的一系列有限步骤。由于算法与计算机编程的关系特别密切,因此很多人或许认为,算法是一个来自计算机科学专业的概念。其实算法这个词已经有几千年历史了。数学中充满了各种各样的算法,有些算法我们天天在用,就连小学生计算加法时所用的竖式(long addition)都可以说是一种算法。
尽管算法的历史很悠久,但它并不是天生就有的,而是由人发明出来的。虽然我们并不清楚第一个发明算法的人是谁,但我们知道,早在四千多年前埃及就已经有了某些算法。
在我们的通常观念中,逻辑简单的问题是不需要算法的,需要算法的都是逻辑比较复杂的问题(比如OJ上的题目)
几千年前,人们用绳子计数,在这个时代,乘法就是一个比较困难的问题,需要算法。
埃及乘法/俄罗斯农夫算法
这种算法所依据的原理是:4a = ((a+a)+a)+a = (a+a) + (a+a), 这个原理是根据加法结合律推算出来的。
如果采用这个办法,那么只需把a+a的值计算一次就行了,这样可以降低加法运算的次数。
埃及乘法算法的思路是:反复地将n减半,并将a加倍,同时求出a的各种倍数,这些倍数与a的比值都是2的整数次幂。
41*59 = 1*59 + 8*59 + 32*59
由于该算法在将n减半的时候需要判断n是奇数还是偶数,因此尽管没有直接的证据,但我们依然能够推测出:古埃及人已经知道了奇数和偶数之间的区别。
埃及乘法,在ACM里面一般叫做快速乘法(算法和快速幂差不多,所以叫法一致)
#define ll long long
ll g(ll a, ll b, ll p)//a*b
{
if (b == 0)return 0;
ll r = g(a, b / 2, p);
r = (r + r) % p;
if (b % 2)r = (r + a) % p;
return r;
}
快速乘法参考:快速乘法、快速幂、矩阵快速幂
4,列竖式计算乘法 VS 埃及乘法
列竖式计算两位数的乘法大概是这样的:
诶及乘法用二进制表示大概是这样的:
当然,诶及乘法和二进制列竖式乘法还是有差异的,诶及乘法不涉及进制转换,所以上图的上面一半是二进制列竖式乘法,下面一半才是埃及乘法。
5,十进制 VS 二进制
列竖式计算乘法 和 埃及乘法 在泛进制的角度上来看是一样的。
(1)为什么人类使用十进制,机器使用二进制?
因为人类有十个手指头,机器的电路有开关两种状态,各自用各自方便的进制基数。
(2)列竖式计算乘法是怎么来的?
并不是每个国家的小学生计算两位数的乘法的方法都和中国一样,甚至据说有些国家的学生到了小学毕业还是觉得计算乘法很难,因为他们的计算方法不一样,而且据说别的语言的九九乘法表没有汉语这么顺口好记。
我推测,应该是在有了阿拉伯数字和九九乘法表之后,埃及乘法就被合理改造成列竖式计算乘法了。
从这个角度来说,埃及乘法就是一个泛型算法,进制就是数据结构的差异,就好像max函数可以传入int也可以传入double一样。
补更:我在本书中刚好发现了这个科普:
0的突破发生在公元1203年。这一年,比萨的列奥那多(Leonardo Pisano,也称为斐波那契,Fibonacci)出版了一本名为《计算之书》(Liber Abaci)的著作,其中不仅介绍了0与十进制位值计数法,而且首次向欧洲人讲述了算术运算的标准步骤,也就是怎样用竖式来计算加减乘除,这些步骤直到今天依然在小学课堂上传授。我们可以说,列奥那多一举将数学带回了欧洲。
(3)埃及乘法是二进制吗?
二进制应该是近代才发明的,据说是牛顿的老对手莱布尼茨发明的。
埃及乘法被发明的时候没有二进制,用绳子计数只有加减法,没有很强的乘法的概念,更没有除法,也更不会涉及进制转换了。
那为什么埃及算法刚好契合二进制呢?因为,加法是二元操作符,而且,绳子计数可以很快的进行除2操作!
虽然没有除法,但是把绳子对折,这就是天然的除2啊,书中也提到,这个时候的人们应该已经有奇数和偶数的概念了。
书中也提到相关概念,这种乘法是依赖加法结合律的,也就是a+a+a+a+a+a = (a+a) + (a+a) + (a+a)
看到了吗?每个括号都是把2个数括起来,让6个数相加的问题变成了3个数相加的问题。
6,加法链
这里指的是一个如何计算nx,其中n是已知数,而x是未知数,设计一个对x通用的算法。
(1)埃及乘法的加法次数
设n是一个a位的二进制数,这a位由b个1和a-b个0组成,那么埃及乘法计算nx的过程中,加法的次数是a+b-2
用函数来表示:AJ(n) = a+b-2
当n是1到15时,函数值分别是0,1,2,2,3,3,4,3,4,4,5,4,5,5,6
显然,AJ函数并不是递增函数
(2)加法链
埃及乘法的加法次数是最少的吗?这个算法是最优的吗?
书中给出了反例,显然也是最小的反例:
这个程序其实是先算出b=3x,需要2次加法,再算出5b,需要3次加法,一共需要5次加法
这样一种计算乘法的方式叫做加法链,埃及乘法本身也是一种加法链(长度为1的链)
(3)最优加法链
按照加法链的方式计算乘法,需要的最少加法次数记为L(n)
如果一个加法链,用它计算乘法需要的加法次数是L(n),那么它就是最优加法链
(4)L函数的性质
L(x)<= AJ(x)
L(yz)<= L(y) + L(z)
L(x+1)<=L(x)+1
L(2x)=L(x)+1
设RL(x)=min{L(y)+L(x/y)|y是x的约数,1<y<x},那么L(x)=min{RL(x), L(x-1)+1}
如此,我们就可以算出最优加法链了。
二,群环域
三,推导泛型算法
1,模板函数
埃及乘法用函数表示:
黑色加粗的是n相关的内容,我们可以分析出,对n的变量类型的要求是,可以判断奇偶、与1进行比较、除2操作、传值、赋值
注意,我们只要求有除2操作,并没有要求支持整个除法,比如尺规作图可以平分线段,但是不能三等分线段。
对r和a的变量类型的要求是,可以进行加法操作、传值、赋值
于是,我们可以写出模板函数,只需要2个模板参数,不需要三个。
multiply_accumulate 函数:
对N的要求是,可以判断奇偶、与1进行比较、除2操作、传值、赋值
对A的要求是,可以进行加法操作、传值、赋值
2,模板参数A
对于类型A,我们有3个句法要求:
- 必须支持加法(也就是必须支持C++语言的operator+)。
- 必须能够按值传递(也就是必须支持C++语言的拷贝构造函数(copy constructor))。
- 必须能够接受赋值(也就是必须支持C++语言的operator=)。
此外,还有语义要求:
这个算法需要加法支持结合律。
c++自有的加法是满足结合律的:数学和计算机中的加法结合律_关联律是否适用于 ieee 计算机加法?-CSDN博客
此外,还有隐含句法要求:
因为支持传值和赋值,所以也应该支持等价性测试:
- 必须能够比较两值是否相等(也就是必须支持C++的operator==)。
- 必须能够比较两值是否不相等(也就是必须支持C++的operator!=)。
PS:虽然埃及乘法并不需要等价性测试,但是如果对这个函数做单元测试,就需要了。
此外,等价性测试也有对应的语义要求:
- 对不等关系取反,其结果是等价关系
- 等价关系是反身的(reflexive)、对称的(symmetric)、可传递的(transitive)
二元关系参考:二元关系_二元关系有哪些-CSDN博客
3,常规类型
常规类型是一种能像int等内置的类型那样,支持在同类数据之间进行构造、赋值及等价测试的类型。
常规类型也称为正规类型、正则类型。
于是,我们队模板A的要求就可以概括为2条:
- 必须是常规类型
- 必须提供具备结合律的加法运算
其实A就是个半群,但是不像数学中的加法半群拥有交换律,A并不需要交换律。
因此,应该将A称为非交换的加法半群(noncommutative additive semigroup)
4,模板参数N
N必须是一种能够实现下列操作的常规类型:
- half
- odd
- ==0
- ==1
接下来,考虑N的语义要求:
- even(n) 则 half(n)+half(n)=n
- odd(n) 则 even(n-1)
- odd(n) 则 half(n-1)=half(n)
- 公理:n≤1∨half(n)=1∨half(half(n))=1∨...
我们把这些类型所满足的C++概念称为Integer
5,泛型算法
对这两种类型提出适当的要求之后,我们现在终于能够完全采用泛型(generic)代码来实现乘法累加函数了:
multiply_accumulate_semigroup 函数:
如果N和A都可以为负,那么可以写成:
multiply_group 函数:
6,半群、幺半群、群
multiply_accumulate函数的适用范围是n是正整数(当时的数学只有正整数),要求A是半群,半群不保证有单元元素0
multiply_accumulate_semigroup函数的适用范围是n是自然数,要求A是幺半群,幺半群保证有单元元素0,但是不保证有逆
multiply_group函数的适用范围是n是全体整数,要求A是群,群保证有单位元素0和逆(此处即 -a)。
7,对运算本身加以泛化
这个算法可以用在加法半群上,也可以用在乘法半群上,还可以用在矩阵乘法半群上。
我的实现:快速幂
首先实现高效的算法,然后在保持精确度的前提下把它泛化到抽象的数学概念上面,最后将其运用于各种不同的场合中——这样一种思路正是泛型编程的核心理念。
8,化简(累加、累乘)
有一个重要的算法值得注意。这个算法叫做化简(Reduction),也就是先对两个元素做二元运算,然后将运算结果与第三个元素再做二元运算,依此类推,以便将该运算运用在一系列元素上面。
数学中最为常见的两个例子,是加法半群的连加(Σ)函数及乘法半群的连乘(Π)函数。
化简算法可以运用在任何一种半群上面。