 
        书名:程序员的数学基础Python实战
ISBN:978-7-115-59773-1
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 [日]谷尻香织
译 郭海娇
责任编辑 赵祥妮
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e59773”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。
数学知识对编程很有用,但是很多写给程序员的数学书都比较难。我们为什么不从基础的数学知识开始学习呢?
本书尽力在计算机的世界中,告诉大家“数学可以怎样用”或者“数学可以解决什么问题”,还尝试用简单的Python程序来展示数学的实际应用效果,帮助大家找到一种“原来如此”的感觉,从而掌握相关的数学知识。本书不仅解释了数学理论,还解释了Python程序中的计算、证明和理论验证。
本书的主要目标是让数学变得易懂!程序员或者是想要成为程序员的高中生、大学生,以及对机器学习和人工智能感兴趣的初学者,甚至是数学基础薄弱的普通读者都适合阅读本书。
我想再学一次数学——不管是什么原因,这都是拿起这本书的人的共同愿望。当然,如果你想认真学习数学,可能数学教科书是比较合适的。但你不会为了考试而学习吧。
你可能是程序员或有志于学习编程的人。在实际编程中,有些人可能会想:“我应该好好学习数学……”你可能买了一本书来学习机器学习或人工智能,但完全不知道上面写的是什么!有些人可能已经感到非常沮丧了。我写这本书,正是希望能帮助这样的人。
本书所涉及的知识只是从小学到高中毕业所学数学的一小部分。与数学教科书不同的是,这本书的重点不在于如何解题,而在于我在学生时代一直有的疑问—这东西哪里会用到呢?或者这到底有什么用?我试图用“在计算机世界里,你可以这样使用它”“可以通过使用它来做这些事情”来回答这些问题。
此外,本书并不是一味地让读者看书,还会用Python创建一些简单的程序,这样读者就可以看到程序是如何工作的。通过尝试改变变量的值或改变程序中表达式的某一部分,看看结果是如何改变的,读者会对数学有更深入的理解。同时,读者应该能够摸索出一些在程序中实现数学公式的技巧。
同样,这也不是一本解决数学问题的书。本书讲述的是数学在我们周围的世界,特别是在计算机世界中的应用,旨在通过具体的例子和实践,帮助读者把数学知识内化于心。从本质上讲,数学是一门美丽的学科,因为它是不含糊的。但本书的主要目标是让数学变得通俗易懂,强调的是易懂和可读性。因此,如果读者认真学习过数学,可能会发现书中的一些地方写得不够严谨。我希望读者能忽略它们,不要在意这些细节。
最后,感谢电气通信大学信息科学与工程学院信息与网络工程专业的关口裕太先生在本书写作过程中提出的宝贵意见。在此,我向他表示衷心的感谢。
谷尻香织
人类虽然在计算能力方面不如计算机,但却有分析和总结各种现象的能力。计算机虽然可以在瞬间完成非常复杂的计算,但无论多么先进的计算机都不能自己思考。为了让人类和计算机能够更好地合作,人类有必要了解不会思考的计算机。那么,我们先来看看计算机是如何处理数字的。
进制计数法是一种表示数字的方法。我们常用的是十进制计数法,计算机使用的是二进制计数法,两者的区别在于计数时可以使用的数字个数。
我们通常按照十进制计数法的规则来表示数字。这是一种基于以下规则的数字表示方法。
● 使用10个数字:0,1,2,3,4,5,6,7,8,9。
● 排列的数字从右到左依次代表个位、十位、百位……
我们数数时,依次数1,2,3,…过了9,再继续,就是10,11,12,…因为我们用的是10个不同的数字,所以我们把它叫作十进制计数法。用这个规则表示的数字叫作十进制数。
例如,数值“2365”,它表示的不是“2”“3”“6”“5”这4个数字,而是
2个1000
3个100
6个10
5个1的总和。
用公式表示的话就是 。
。
1000、100、10、1称为权重,它们是赋予每个数字意义的重要数值。为了理解权重的含义,我们再把上面的公式转换一下。
大家是否注意到,所有数字的权重都是“10的n次方”。另外,10的右肩上的小数字(被称为“指数”)从右向左逐渐增加:0,1,2,3,…这意味着在十进制计数法中,每向左移动一位,权重就变为上一位的10倍。
作为权重基础的“10”是十进制计数法中的“10”,这个数值被称为底数。在后面要解释的二进制计数法和十六进制计数法中,底数分别为“2”和“16”。
Python有一个指数运算符“**”,可以计算m的n次方。比如10的3次方就是
>>> 10**3
1000同样的方式,还可以尝试计算10的0次方或2的0次方。结果永远是1。是不是觉得这很奇怪呢?
>>> 10**0
1
>>> 2**0
110n(10的n次方)表示“n个10相乘”。如果我们遵循这一规则,那么101(10的1次方)是10就说得通了。但100(10的0次方)呢?用0个10相乘,所以是10×0吗?而结果并不是0。10×0是10乘以0,与0个10相乘的意义完全不同。
很难想象如何实现0个10相乘。
如果10的指数减少1,那么这个新的数值就是原数值的 。按照这种计算方式,10的0次方就是“1”(见图1-1)。
。按照这种计算方式,10的0次方就是“1”(见图1-1)。
用同样的方法,我们看看底数为2的情况(见图1-2)。如果2的指数减少了1,新数值就会变为原数值的 ,所以20(2的0次方)就是1。换句话说,无论底数是多少(除0以外),它的0次方都是1。
,所以20(2的0次方)就是1。换句话说,无论底数是多少(除0以外),它的0次方都是1。
图1-1 指数变小(十进制)
图1-2 指数变小(二进制)
现实中我们采用十进制计数法来表示数字,但在计算机世界中,使用的是二进制计数法。基于以下规则表示的数字称为二进制数。
● 使用0和1两个数字。
● 排列的数字从右到左依次代表20,21,22,23,…。
为什么计算机使用二进制计数法呢?因为计算机是靠电运行的机器。当电流流过灯泡时,灯泡就会亮;当电流不流过灯泡时,灯泡不亮。当然,计算机中并没有灯泡,它用的是电子元器件,但原理是一样的,即用流经电子元器件的电信号表示开和关。所以,计算机只能处理两个电信号,即数字中的“1”和“0”。
让我们试着像计算机一样计数,从0开始,然后是1。当用完所有可用的数字后,让我们向左移动1位数字到10,然后是11。再向左移动到100,然后是101,110,111,…二进制数的读法是“一”或“零”。例如,“10”读作“一,零”,而“100”则读作“一,零,零”。
Try Python 十进制数转换为二进制数
在Python中,可以使用bin()函数将一个十进制数转换为二进制数。
>>>bin(10)  ← 将十进制数10转换为二进制数
'0b1010'    ← 转换后的结果显示在结果开头的“0b”代表该值是一个二进制数。这个符号是Python语法规定的。当然还有其他的符号规定,如“0x”代表十六进制数,“0o”代表八进制数。
专栏 进制与进制数
表示数字的方法有很多种,如十进制计数法和二进制计数法。在本书中,十进制计数法和二进制计数法指各自进制的计数方法。
而“十进制数”或“二进制数”指的是计数方法中的数字所表示的值。在本书中,我们使用“二进制1101”或“二进制数1101”这样的表达方式,它们的意思是一样的。
二进制对计算机来说是一种非常方便的计数方法,但对我们来说很难使用,因为数字往往很长。而十六进制计数法为我们解决了这个问题。按以下规则所表示的数字被称为十六进制数。
● 使用16个字符(字母可使用小写):0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F。
● 排列的数字从右到左依次代表160,161,162,163,…。
我们不惜用字母也要用十六进制,是因为1位十六进制数字可以代表4位二进制数字。例如,我们很难一下数出在“1111111111”中有多少个1,但如果是FF呢?如果我们懂得十六进制的话,就可以在脑中快速将其转换为“1111 1111”。事实上,十六进制是一种很方便的计数方法,它在使用二进制计数的计算机和我们熟悉的十进制计数之间起了桥梁的作用。
表1-1展示了不同计数法表示的0~31。
表1-1 不同计数法表示的0~31
| 十进制 | 二进制 | 十六进制 | 
|---|---|---|
| 0 | 0 | 0 | 
| 1 | 1 | 1 | 
| 2 | 10 | 2 | 
| 3 | 11 | 3 | 
| 4 | 100 | 4 | 
| 5 | 101 | 5 | 
| 6 | 110 | 6 | 
| 7 | 111 | 7 | 
| 8 | 1000 | 8 | 
| 9 | 1001 | 9 | 
| 10 | 1010 | A | 
| 11 | 1011 | B | 
| 12 | 1100 | C | 
| 13 | 1101 | D | 
| 14 | 1110 | E | 
| 15 | 1111 | F | 
| 16 | 10000 | 10 | 
| 17 | 10001 | 11 | 
| 18 | 10010 | 12 | 
| 19 | 10011 | 13 | 
| 20 | 10100 | 14 | 
| 21 | 10101 | 15 | 
| 22 | 10110 | 16 | 
| 23 | 10111 | 17 | 
| 24 | 11000 | 18 | 
| 25 | 11001 | 19 | 
| 26 | 11010 | 1A | 
| 27 | 11011 | 1B | 
| 28 | 11100 | 1C | 
| 29 | 11101 | 1D | 
| 30 | 11110 | 1E | 
| 31 | 11111 | 1F | 
现在让我们看看如何将二进制数字转换成十六进制数字。
● 在进制计数法中,从右边开始表示数字。
● 4位二进制数字等于1位十六进制数字。
基于这两点,我们首先可以将二进制数字从右边开始每4位分成一组。然后,用一个十六进制的数字替换这4位数字。如果数字少于4位,在左边补0。如“101”,在左边补一个0,使之成为“0101”;“11010”,在左边补3个0,使之成为“0001 1010”,然后用十六进制数字替换前4位和最后4位(见图1-3)。
图1-3 从二进制转换到十六进制
Try Python 十进制、二进制数转换为十六进制数
可以使用hex()函数将十进制和二进制数转换为十六进制数。如前所述,Python规定十六进制数的前缀为“0x”,二进制数的前缀为“0b”。
>>> hex(28)       ← 将十进制数28转换为十六进制数
'0x1c'            ← 显示的结果
>>> hex(0b11010)  ← 将二进制数11010转换为十六进制数
'0x1a'            ← 显示的结果十进制转换到二进制,二进制转换到十进制……用不同的计数方法来表示同一个数字,就是进制转换。
在十进制数2365中,千位是2,百位是3,十位是6,个位是5。为什么是“2”“3”“6”“5”?算一算就知道了。你认为我应该做什么样的计算?“这是2365,你看到它就知道了!”这个回答不够好。请看图1-4并思考一下。
图1-4 十进制数的每位的数值
答案是“原值重复除以底数后的余数”。如果是十进制数,则除以10,余数是个位的数值;接下来,用商除以10,余数就是十位的数值;然后用商除以10……重复这个过程,直到商变成0,你将得到每位的数值。计算结束后,再按从右到左的顺序排列余数,结果又变成了原来的数字!
当把一个十进制数(如26)转换为二进制数时,要除以2,这是转换的基数。计算到商变成0后,将余数从右到左依次排列,就可以用二进制表示原来的十进制数(见图1-5)。
图1-5 十进制转二进制
Try Python 十进制到二进制的转换程序
下面让计算机做图1-5所示的工作。代码1-1中的dec2bin()函数将一个十进制数转换为二进制数,这就是Python的bin()函数在内部所做的工作。在执行dec2bin()函数时,target参数必须是一个十进制的数字。例如,要将26转换为二进制数,可以用以下代码实现。可见,执行bin()函数后结果的显示方式与执行dec2bin()函数后的不同,但数字的排列顺序是一样的。
>>> dec2bin(26)       ← dec2bin()函数将十进制数26转换为二进制
[1, 1, 0, 1, 0]       ← 显示的结果
>>> bin(26)            ← Python的bin()函数将26转换为二进制 
'0b11010'              ← 显示的结果代码1-1 将十进制数转换为二进制数
 1. def dec2bin(target):
 2.     amari = []   # 放余数的列表
 3. 
 4.     # 直到商为0
 5.     while target != 0:
 6.         amari.append(target % 2) # 余数     ← ①
 7.         target = target // 2     # 商
 8.
 9.     # 按相反顺序返回列表中的元素
