经常听到程序员吐槽“数学好难!”而程序员在编程中却又少不了这数学基础。编程的基础是计算机科学,计算机科学的基础就是数学。
我们都知道,只有学好数学才能打好编程的地基,写出健壮的程序。但还是会有人说“但我数学就是不好啊”。特别是很多人“一碰到算式就跳过不读”。坦率而言,我也曾这样过。
但是看了《程序员的数学》以后,我很确信是我以前打开数学的方式不对,而让我对数学产生了某种误解。
《程序员的数学》由图灵出版,由日本资深技术作家结城浩著作。书中尽可能减少了“大家不想看的算式”,也省去了过多的定义、定理和证明。单纯是为帮助程序员更容易理解编程而写的书。真的是受益匪浅,特别是在编程中的“数学思维”方面!下面我们根据书中的讲解来重新认识一下编程中的数学吧。
结城浩著作还有《数学女孩》《数学女孩2:费马大定理》《数学女孩3:哥德尔不完备定理》《数学女孩4:随机算法》《图解密码技术(第3版)》,点击链接可以试读。
众所周知,我们人类使用 10进制,计算机使用 2进制,这样的按位计数法中,0在其中所起的作用是什么?乍一看, 0仅仅是表示 “什么都没有 ”的意思,而实际上它具有创建模式、简化并总结规则的重要作用。
0 的故事——无即是有
◎◎课前对话
老师:1,2,3 的罗马计数法是I,II,III。
学生:加法很简单嘛。I + II,只要将3 个I 并排写就行了。
老师:不过II + III 可不是IIIII,而是V 喔!
学生:啊,是这样啊!
老师:没错,如果数目变大,那数起来可就费劲啦!
小学一年级的回忆
以下是小学一年级时发生的事,我依然记忆犹新。
“下面请打开本子,写一下‘十二’。”老师说道。于是,我翻开崭新的本子,紧握住削尖了的铅笔,写下了这样大大的数字。
老师走到我跟前,看到我的本子,面带微笑亲切地说:“写得不对喔。应该写成12 喔。”
当时我是听到老师说“十二”,才写下了10 和2。不过那样是不对的。众所周知,现在我们把“十二”写作12。
而在罗马数字中,“十二”写作XII。X 表示10,I 表示1。II 则表示两个并排的1,即2。也就是说,XII 是由X 和II 组成的。
如同“十二”可以写作12 和XII,数字有着各种各样的计数法。12 是阿拉伯数字的计数法,而XII 是罗马数字的计数法。无论采用哪种计数法,所表达的“数字本身” 并无二致。下面我们就来介绍几种计数法。
10 进制计数法
下面介绍10 进制计数法。
什么是10 进制计数法
我们平时使用的是10 进制计数法。
- 使用的数字有 0、1、2、3、4、5、6、7、8、9共 10种。
这里的“种”指的是数字的种类,用来说明10 进制和2 进制中数字复杂程度的差异。如2561 中包含四种数字,而1010 中只包含两种数字。
- 数位有一定的意义,从右往左分别表示个位、十位、百位、千位……
以上规则在小学数学中都学到过,日常生活中也一直在用,是众所周知的常识。
在此权当复习,后面我们将通过实例来了解一下10 进制计数法。
分解2503
首先,我们以2503 这个数为例。2503 表示的是由2、5、0、3 这4 个数字组成的一个称作2503 的数。
这样并排的数字,因数位不同而意义相异。
- 2 表示“1000的个数”。
- 5 表示“100的个数”。
- 0 表示“10的个数”。
- 3 表示“1 的个数”。
综上所述,2503 这个数是2 个1000、 5 个100、0 个10 和3 个1 累加的结果。
用数字和语言来冗长地说明有些无趣,下面就用图示来表现。
如图,将数字的字体大小加以区别,各个数位上的数字2、5、0、3 的意义便显而易见了。
1000 是10×10×10,即 103(10 的3 次方),100 是10×10,即102(10 的2 次方)。因此,也可以写成如下形式(请注意箭头所示部分)。
再则,10 是101(10 的1 次方),1 是100(10 的0 次方),所以还可以写成如下形式。
千位、百位、十位、个位,分别可称作103 的位、102 的位、101 的位、100 的位。10 进制计数法的数位全都是10n 的形式。这个10 称作10 进制计数法的基数或底。
基数10 右上角的数字——指数,是3、2、1、0 这样有规律地顺次排列的,这点请记住。
2 进制计数法
下面讲解2 进制计数法。
什么是2 进制计数法
计算机在处理数据时使用的是2 进制计数法。从10 进制计数法类推,便可很快掌握它的规则。
- 使用的数字只有 0、1,共2 种。
- 从右往左分别表示 1 位、2 位、4位、8 位……
用2 进制计数法来数数,首先是0,然后是1,接下去……不是2,而是在1 上面进位变成10,继而是11,100,101……
表1-1 展示了0 到99 的数的10 进制计数法和2 进制计数法。
表1-1 0 到99 的数的10 进制计数法和2 进制计数法
分解1100
在此,我们以2 进制表示的1100(2 进制数的1100)为例来探其究竟。
和10 进制计数法一样,并排的数字,各个数位都有不同的意义。从左往右依次为:
- 1 表示“8 的个数”。
- 1 表示“4 的个数”。
- 0 表示“2 的个数”。
- 0 表示“1 的个数”。
也就是说,2 进制的1100 是1 个8、 1 个4、0 个2 和0 个1 累加的结果。这里出现的8、4、2、1,分别表示23、22、21、20。即2 进制计数法的1100,表示如下意思。
如此计算就能将2 进制计数法的1100 转换为10 进制计数法。
由此可以得出,2 进制的1100 若用10 进制计数法来表示,则为12。
基数转换
接下来我们试着将10 进制的12 转换为2 进制。这需要将12 反复地除以2(12 除以2,商为6 ;6 再除以2,商为3 ;3 再除以2……),并观察余数为“1”还是“0”。余数为0 则表示“除完了”。随后再将每步所得的余数的列(1 和0 的列)逆向排列,由此就得到2 进制表示了。
同样地,我们试将10 进制的2503 转换为2 进制计数法。
我们从图1-2 可以知道2503 用2 进制表示为100111000111。各个数位的权重如下:
在10 进制中,基数为10,各个数位是以10n 的形式表现的。而2 进制中,基数为2,各个数位是以2n 的形式表现的。从10 进制计数法转换为2 进制计数法,称作10 进制至2进制的基数转换。
计算机中为什么采用2 进制计数法
计算机中一般采用2 进制计数法,我们来思考一下原因。计算机在表示数的时候,会使用以下两种状态。
- 开关切断状态
- 开关连通状态
虽说是开关,但实际上并不需要机械部件,你可以想象成是由电路形成的“电子开关”。总之,它能够形成两种状态。这两种状态,分别对应0 和1 这两个数字。
- 开关切断状态 … 0
- 开关连通状态 … 1
1 个开关可以用0 或1 来表示,如果有许多开关,就可以表示为许多个0 或1。你可以想象这里排列着许多开关,各个开关分别表示2 进制中的各个数位。这样一来,只要增加开关的个数,不管是多大的数字都能表示出来。
当然,做成能够表示0 ~ 9 这10 种状态的开关,进而让计算机采用10 进制计数法,这在理论上也是可能的。但是,与0 和1 的开关相比,必定有更为复杂的结构。
另外,请比较一下图1-3 和图1-4 所示的加法表。2 进制的表比10 进制的表简单得多吧。
若要做成1 位加法的电路,采用2 进制要比10 进制更为简便。
不过,比起10 进制,2 进制的位数会增加许多,这是它的缺点。例如,在10 进制中2503 只有4 位,而在2 进制中要表达同样的数则需要100111000111 共12 位数字。这点从表1-2 中也显而易见。
人们觉得10 进制比2 进制更容易处理,是因为10 进制计数法的位数少,计算起来不容易发生错误。此外,比起2 进制,采用10 进制能够简单地通过直觉判断出数值的大小。人的两手加起来共有10 个指头,这也是10 进制更容易理解的原因之一。
不过,因为计算机的计算速度非常快,位数再多也没有关系。而且计算机不会像人类那样发生计算错误,不需要靠直觉把握数字的大小。对于计算机来说,处理的数字种类少、计算规则简单就最好不过了。
让我们来总结一下。
- 在 10进制计数法中,位数少,但是数字的种类多。
→对人类来说,这种比较易用。
- 在 2 进制计数法中,数字的种类少,但是位数多。
→对计算机来说,这种比较易用。
鉴于上述原因,计算机采用了2 进制计数法。
人类使用10 进制计数法,而计算机使用2 进制计数法,因此计算机在执行人类发出的任务时,会进行10 进制和2 进制间的转换。计算机先将10 进制转换为2 进制,用2 进制进行计算,再将所得的2 进制计算结果转换为10 进制。
按位计数法
什么是按位计数法
我们学习了10 进制和2 进制两种计数法,这些方法一般称作按位计数法。除了10 进制和2 进制以外,还有许多种类的按位计数法。在编程中,也常常使用8 进制和16 进制计数法。
● 8 进制计数法
8 进制计数法的特征如下:
- 使用的数字有 0、1、2、3、4、5、6、7共 8 种。
- 从右往左分别为 80 的位、81 的位、82 的位、83 的位……(基数是8)
● 16 进制计数法
16 进制计数法的特征如下:
- 使用的数字有 0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F共 16种。
- 从右往左分别为 16º 的位、16¹ 的位、16²的位、16³ 的位……(基数是16)
在16 进制计数法中,使用A、B、C、D、E、F(有时也使用小写字母a、b、c、d、e、f)来表示10 以上的数字。
● N 进制计数法
一般来说,N 进制计数法的特征如下:
- 使用的数字有 0,1,2,3,…,N-1,共 N种。
- 从右往左分别为 Nº 的位、N¹的位、N² 的位、N³的位……(基数是 N)
例如,N 进制计数法中,4 位数a₃a₂a₁a。 为
不使用按位计数法的罗马数字
按位计数法在生活中最为常见,因此人们往往认为这种方法是理所当然的。实际上,在我们身边也有不使用按位计数法的例子。
例如,罗马计数法。
罗马数字至今还常常出现在钟表表盘上。
还有,在电影最后放映的演职员名单中,也会出现表示年号的MCMXCVIII 等字母。
这也是罗马数字。
罗马计数法的特征如下:
- 数位没有意义,只表示数字本身
- 没有 0
- 使用 I(1)、V(5)、X(10)、L(50)、C(100)、D(500)、M(1000) 来记数
- 将并排的数字加起来,就是所表示的数。
例如,3 个并排的I(III)表示3,并排的V 和I(VI)表示6,VIII 表示8。
罗马数字的加法很简单,只要将罗马数字并排写就可以得到它们的和。比如,要计算1+2,只要将表示1 的I 和表示2 的II 并排写作III 就行了。但是,数字多了可就不太简单了。
例如,计算3+3 并不是把III 和III 并排写作IIIIII,而是将5 单独拿出来写作V,所以6 就应该写作VI。CXXIII(123) 和LXXVIII(78) 的加法, 也不能仅仅并排写作CXXIIILXXVIII,而必须将IIIII 转换为V,VV 转换为X,XXXXX 转换为L,再将LL 转换为C,如此整理最后得到CCI(201)。在“整理”罗马数字的过程中,必须进行与按位计数法的进位相仿的计算。
罗马计数法中还有“减法规则”。例如IV,在V 的左侧写I,表示5-1,即4(在钟表表盘上,由于历史原因也有将4 写作IIII 的)。
让我们试着将罗马数字的MCMXCVIII 用10 进制来表示。
可以发现,MCMXCVIII 表示的就是1998。罗马数字真是费劲啊!
指数法则
10 的0 次方是什么
在10 进制的说明中,我们讲过“1 是10º(10 的0 次方)”,即10º=1。
也许有些读者会产生以下疑问吧。
10² 是“2 个10 相乘”,那么10º 不就是“0 个10 相乘”吗?这样的话,不应该是1,而是0 吧?
这个问题的核心在哪里呢?我们来深入思考一下。问题在于“10ⁿ 是n 个10 相乘”这部分。在说“n 个10 相乘”时,我们自然而然会把n 想作1,2,3…。因此,在说“0 个10 相乘”时,却不知道应该如何正确理解它的意义。
那么,暂且抛却“n 个10 相乘”这样的定义方式吧。我们从目前掌握的知识来类推,看看如何定义10º 比较妥当。
众所周知,10³ 是1000,10² 是100,10¹ 是10。
将这些等式放在一起,寻找它们的规律。
每当10 右上角的数字(指数)减1,数就变为原先的10 分之1。因此, 100 就是1。综上所述,在定义10ⁿ(n 包括0)的值时可以遵循以下规则:
指数每减1,数字就变为原来的10 分之1。
10的-1 次方是什么
不要将思维止步于10º 之处。对于10 的-1 次方,让我们同样套用这一规则(指数每减1,数字就变为原来的10 分之1)。
规则的扩展
首先让我们做一个小结。
我们学习了10ⁿ 计数法的相关内容。
起初,我们把n 为1,2,3…时,即10¹,10²,10³…想作“1 个10 相乘” 、“2 个10 相乘”、“3 个10 相乘”……
然后,我们抛却了“n 个10 相乘”的思维,寻找到了一个扩展规则:对于10ⁿ,n 每减1,就变成原来的10 分之1。
当n 为0 时,若套用“10ⁿ 为n 个10 相乘”的规则,着实比较费解。于是我们转而求助于“n 每减1,就变成原来的10 分之1”的规则”,定义出10º 是1(因为10¹ 的10 分之
1 就是1)。
对2º 进行思考
让我们用思考10º 的方法,也思考一下2º 的值吧。
由此可知,对于2ⁿ 来说,n 每减1,数值就变成原来的2 分之1。
2¹ 的2 分之1 是2º,那么2º=1。
在这里我想强调的是,不要将2º 的值作为一种知识去记忆,我们更需要考虑的是,如何对2º 进行适当的定义,以期让规则变得更简单。这不是记忆力的问题,而是想象力的问题。请记住这种思维方式:以简化规则为目标去定义值。
2的-1次方是什么
让我们参照10的-1次方 的规则来思考2的-1次方。2º 除以2,得到的是2的-1次方,即2的-1次方=½。
“2 的-1 次方”在直觉上较难理解。鉴于规则的简单化和一致性,2 的-1 次方可以定义为
综上所述,可以总结出如下等式:
看了上面的等式之后, 你应该就更能体会10º 和2º 为什么都等于1 了吧。
到这里,我们给之前所说的“规则”取名为“指数法则”。指数法则的表达式为
即“N 的a 次方乘以N 的b 次方,等于N 的a+b 次方”法则(但N ≠ 0)。
0 所起的作用
0 的作用:占位
例如,用10 进制表示的2503,它当中的0 起到了什么作用呢? 2503 的0,表示十位“没有”。虽说“没有”,但这个0 却不能省略。因为如果省略了0,写成253,那就变成另一个数了。
在按位计数法中,数位具有很重要的意义。即使十位的数“没有”,也不能不写数字。这时就轮到0 出场了,即0 的作用就是占位。换言之,0 占着一个位置以保证数位高于它的数字不会产生错位。
正因为有了表示“没有”的0,数值才能正确地表现出来。可以说在按位计数法中0是不可或缺的。
0 的作用:统一标准,简化规则
在按位计数法的讲解中,我们提到了“0 次方”,还将1 特意表示成10º。使用0,能够将按位计数法的各个数位所对应的大小统一表示成10ⁿ。
否则,就必须特别处理“1”这个数字。0 在这里起到了标准化的作用。
如果从高到低各个数位的数字依次为
那么10 进制的按位计数法就能用以下表达式来表示:
按位计数法的各个数位也能统一写作
请注意:a右下角的k 和10的指数k 是一致的。
在上述表达式中,设n=3,a₃=2,a₂=5,a₁=0,a。=3,最后的结果是2503。
通过0 来明示“没有”,能够使规则简单化。在许多情况下,规则是越简单越好的。当你在面对问题的时候,是否也可以借助0 来使问题简单化呢?请想一想吧。
日常生活中的0
在我们的日常生活中,有时也会遇到像0 那样表示“没有”的情况。
●没有计划的计划
我们常常使用日程表来管理计划。在日程表中填入“案头工作”、“出差”、“研讨会”等计划。那么,和“0”相当的计划是什么呢?
例如,我们可以将没有计划的状况设定成“空计划”。通过在计算机的日程表中搜索“空计划”,就能找到没有计划的日期。这样一来,我们就既能搜索已有的计划,又能搜索“空计划”了。
还有,我们也可以将“预计不安排计划(即,将该时间空出来)”当作0 来考虑。在日程表中先将“预计不安排计划”的日程填写占位,然后再填写需要安排工作的日程。这样就不至于引起混乱。这正好与按位计数法中的0 起到的占位作用相似。
●没有药效的药
假设现在必须有规律地服用一种胶囊,每4 天停用1 次。也就是3 天服用,1 天停用,3 天服用,1 天停用,按照这种周期循环服药,有难度吧?
灵机一动,妙法自然来。那就每天都吃药吧。只是,每4 粒中有1 粒是“没有药效”的假胶囊。事先准备好标有日期的盒子,并在其中放入每天需要服用的药,不是更好吗?
这样一来,就无需判断“今天是服药日还是停药日”了。正因为有了“没有”药效的药,才形成了“每天服用一粒胶囊”的简单规则。
由此可见,这时的假胶囊与按位计数法中“0”所起的作用相同。
人类的极限和构造的发现
重温历史进程
现今,10进制计数法已经深深地融入了我们的生活。然而这个过程经历了几千年的历史,涉及全世界的各式文明。下面我们就来快速回顾一下数学表述法的这段历史吧。
古埃及人使用5进制和10进制混合的计数法。5和10为一个单元,用记号标识,但是,他们的计数法不是按位计数法,当然也不存在0了。古埃及人将数字记在一种纸莎草纸(papyrus)上面。
巴比伦人在粘土板用菱形记号来表示数。他们使用1和10两种菱形记号来表示1~59,并通过记号的所在位置来表示60ⁿ的数位。由此,10进制和60进制混合的按位计数法就诞生了。现在通用的1小时为60分钟,1分钟为60秒的时间换算就是源于巴比伦的60进制计数法。粘土板和纸莎草纸有所不同,很难在上面书写多种不同的记号,因此,巴比伦人需要以尽可能少的记号来表示数。换句话说,也许正是因为粘土板的硬件限制,才促成了按位计数法的产生。
古希腊人不仅仅把数字当成运算工具,还在其中注入哲学真理。他们将图形、宇宙、音乐与数字相关联。
玛雅人数数时从0开始,使用的是20进制计数法。
罗马人使用5进制和10进制混用的罗马数字,以5为一个单元,记做V。以10为一个单元记做X,同样,将50、100、500、1000记做L、C、D、M。诸如IV表示4,IX表示9,XL表示40等,将数字列在左侧作为减法的表示法是后来制定的,古罗马时并不这样使用。
印度人在引进巴比伦的按位计数法的同时,清楚地认识到0也是数字。而且,他们用的是10 进制计数法。现在我们使用的0、1、2、3、4、5、6、7、8、9,被称为阿拉伯数字而不是印度数字,也许是因为将印度数字传入西欧的是阿拉伯学者的缘故吧。
光是讨论数字的表示法,就已经涉及了如此众多的国家和文明了。
为了超越人类的极限
这里,我们稍微思考一下更深层次的问题。为什么人类需要发明计数法呢?在罗马数字中,将1、2、3 记作I、II、III,将4 写作IIII 或IV,5 写作V。不过,将5 记作IIIII 好像也可以,却又为何不那么做呢?
答案显而易见。原因是:在这种表示方法下,数越大就越难处理。比如,IIIIIIIIII 和IIIIIIIIIII 哪个大?不能马上得知。而X 和XI 就能马上比较得出孰大孰小。如果光将I 排成一排,若要表示较大的数字就非常不便了。因此先贤们创造出了“单元”的概念。
为了表示较大的数而创造出“单元”的概念,看似是一件非常理所当然的事情。而实际上在这里却给了我们极其重要的启发。要表示“十二”,比起IIIIIIIIIIII,用XII 比较方便。若使用按位计数法,写成“12”则更方便。我们可以从中获得哪些启发呢?
那就是:将大问题分解为小“单元”。
如何高效地表示一个较大的数,对于古代的先人们来说是个重要的问题。对此历史给出了两种方法:10 进制计数法和按位计数法。由于人类的能力有限,因此必须开动脑筋,想出简便的计数法。如果人类对数有更高的认知能力,就不会发展出以“单元”表示的计数法了吧。
如今,人类发展到了能够发射火箭、分析基因信息的阶段,我们所处理的数据呈爆炸性增长。这样,按位计数法也显得力不从心了。1000000000000 和10000000000000 哪个大呢?很难一眼就看出来。这时,指数表示法显得异常重要。
刚才的两个数字若写作10的12次方 和10的13次方,便能一眼看出后者较大。指数表示法是着眼于0的个数的计数法。
问题不光停留在计数法上。在现代,我们使用计算机来解决人类难以处理的大规模问题。我们竭尽全力地编写程序,绞尽脑汁地思考如何在短时间内解决大规模问题。“将大问题分解为小‘单元’”的解决办法,至今依然适用。“要解决大问题,就将它分解成多个小‘单元’。如果小‘单元’还是很大,那就继续分解成更小的‘单元’,直到问题最终解决。”这种方法至今依然通用。比如在编写大程序的时候,一般会分解成多个小程序(模块)来开发。
总结
本文通过按位计数法,思考了0 所起的作用。0 虽然没有实际的数量,却起到了占位的作用。正因为有了0,才能够实现简单的按位计数法。
另外,我们还学习了指数法则的相关内容。尤其是思考了如何定义0 次方才更为妥当。一定要在保持简单规则的前提下扩展概念,请大家一定要理解这点。
◎◎课后对话
学生:乐谱上的休止符也像0 呢。
老师:正是!它明确地表示不发音!
学生:0 与其说是“空”,还不如说是“填空”更恰当。因为它的作用是占位。
老师:说得对!这称作占位符。
学生:占位符?
老师:有了占位符才会产生模式,有了模式才会产生简单的规则。
学生:原来如此!正是通过0 这个占位符,才能实现简单的按位计数法!
————
面向读者
本书面向程序员介绍了编程中常用的数学知识,借以培养初级程序员的数学思维。本书主要面向的读者是程序员。不过若你对编程或数学感兴趣,读起来也会一样有意思。
你不需要精通数学。书中不会出现∑和∫等很难的算式,因此自认为数学不太好的读者也完全可以阅读。阅读本书只需具备四则运算(+- ×÷)和乘方(23=2×2×2)等基础知识。除此以外的知识在书中皆有说明。
如果你对数字和逻辑感兴趣,可能会更喜欢本书。
你也不需要精通编程。不过如果稍有一些编程经验,可能会更容易理解本书内容。书中有个别例子是用C 语言写的程序,不过即使不懂C 语言也不妨碍理解。
主要内容
书中讲解了二进制计数法、逻辑、余数、排列组合、递归、指数爆炸、不可解问题等许多与编程密切相关的数学方法,分析了哥尼斯堡七桥问题、高斯求和方法、汉诺塔、斐波那契数列等经典问题和算法。引导读者深入理解编程中的数学方法和思路。本书适合程序设计人员以及编程和数学爱好者阅读。
目录
前言
第1章 0故事——无即是有
第2章 逻辑——真与假的二元世界
第3 章 余数 ——周期性和分组
第4 章 数学归纳法 ——如何征服无穷数列
第5章 排列组合——解决计数问题的方法
第6章 递归——自己定义自己
第7章 指数爆炸——如何解决复杂问题
第8章 不可解问题——不可解的数、无法编写的程序
第9章 什么是程序员的数学——总结篇
相关图书:《程序员的数学2:概率统计》《程序员的数学3:线性代数》