乐读窝

程序员的数学思维修炼

乐读窝 > 科普学习 > 程序员的数学思维修炼

1.2司空见惯的十进制数

书籍名:《程序员的数学思维修炼》    作者:周颖



有没有想过,为什么6+8=14?

从小就这样学的呗!

对,我们小学就开始学“逢十进一,借一当十”,觉得很自然。这就是司空见惯的十进制计数法。



1.2.1 远古的结绳记事


远古时期,人类文明还没有得到发展,但是,“数学”却先于语言、文字而产生。这是因为人们在生活中用到数学的地方很多。例如,每个人捕获猎物的数量,应该怎么表示呢?首先想到的是双手10个手指。天长日久,人类在大自然的生存过程中,积累了更多的生存经验。随着人类征服自然、适应自然的能力逐步提高,捕获或养殖的动物数量也逐步增加,此时靠双手的十个手指来计数就不够了。从史料来看,此时人类进一步的做法是排石子、划道道等。此时,数数还不会,计算更是谈不上。

在我国民间有一种传说,认为伏羲氏始创了结绳记事的方法。结绳记事,是在绳子上打一个结来表示一个数,如图1-10所示,“事大大其绳,事小小其绳,结之多少,随物众寡”,这在当时所起的作用是非常大的。

图1-10

随着人类文明的进步,人类将一只羊、两只羊、三只羊……这些具体的概念抽象化,得到了数字1、2、3……,只是当时的表现方式有所不同,如图1-11所示。从图中可看到,巴比伦数字类似于按数量摆放石子,而中国数字类似于画痕,罗马数字进一步抽象,用Ⅴ表示数字5,如果在其左侧有一竖,表示为4(=5-1),若在其右侧有一竖,表示为6(=5+1),右侧有两竖,表示为7(=5+2),依次类推。在罗马数字中用Ⅹ表示10,根据其左侧或右侧的竖线数量来表示低于10或大于10的数。现在罗马数字仍在很多地方使用。

图1-11

阿拉伯数字则是现今国际通用的数字,最初由印度人发明,后由阿拉伯人传向欧洲,之后再经欧洲人将其现代化。正因阿拉伯人的传播,成为该种数字最终被国际通用的关键点,所以人们称其为“阿拉伯数字”。阿拉伯数字由0、1、2、3、4、5、6、7、8、9共10个计数符号组成。采取位值法,高位在左,低位在右,从左往右书写。借助一些简单的数学符号(小数点、负号等),这个系统可以明确地表示所有的有理数。为了表示极大或极小的数字,人们在阿拉伯数字的基础上还创造了科学记数法。



1.2.2 什么是十进制计数


正如本节开始时所说,十进制计数法是我们司空见惯的,从小学习的就是十进制。那么,什么是十进制计数?

十进制数基于位进制和十进位两条原则,即所有的数字都用10个基本的数字表示,满10进1,同时同一个数字在不同位置上所表示的数值大小不同,因此数字的位置非常重要。

十进制的基本数字是0、1、2、3、4、5、6、7、8、9。要表示这10个数字的10倍,就将这些数字左移一位,右侧用0补上空位,即可得到10、20、30、……90(0的10倍还是0),如图1-12所示。若要继续扩大10倍来表示数字,就继续左移数字的位置,然后在右侧用0补上空位,即100、200、300……。

图1-12

要表示一个数的十分之一,百分之一,千分之一,就将数字向右移,在左侧(小数点右侧)补上0,即可得到十分位(0.1)、百分位(0.01)、千分位(0.001),如图1-13所示。

图1-13



1.2.3 为啥人类习惯十进制


为什么我们从小学习的就是十进制,而不是更简单的二进制?

首先,看看我们的双手,我们有10根手指,如图1-14所示。从人类最初计数时起,首先想到的就是用双手的手指来计数,数满10个数再增加一双手,这样就产生了十进制。

图1-14

另一个很重要的方面,就是习惯。我们从小接受的教育就是使用十进制数进行计算,因此习惯了十进制数的运算。