10.     amari.reverse()                        ← ②
11.     return amari让我们看一下程序的内容。amari是一个空列表,用来存放余数。①的while循环会一直循环到target的值变成0。
amari.append(target%2) ← 将target除以2的余数添加到amari中
target = target // 2   ← 用target除以2得到的商覆盖target退出while循环后的②是一个命令,将amari的元素按相反的顺序重新排列,实现从右到左对余数进行排序(见图1-6)。
图1-6 将列表中的元素按相反顺序排列
把十进制数除以2后可以转换到二进制,同样从十进制转换到十六进制也是如此。将原值重复除以16直到商为0,并将余数按从右到左的顺序排列。除以16后的余数将是0~15。其中,10~15需用A~F代替(见图1-7)。
图1-7 将十进制数转换为十六进制数
Try Python 十进制到十六进制的转换程序
代码1-2显示了一个将十进制数转换为十六进制数的程序。除了把余数10~15转换为A~F(①的for循环)的部分,它几乎与代码1-1相同。target参数必须是一个十进制的数字。
>>> dec2hex(26)  ← dec2hex()函数将十进制数26转换为十六进制数
 [1, 'A']        ← 显示的结果
>>> hex(26)      ← Python的hex()函数将26转换为十六进制数 
 '0x1a'          ← 显示的结果代码1-2 将十进制数转换为十六进制数
 1. def dec2hex(target):
 2.     amari = [] # 放余数的列表
 3.
 4.     # 直到商为0
 5.     while target != 0:
 6.         amari.append(target % 16) # 余数
 7.         target = target // 16 # 商
 8.
 9.     # 用A~F替换余数的10~15 