二进制的运算规则比十进制简单,为什么不使用二进制呢?这是因为二进制的运算规则虽然简单,但是要表示一个较大的数据时,需要用很长一串数据,如十进制的50000写成二进制为1100  0011  0101  0000,一共需要16位,谁一眼能看出该数据的大小?

如果我们一直使用二进制,可能对二进制表示的数也能方便地识别出来,但是和十进制相比可以看出,十进制数比二进制数更简洁,更易识别。

而比十进制更大的进制(如十六进制),其运算规则复杂,更难以使用。

因此,在日常生活中是以十进制数为主。



1.2.4 十进制运算规则


十进制数的常用运算包括加、减、乘、除这4种,也称为四则运算。在初等数学中,当一级运算(加、减)和二级运算(乘、除)同时出现在一个算式中时,它们的运算顺序是先乘除,后加减。要改变这种运算规则,则需要通过括号,因为四则混合运算中,总是先计算括号内,然后再计算括号外。同一级运算顺序则是按从左到右的顺序进行。

在加、减、乘、除这4种运算中,加、减法互为逆运算,乘、除法互为逆运算,而乘法是加法的简便运算,如图1-15所示。

图1-15

1.加法

加法运算是把两个数合并成一个数的运算,可以将整数、小数、分数进行合并运算。在加法运算中,首先应当将相加的数从个位开始按位对齐,然后从个位开始(从右向左)逐位相加。加法运算时,两数(或多数)相加的和超过10时就向前一位进位,这种规则称为“逢10进1”,如图1-16所示。

图1-16

2.减法

减法运算是已知两个加数的和与其中一个加数,求另一个加数的运算。在减法运算中,首先应当将相减的数从个位开始按位对齐,然后从个位开始逐位相减。如果对应位上被减数小于减数时,需向被减数前一位进行借位,借1当10,再和本位的数相加,得到一个超过10的数,再用这个数与减数进行运算即可,如图1-17所示。

图1-17

3.乘法

乘法运算是求几个相同加数的和的简便运算。乘法运算比加、减法更复杂,不过,对于十进制数的乘法运算来说,只需要背熟九九乘法表,并按此规则逐位相乘,然后再将各位乘积进行累加,即可得到最终结果,如图1-18所示。

图1-18

4.除法

除法运算是已知两个因数的积与其中一个因数,求另一个因数的运算。

除法法则:除数是几位,先看被除数的前几位,前几位不够除,多看一位,除到哪位,商就写在哪位上面,不够商1时,要用0占位。除法可能会有余数,余数要比除数小。如果商是小数,商的小数点要和被除数的小数点对齐;如果除数是小数,要将其化成整数后再用整数的除法进行计算。

除法是乘法的逆运算。图1-18所示的乘法算式,可表示成如图1-19所示的除法算式:

图1-19



1.2.5 十进制数的分解


十进制数由0~9这10个数字组成,依据数字所在位置决定数值的大小。数据的各位从右向左依次为个位、十位、百位、千位……。

如图1-20所示,个位的9表示有9个1,十位的8表示8个10,百位的7表示7个100,按这种方式,可将十进制数按图1-21所示方式进行分解。

图1-20

图1-21

仔细看图1-21所示数据的分解式,从右向左,每个数码都比其右侧的数大10倍,可将上式简写成图1-22所示的形式。

图1-22

十进制是我们从小就开始学的,以上这些规则都很简单,为什么还要在这里重复呢?因为程序员通过十进制的这些运算规则可推导出其他进制数的运算规则,也可设计解决更多的问题,例如大整数的运算问题。



1.2.6 20!等于多少


在设计大整数之前,我们先来看一个例子。以下是一个C语言程序,该程序中定义了一个计算整数阶乘的函数fact(),在主函数中调用fact()函数计算1~20各数的阶乘。

运行以上程序,得到如图1-23所示的结果。

图1-23

从图1-23中可以看出,14!的结果比13!的结果还小,肯定出问题了。为什么会出现这种错误呢?电脑连15的阶乘都计算不出来?

分析程序代码可看出,程序中使用的是int类型的变量,在C语言中,这种变量保存的数据范围为-2,147,483,648~2,147,483,647,而13!再乘以14,其结果已经超过int类型的表示范围(其实,13!的值应该为6,227,020,800,已经超过了int类型的表示范围),因此,数据就出错了,从17!、18!还可以看出其结果变成了负数。

既然知道了出错原因是由于数据类型导致的,那么,是不是将以上程序的int类型改变为位long类型,就可以计算更大数的阶乘了呢?理论上是这样,不过,由于ANSI  C中规定,在字长为32位的计算机中,int类型和long类型都是32位。因此,这里将数据类型修改为long也不能解决问题。

在支持64位字长的C#系统中,long类型使用64位二进制位表示(8个字节),其表示的数据范围为-9,223,372,036,854,775,808~9,223,372,036,854,775,807。但是,这么大的数在阶乘面前也很快就被会填满,图1-24所示为使用C#计算各数阶乘的输出结果。

从图1-24中可看到,使用64位字长的long类型,20的阶乘也可以被正确表示出来了。但是更大的数呢?21!、22!的结果是多少?

图1-24

哦,My  God!还是出错了,21的阶乘就变成负数了。这还是long类型的表示范围问题,long类型的表示范围为-9,223,372,036,854,775,808~9,223,372,036,854,775,807。

对于基本的整数类型,使用ulong(无符号长整型)类型来保存数据,其表示范围也只为0~18,446,744,073,709,551,615,再大的数就没办法表示了。那么,更大数的阶乘该怎么办呢?



1.2.7 大整数构想


在实际应用中,除了阶乘之外,还有很多地方需要使用到非常大的整数,而计算机程序设计语言对数据的表示范围总是有限的。因此,还得我们程序员自己想办法,设计一个能处理大整数的类,这个类应该能处理任意位数长度的整数。

根据本节前面对十进制数的分析,可以很容易地想到,可以在程序中用一个数组来表示大整数的各位,由于数组元素的多少只受计算机内存限制,因此,就可以处理任意长度的大整数了。

如图1-25所示,定义一个数组,然后将数据的各位分解到数组的各个元素中。

图1-25

这个数组只是大整数的一种表示形式,要处理大整数,还需要记录数据的正负、加减时的进位等。然后定义以下常用操作:

加法:将整数从个位开始(即数组的0号元素)进行累加。累加时还需判断是否要进位(逢10进1),因此,在累加时还需要将进位数进行累加。

减法:将整数从个位开始(即数组的0号元素)进行减法操作,若不够减还需要从前一位借位(借1当10)。被减数在进行减法操作时还需要减去被借的位。

乘法:按图1-18所列的乘法算式,可将乘数中的每一个数组元素与被乘数相乘,然后将结果累加,即可得到大整数相乘的结果。当然,在进行加法和乘法运算时也需要考虑进位的情况。

除法:除法的实现要麻烦一点。首先要考虑试商的问题,从被除数的高位开始与除数对齐,试商时用被除数的部分位减去除数,判断能减几次,就可商几。另外,除法还需要考虑余数问题。除法的过程如图1-26所示。

图1-26

从图1-26的演算过程可看到,除法运算需要循环调用加法运算进行试算,然后再调用减法运算计算试商后的余数。

根据这个构想编写大整数处理函数,即可处理任意长度的整数(如可保存、计算长度为100位、200位甚至更多位整数的加、减、乘、除运算),不再局限于C语言所提供数据类型中有限的整数长度了。

有幸的是,在微软的.NET  Framework  4(以及JAVA的JDK  1.5)中已经提供了一个大整数类型,可以处理任意长度的大整数。如果使用.NET  Framework  4进行开发,就不用自己编写大整数类型了。当然,如果在ANSI  C环境下编写程序,仍然可以按本节介绍的构思编写自己的大整数类型。