10.     for i in range(len(amari)):                    ← ①
11.         if amari[i] == 10:       amari[i] = 'A'
12.         elif amari[i] == 11:     amari[i] = 'B'
13.         elif amari[i] == 12:     amari[i] = 'C'
14.         elif amari[i] == 13:     amari[i] = 'D'
15.         elif amari[i] == 14:     amari[i] = 'E' 
16.         elif amari[i] == 15:     amari[i] = 'F' 
17.
18.     # 按相反顺序返回列表中的元素
19.     amari.reverse()
20.     return amari现在让我们把二进制或十六进制的数字转换成十进制的数字。实际上,同样的规则也适用于所有从其他进制的数字到十进制的转换。该规则如下。
● m进制的“m”为底数。
● 一个数字的权重用“m的n次方”表示,n的值从右到左依次为0, 1,2,3,…即“位数−1”。
这个规则也是进制计数法的特点(见图1-8)。
图1-8 m进制数字的权重
例如,将4个数字“2”“3”“6”和“5”转换成十进制数“2365”,可以使用如下计算方法。
(103×2) + (102×3) + (101×6) + (100×5)
=2000+300+60+5
= 2365
该计算方法也适用于将二进制或十六进制数转换为十进制数。在二进制中,底数是2,所以11010就是
(24×1) + (23×1) + (22×0) + (21×1) + (20×0)
=16+8+0+2+0
=26
它在十进制中的数值为26。另外,十六进制数1A转换为十进制数后值也为26,计算方法如下。
(161×1) + (160×10)
=16+10
=26
Try Python 其他进制到十进制的转换程序
Python的int()函数可以用来将其他进制数转换为十进制数,该函数有两个参数,第一个参数是要转换的数值的字符串,第二个参数是底数。
>>> int('0b11010', 2) ← 将二进制数11010转换成十进制数
26                    ← 显示的结果
>>> int('0x1A', 16)   ← 将十六进制数1A转换成十进制数
26                    ← 显示的结果当然,我们可以直接使用int()函数实现到十进制的转换,但现在我们已经知道算法了,让我们编写一个从其他进制到十进制的转换程序,这就是代码1-3。代码中有两个参数,target参数指定转换前的值为一个字符串,m参数指定底数。 注意,只有二进制和十六进制的数字可以用这个程序转换为十进制。
>>> any2dec('11010', 2) ← 将二进制数11010转换成十进制数
26                      ← 显示的结果
>>> any2dec('1A', 16)   ← 将十六进制数1A转换为十进制数
26                      ← 显示的结果代码1-3 将一个m进制数转换为十进制数
 1. def any2dec(target, m):
 2.     n = len(target)-1 # 指数的最大值                  ← ①
 3.     sum = 0 # 转换为十进制的值
 4.
 5.     # 逐一检查target中的字符
 6.     for i in range(len(target)):                      ← ②
 7.         if target[i] == 'A':     num = 10 
 8.         elif target[i] == 'B':   num = 11
 9.         elif target[i] == 'C':   num = 12 
10.         elif target[i] == 'D':   num = 13
11.         elif target[i] == 'E':   num = 14
12.         elif target[i] == 'F':   num = 15
13.         else:                    num = int(target[i])
14.
15.         sum += (m ** n) * num # 各位的值和其对应权重乘积的总和
16.         n -= 1              # 指数减1
17.      return sum①的变量n是在计算一个数字的权重时使用的值(指数)。不管底数是多少,最高位的指数都是“位数−1”。len()函数用来计算字符串的字符个数。
在②的for循环中,逐一检查target中的字符,如果它们是A~F,则对应数值为10~15。如果不是,即为0~9,数值被转换后进行如下计算。
sum += (m ** n)*  num  ← 各位值和其对应权重乘积的总和
n -= 1                 ← 指数减1
退出for循环后的sum值就是转换后十进制的值。
为了加深读者对数学知识的理解,本书将使用擅长计算的计算机来进行各种计算。为了避免丢失重要数据,让我们先看看计算机世界是如何处理数据的。关键概念是比特(位)和字节。
位是计算机可以处理的最小信息量的单位,代表二进制数的一个数字。当8位的数字“聚集”在一起时,就是1字节(Byte)。换句话说,1字节可以处理8位二进制数,2字节可以处理16位二进制数(=2×8)。
为了有效地进行计算,计算机在一个固定大小的“容器”中读取和写入数据,这个容器大小的表示单位是字节。如下面的代码,在运行过程中计算机会准备一个名为“a”的容器(为了说明问题,图1-9用1字节表示容器的大小。然而,一般的编程语言将整数处理为4或8字节),并将00000110分配给它(见图1-9)。十进制数6在二进制中是110,但计算机使用字节作为其基本单位,左边用0填满,整个8位都被使用。
>>>a = 6图1-9 1字节为8位
现在我们假设数字11111111在1字节的容器中。如果我们给它加1,这个数字就会上升一位,变成100000000,但这个容器的大小只有1字节(8位)。在这种情况下,缺失的数值被丢弃,只有右边的8位数字有效(见图1-10)。
图1-10 溢出
这种情况被称为溢出。请注意,无论计算机的计算多么正确,如果发生溢出,就无法得到正确的答案。
溢出是由于容器对所处理的数据来说太小。对于图1-10所示的情况,使用一个2字节的容器可以避免溢出(见图1-11)。
图1-11 2字节是16位
专栏 填充0的意义
在进制计数法中,从最右边的数字开始依次有意义,所以插入数值时,右对齐是基本的要求(见图1-12)。在我们平常使用的纸张上,将多余的数字留空是没有问题的,但在计算机世界中,信息是以字节为单位进行处理的,空位要用0来填充。0是一个重要的数值,表示这个数位上没有值。
图1-12 0的作用
用4位十进制数可以表示多少个数呢?因为每位可以用0~9共10个数字表示,所以答案是10×10×10×10,即10000(见图1-13)。当然,10000不是4位十进制数,因为它有5个数字。所以,用4位十进制数可以表示0~9999共10000个数字。
如果是二进制的4位数呢?二进制中每位可以用0和1两个数字表示,所以答案是 2×2×2×2=16(见图1-14)。
图1-13 可由4位十进制数表示的数值数量
图1-14 可由4位二进制数表示的数值数量
十进制中的10或二进制中的2按位数自乘所得到的值就是该进制所能处理的数值的数量。如果我们把数值限制在0及0以上(正数),那么可以处理的数值范围如下。
0~mn−1(m为底数,n为位数)
以二进制为例,可处理的数值范围如表1-2所示。
表1-2 可处理的数值范围
| 字节数 | 位数 | 值的数量 | 值的范围 | 
|---|---|---|---|
| 1 | 8 | 28 = 256 | 0~225 | 
| 2 | 16 | 216 = 65536 | 0~65535 | 
| 4 | 32 | 232 = 4294967296 | 0~4294967295 | 
| 8 | 64 | 264 = 18446744073709551616 | 0~18446744073709551615 | 
一般我们使用符号“−”来表示负数,如−5、−10,但在计算机中不能使用“−”(因为所有的信息都由0和1表示),而是使用符号位来表示负数。
用十进制计算加1后可以得到0的数,方程如下。
x+1=0
x= −1
如果换用8位二进制数来计算,与0000 0001相加得到0000 0000的数值是多少呢?请参考图1-15并思考一下。
图1-15 1加1111 1111
1111 1111加上0000 0001,可以得到1 0000 0000,结果值的位数增加了。但我们这里的第一个条件是必须用8位二进制数进行计算,如果放弃溢出的数字,结果是0000 0000。
加1后变成0的值只有−1。而十进制数字−1用二进制表示为1111 1111。
根据1.2.3小节的内容,二进制数字1111 1111转换为十进制后为数字“255”。结果显然不是“−1”,是不是感到有些困惑呢?
让我们把二进制的数字减少到4位。表1-3显示了当数值a为0~15时,a+x=0成立的x值(见图1-16)。如果我们仔细观察可以看到,二进制的x列除了0000之外,是按a列的值降序排列的。
表1-3 a + x=0成立时的x值
| 十进制 | 二进制 | ||
| a | x | a | x | 
| 0 | 0 | 0000 | 0000 | 
| 1 | −1 | 0001 | 1111 | 
| 2 | −2 | 0010 | 1110 | 
| 3 | −3 | 0011 | 1101 | 
| 4 | −4 | 0100 | 1100 | 
| 5 | −5 | 0101 | 1011 | 
| 6 | −6 | 0110 | 1010 | 
| 7 | −7 | 0111 | 1001 | 
| 8 | −8 | 1000 | 1000 | 
| 9 | −9 | 1001 | 0111 | 
| 10 | −10 | 1010 | 0110 | 
| 11 | −11 | 1011 | 0101 | 
| 12 | −12 | 1100 | 0100 | 
| 13 | −13 | 1101 | 0011 | 
| 14 | −14 | 1110 | 0010 | 
| 15 | −15 | 1111 | 0001 | 
图1-16 计算a+x=0
二进制x列中显示的数字被称为二进制补码,可以通过对二进制数的0和1取反,并在取反后的数值上加1来获得。例如,对二进制数0001的所有数字取反,会得到1110,再加1,就会得到1111这与十进制数15的数值相同。二进制补码可以作为负数用于计算,这有点让人不可思议,让我们看看其计算过程。
图1-17显示了5−3的计算过程。在将每个值转换为二进制数之后,减数用二进制补码转换。因为5−3可以看作5+(−3)这样的加法来计算。最后,如果我们将二进制数0101和1101相加,就可以得到1 0010,但由于是4位二进制计算,所以要放弃溢出的数字部分,结果就是0010,转换为十进制数就为2。
图1-17 5−3的计算过程(十进制)
换一个计算示例,如3−5(见图1-18)。把它看作3+(−5),把5表示为二进制补码进行计算。0011和1011相加,可以得到1110。对照表1-3中的二进制的x列,你会发现1110是2的二进制补码,也就是−2。然而,如果看一下列,就会发现1110转换为十进制是14。
图1-18 3−5的计算过程(十进制)
正如我们所看到的,对于计算机来说,采用二进制补码是非常方便的,因为可以用加法运算来做减法。然而,我们不希望该值有两种不同的解释。这就要靠符号位了。
我们制定了在数字前加“−”表示负数的规则,但计算机只能处理0和1。因此,我们把二进制数最左边的数字称为“符号位”。当符号位为0时,代表正数;为1时,代表负数(见图1-19)。
图1-19 符号位
根据这一规则,二进制补码只能以一种方式解析,所以,4位二进制数表示的数值也就确定了,如表1-4所示。
表1-4 由4位二进制数表示的数值
| 二进制 | 十进制 | 
|---|---|
| 0000 | 0 | 
| 0001 | 1 | 
| 0010 | 2 | 
| 0011 | 3 | 
| 0100 | 4 | 
| 0101 | 5 | 
| 0110 | 6 | 
| 0111 | 7 | 
| 1000 | −8 | 
| 1001 | −7 | 
| 1010 | −6 | 
| 1011 | −5 | 
| 1100 | −4 | 
| 1101 | −3 | 
| 1110 | −2 | 
| 1111 | −1 | 
用8位(1字节)的二进制数可以表示256(= 28)个值。如果其中1位用于符号位,就只剩下7位,如果用7位数字似乎看起来能表示的数值会减少至128(= 27)个。但由于正数和负数两种类型都需要用符号位表示,1字节仍然可以表示256(=128×2)个值(见图1-20)。
图1-20 8位二进制数可表示的数值范围
如果二进制数的位数为n,则可以处理的数值范围的计算方法为−2n−1~2n−1−1,常用数值范围如表1-5所示。
表1-5 常用数值范围
| 字节数 | 位数 | 数值个数 | 数值范围 | 
|---|---|---|---|
| 1 | 8 | 27 = 128 | −128~127 | 
| 2 | 16 | 215 = 32768 | −32768~32767 | 
| 4 | 32 | 231 = 2147483648 | −2147483648~2147483647 | 
| 8 | 64 | 263 = 9223372036854775808 | −9223372036854775808~9223372036854775807 | 
专栏 处理数字的数据类型
Python使用int(整数)和float(浮点数)数据类型来处理数字。这两种数据类型都可以处理负数。
>>> a = 100   ← 将100分配给变量a
>>> a         ← 确认a的值
100           ← 显示的结果
>>> type(a)   ← 确认变量a的数据类型
<class 'int'> ← 显示的结果
>>> a = -100  ← 将-100赋给变量a
>>> a         ← 确认a的值
-100          ← 显示的结果由于Python的int类型没有固定的大小,它可以处理的数值范围没有限制。然而,在大多数编程语言中,每个数据类型都有自己的大小(以字节为单位)。此外,即使是相同大小(字节数)的数据,也有两种类型,即“有符号”和“无符号”类型,这取决于是否使用符号位。例如,表1-6显示了C语言中处理整数的主要数据类型。请注意,每种类型可以处理的数值范围是不同的。
表1-6 C语言中处理整数的主要数据类型
| 数据类型 | 
 | 大小(字节数) | 数值范围 | 
|---|---|---|---|
| 有符号 | char | 1 | −128~127 | 
| 
 | short | 2 | −32768~32767 | 
| 
 | long | 4 | −2147483648~2147483647 | 
| 无符号 | unsigned char | 1 | 0~255 | 
| 
 | unsigned short | 2 | 0~64535 | 
| 
 | unsigned long | 4 | 0~4294967295 | 
    注:long类型的字节数因处理系统的不同而不同。
在计算机世界中,负数用二进制补码表示。例如,十进制数−10按如下步骤转换为二进制。
① 将绝对值转换为二进制。
② 对数字0和1取反。
③ 在转换后的数值上加1(二进制补码)。
得到的结果是1111 0110。在图1-21中,我们使用了8位数字(1字节),但如果我们想使用2字节,需用与符号位相同的值来填充空位,得到的结果就是1111 1111 1111 0110。
图1-21 从十进制到二进制补码
反之,把二进制补码[1]转换为十进制时,需要按如下步骤进行。
[1] 符号位(二进制数最左边的数字)为0时并不是二进制补码。在这种情况下,请用1.2.3小节中描述的方法将其转换成十进制。
① 所有数字0和1取反。
② 转换后的数值加1。
③ 转换为十进制,并加上“−”。
例如,如果用这种方法,把二进制补码1111 0011转换为十进制后的结果为−13(见图1-22)。
图1-22 从二进制补码到十进制
Try Python 关于二进制补码
如果把一个负数作为bin()函数的参数,将会在二进制数的开头看到一个“−”。
>>> bin(-10)  ← 将十进制中的-10转换为二进制
'-0b1010'     ← 显示的结果请注意这是Python处理后显示的结果,以便我们理解,而不是计算机内部使用的实际值(二进制补码)。
如果你想显示二进制补码,可以将给bin()函数传递的参数转换为补码。详细内容会在2.3.6小节中进行介绍。
>>> bin(-10 & 0xFF) ← 将十进制数-10转换为二进制补码
'0b11110110'        ← 显示的结果包括小数点的数值,如3.14、9.8,被称为小数。当然,小数点“.”不能在计算机世界中使用。在计算机世界中,以浮点数的形式来处理小数。
在1.1.1小节中,我们介绍了十进制数中数位的权重由10n表示,n每增加1,新权重就变为原权重的10倍。请根据上述内容来思考这个问题:值“10.625”表示10有1个,1有0个,0.1有6个,0.01有2个,而0.001有5个,所有这些的总和就是该数值,这怎么用数学表达式来表示呢?
像10和1一样,0.1和0.01也有数位权重,我们可以把它表示为10.625 = (101×1) + (100×0) + (10−1×6) + (10−2×2) + (10−3×5)。如果不知道其中原因,请看图1-23,它显示了小数中每低一位数,新权重变为原权重的 。
。
同样,让我们考虑二进制数“0.1001”。当然,“0.1001”中的“.”不能在计算机世界中使用,但在二进制计数法中,数位低一位,新权重就变为原权重的 。
。
因此,二进制数“0.1001”转换为十进制数就为“0.5625”(见图1-24)。
图1-23 十进制数的数位权重
图1-24 二进制数的数位权重
还记得如何将十进制的整数转换为二进制数吗?重复除以2,直到商变成0,然后从右到左依次排列余数。如小数10.625的整数部分可以用该方法转换为二进制数(见图1-25)。
图1-25 将整数部分转换为二进制数
对于小数部分,将小数部分乘以2,重复该过程直到小数部分变为0,并将所得整数按从左到右的顺序排列(见图1-26)。如果你不明白除以2或乘以2的原因,请看图1-24,并仔细思考一下原因。
图1-26 将小数部分转换为二进制数
根据这种方式转换,十进制数10.625转换成二进制数就是1010.101。当然,这只是一个显示在纸面上的值。在计算机内部,小数是以浮点数的形式处理的。
Try Python 十进制小数转换为二进制数的程序
代码1-4中的dec2bin_ex()函数是对代码1-1中dec2bin()函数的修改,可将小数转换为二进制。参数target必须是一个十进制数。执行示例如下。
>>> dec2bin_ex(10.625)  ← 将十进制数10.625转换成二进制数
([1, 0, 1, 0], [1, 0, 1]) ← 显示的结果(格式为[整数部分], [小数部分])代码1-4 “dec2bin_ex”将十进制数转换为二进制数(小数版)
 1. def dec2bin_ex(target):
 2.     # 将target分离成整数和小数部分
 3.     i = int(target) # 整数部分
 4.     f = target - i # 小数部分
 5.
 6.     # 将整数部分转换为二进制数    ← ①
 7.     a = [] # 剩余部分的列表
 8.
 9.     # 直到商为0
10.     while i ! = 0:
11.         a.append(i % 2) # 余数
12.         i = i // 2 # 商
13.
14.     # 把元素按相反的顺序排列
15.     a.reverse()
16.
17.     # 将小数部分转换为二进制数       ← ②
18.     b = [] # 带有整数部分的列表
19.     n = 0 # 重复的次数
20.
21.     # 乘以2直到小数部分为0
22.     while (f ! = 0):
23.         temp = f * 2 # 小数部分 x 2
24.         b.append(int(temp)) # 整数部分
25.         f = temp - int(temp) # 小数部分
26.         n += 1 
27.         if(n >= 10):       ← ③
28.             break
29.
30.     # 值转换为二进制
31.     return a, b首先,我们把给定的参数target分成整数和小数部分。可以用int()函数来获取target的整数部分。从target值中减去这个值,就得到了小数部分。随后代码部分①将整数转换为二进制数。更多细节见代码1-1。
代码部分②是转换小数部分。将小数部分乘以2,然后将结果的整数部分加到列表中,重复这个过程,直到小数部分为0。代码部分③是在10次迭代后退出循环的指令。关于限定迭代次数的原因,见1.5.4小节。
十进制数10.625转换为二进制数,即1010.101。那么0.1呢?0.000110011001100…后面会一直持续下去,但就目前而言,如果结果取小数点后十位,就是0.0001100110。然而,这只是书面上的表达。“.”不能在计算机世界中使用。在计算机世界,我们使用指数来表达这些值,即浮点数(见图1-27)。符号部分与符号位相同。0代表正数,1代表负数,以此类推。剩下的指数部分和小数部分的数值由小数点的位置决定。
图1-27 浮点数
首先,移动小数点的位置,使二进制数的整数部分变成1。例如,将1010.101的小数点向左移动3位,得到1.010101。如果再乘以移动位数的权重,则为1.010101×23,这样就可以恢复到原来的值。以同样的方式,我们用指数来表示0.00011001100。这一次,我们将小数点向右移动4位,所以得到1.100110×2−4(见图1-28)。你知道为什么指数是负的吗?小数点每向右移动一位,数值就会变为移动前数值的2倍、4倍……还原的话必须相应变为 、
、 ……
……
图1-28 将小数点向左或向右移动
现在,通过这种方法,你可以将任何数值表示为“1.××××…×2n”。为了得到“1.××××…×2n”,可以把小数点向左或向右移动,这就是浮点数。数字的小数部分指的是小数点后的部分,而指数部分指的是“n”。
你对什么是浮点数有一个模糊的概念了吗?大多数编程语言提供两种处理浮点数的数据类型:单精度浮点数和双精度浮点数(见表1-7)。Python的float类型具有与双精度浮点数相同的精度。
表1-7 用于处理浮点数的数据类型
| 数据类型 | 大小 | 符号部分 | 指数部分 | 小数部分 | 
|---|---|---|---|---|
| 单精度浮点数 | 32位(4字节) | 1 | 8 | 23 | 
| 双精度浮点数 | 64位(8字节) | 1 | 11 | 52 | 
坦率地说,你不需要知道浮点数的构成就可以编写程序。但要记住如下两点。
● 小数在计算机内会被当作浮点数来处理。
● 在这种情况下会产生误差。
例如,如果你把十进制数0.1转换成二进制数,会得到0.0001100 1100…没有穷尽。这同样适用于0.2和0.3。无论你在小数点之后增加多少位,你都不能用精准的二进制数来代替它们。但容器的大小是有限的,切分位数将产生非常小的误差。当然,即使是最先进的计算机也不能消除这种误差。重要的是,我们要明白,当我们与计算机互动时,会有误差。
计算机处理的不仅仅是数字,还包括文本、图像、声音等各种信息,但也只使用0和1来处理。让我们看一下这些信息如何替换为0和1。
在计算机世界中,我们使用一套称为字符编码的规则来处理字符。例如,根据表1-8所示的ASCII(美国信息交换标准代码),“A”指的是“1000001”(十进制的65),“a”指的是“1100001”(十进制的97)。由于ASCII被限制在1字节以内(准确地说是7位),所以只能表示128种字符。使用平假名、片假名和汉字等多种字符的日语,使用的是JIS(日本工业标准)码或Shift-JIS(是一个日本常用的计算机系统的编码表)码,用2字节来表示一个字符,并根据处理系统的不同有的时候使用EUC(是一种使用8位编码来表示字符的方法)码。
表1-8 ASCII
| ASCII值 | 字符 | 
|---|---|
| 0 | NUL | 
| 1 | SOH | 
| 2 | STX | 
| 3 | ETX | 
| 4 | EOT | 
| 5 | ENQ | 
| 6 | ACK | 
| 7 | BEL | 
| 8 | BS | 
| 9 | HT | 
| 10 | LF | 
| 11 | VT | 
| 12 | FF | 
| 13 | CR | 
| 14 | SO | 
| 15 | SI | 
| 16 | DLE | 
| 17 | DC1 | 
| 18 | DC2 | 
| 19 | DC3 | 
| 20 | DC4 | 
| 21 | NAK | 
| 22 | SYN | 
| 23 | ETB | 
| 24 | CAN | 
| 25 | EM | 
| 26 | SUB | 
| 27 | ESC | 
| 28 | FS | 
| 29 | GS | 
| 30 | RS | 
| 31 | US | 
| 32 | [SPACE] | 
| 33 | ! | 
| 34 | " | 
| 35 | # | 
| 36 | $ | 
| 37 | % | 
| 38 | & | 
| 39 | ' | 
| 40 | ( | 
| 41 | ) | 
| 42 | * | 
| 43 | + | 
| 44 | , | 
| 45 | - | 
| 46 | . | 
| 47 | / | 
| 48 | 0 | 
| 49 | 1 | 
| 50 | 2 | 
| 51 | 3 | 
| 52 | 4 | 
| 53 | 5 | 
| 54 | 6 | 
| 55 | 7 | 
| 56 | 8 | 
| 57 | 9 | 
| 58 | : | 
| 59 | ; | 
| 60 | < | 
| 61 | = | 
| 62 | > | 
| 63 | ? | 
| 64 | @ | 
| 65 | A | 
| 66 | B | 
| 67 | C | 
| 68 | D | 
| 69 | E | 
| 70 | F | 
| 71 | G | 
| 72 | H | 
| 73 | I | 
| 74 | J | 
| 75 | K | 
| 76 | L | 
| 77 | M | 
| 78 | N | 
| 79 | O | 
| 80 | P | 
| 81 | Q | 
| 82 | R | 
| 83 | S | 
| 84 | T | 
| 85 | U | 
| 86 | V | 
| 87 | W | 
| 88 | X | 
| 89 | Y | 
| 90 | Z | 
| 91 | [ | 
| 92 | \ | 
| 93 | ] | 
| 94 | ^ | 
| 95 | _ | 
| 96 | ` | 
| 97 | a | 
| 98 | b | 
| 99 | c | 
| 100 | d | 
| 101 | e | 
| 102 | f | 
| 103 | g | 
| 104 | h | 
| 105 | i | 
| 106 | j | 
| 107 | k | 
| 108 | l | 
| 109 | m | 
| 110 | n | 
| 111 | o | 
| 112 | p | 
| 113 | q | 
| 114 | r | 
| 115 | s | 
| 116 | t | 
| 117 | u | 
| 118 | v | 
| 119 | w | 
| 120 | x | 
| 121 | y | 
| 122 | z | 
| 123 | { | 
| 124 | | | 
| 125 | } | 
| 126 | ~ | 
| 127 | DEL | 
然而,随着字符编码类型的增加,出现了一些不便之处。当你打开一个网页时,你是否看到过无法正常显示的字符(乱码)?当创建文件使用的字符编码与解释文件使用的字符编码不同时,就会发生这种情况。当你使用这些字符时,应该注意其使用的是哪种字符编码。UTF-8和UTF-16是为了解决乱码等不便之处而开发的字符编码。[2]
[2] 更确切地说,UTF-8和UTF-16是对Unicode(统一码)中定义的字符进行编码的方法。
计算机屏幕上显示的所有颜色都是由红、绿、蓝(被称为三原色或三基色)的组合来表示的。由于每种颜色的强度可以在0~255的范围内调整,所以可以显示的颜色数量计算如下。
256×256×256=16777216种
这意味着大约有1670万种颜色可以被显示,主要颜色强度值参考表1-9。
表1-9 主要颜色强度值
| 颜色 | 红色强度 | 绿色强度 | 蓝色强度 | 
|---|---|---|---|
| 黑色 | 0 | 0 | 0 | 
| 红色 | 255 | 0 | 0 | 
| 绿色 | 0 | 255 | 0 | 
| 蓝色 | 0 | 0 | 255 | 
| 黄色 | 255 | 255 | 0 | 
| 浅蓝色 | 0 | 255 | 255 | 
| 紫色 | 255 | 0 | 255 | 
| 白色 | 255 | 255 | 255 | 
你对0~255熟悉吗?这些是可以用1字节(8位)表示的值。这个系统被称为24位色,因为它用红、绿、蓝各8位来表示一种颜色。另外,除了红、绿、蓝的强度外,还有增加了8位表示透明度(alpha值)的32位色[3](见图1-29)。两者所能代表的颜色数量没有区别。
[3] 在某些情况下,为了方便计算机运算,增加了8位,原因是计算机可以更有效地处理32位数据。
图1-29 用4字节表示颜色
用智能手机拍摄的照片或用绘画软件绘制的图片由一系列的小彩点(像素)组成。在计算机内部,每个像素的颜色信息被按顺序记录下来,并作为一个单一的图像处理(见图1-30)。在2.3.7小节中,我们将向你展示如何将一个像素的颜色分解为红、绿、蓝三部分。
图1-30 图像是像素的集合
在这种情况下,新增加的8位是没有特殊含义的值。
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e59773”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。