目录
概述程序、数字与进制
本书不是Python语言教科书,我们使用一门语言,最主要的用途是用它来解决各种实际问题。这就是本书的重点:如何用Python语言来解决问题,也因此,本书将会有大量的代码演示及讲解。
计算机编程,我们会要处理各种数据,使用各种算法,所以,本书将涉及到数据结构、算法等一些原理方面的知识。我们由浅入深,一步步讲解怎么编程序,因此,本书讲解的只是绝不仅仅限于学习Python,这里面学到的知识是通用的,你可以不费劲地用另一门语言替代书中的Python程序。
基本内容如下:
程序、数字与进制
初等数学题解
字符的处理
计算机内部数据存储及执行
初等科学题解
排序及查找算法初步
树形结构
动态规划
图
表
本书是面向初学者的,高中生以及大学生,如果你觉得这本书将你引进了编程的大门,领略了计算机这个伟大发明的魅力,作为写书人,我会很欣慰。事实上,本书大部分Python代码是Clive,一个九年级中学生,他在假期学习过程中所实际编写的。我用学习者的代码作为本书的实例,而不是用专业代码,就是想让学习者明白你也能做到,编程没有什么难的。
信息时代的程序员,正如工业时代的工人,正如农业时代的农民。我相信,面向未来的中小学教育,编程将会成为必备常识。
老话说“开卷有益”,这是我内心的期许和承诺。
现在我们用的计算机跟历史上发明的其它工具都不相同:它需要编程。
人是好奇而有创造力的物种,从我们远古的祖先在数百万年前手握两块石头敲打砍砸发明石器那个时候起,人类就开始了漫长的发明之旅。
(Olduvai砍砸器,约200万年前。大英博物馆收藏。)
人类为了生存,不断迁徙和扩展,从非洲走出,遍布亚洲欧洲最后到美洲(那个南非出生的Elon Musk觉得地球装他不下了,一心要移民火星),一路上发明了许多帮助人类生存的工具。我们骄傲的祖先筚路蓝缕以启山林,发明了棍棒、长矛,点燃了火种,成了采集者、猎人和渔夫;后来又发明了语言,开始歌舞绘画,成为讲故事的人和艺人;接着发明了葬礼,有了信仰,成为祭司;发明了数字,发明了陶器、人工栽培和轮子,成了农民;发明冶炼锻造,成为手工业者,数百年前蒸汽机出现,开始工业革命;最后出现了计算机。
有词为证:
贺新郎 读史
人猿相揖别。历数百万年岁过,蛮荒时节。采集渔猎引火种,为问何时猜得?又是十万寒暑。认知初开智人起,又开口从此说各色。生物链,顶头客。
一篇读罢头飞雪,但记得刀耕火种,宫阙村舍。耶稣孔孟路德事,抚慰无涯众生。问多少纪元更迭?工业革命流誉后,更电脑崛起智能热。歌未竟,宇宙白。
我们来看历史上几个典型的发明,手斧、轮子、蒸汽机,它们的目的单一,结构一旦定下来,它的功能也就确定下来了,不能用于别的工作。
但是我们使用的计算机,却可以通信,可以放音乐,可以写作,可以记账。它是一个通用的计算机器,其具体的功能取决于一个叫做“程序”的东西。
为了理解“程序”这个概念,我们要看看没有程序的计算机器是个什么样子。
人类很早就知道计数,有了数字,就需要有对数字的操作,最典型的就是加法和减法。这个过程我们现在的小孩子就会,不过人类可是花费了上万年才搞好这件事。说起这个过程还真是艰辛,最早通过手指计数,这个太方便了,人人自带工具,不过有一个问题就是不好存储这个数,你刚弯了几个手指数到8,你知道了这个数,但你不能一直僵在那里,一会儿就得干别的去,数好的这个数字就没有了,于是人们就想到了用分石子或者给绳子打结来存储这个数。可能是5000年前出现了数字系统,这意味着人类已经把“数”这个概念抽象出来了,然后用了更久的时间才发明了0,至此我们习惯的十进制系统才成形。
十进制数字系统方便了计算,促进了生产和贸易,也就有人专门从事这个职业,计数和计算。这是当时的新兴行业,受过教育的年轻人拿着小刀和泥板,一笔一划地诚实记录用心计算,日复一日年复一年。这不仅仅是一个脑力劳动还是一个体力活儿,虽然东西方都分别发明了算筹和算盘,但是这种手工式的计算本身仍然费时费力。也许在那些岁月里,一些年轻人累了揉着自己发疼的手腕的时候,闭着眼睛想能不能有一个机器帮着计算,把自己从繁重的劳动中解脱出来,不用再这么996干活儿。
有人梦想就会有人去实现。
1642年,法国19岁的Pascal(就是历史上那个著名的散文家数学家物理学家)发明了加法机,由齿轮构造而成,加法通过转动齿轮实现,用连杆实现进位。这个发明也影响了Pascal的哲学观,他认为人的思维活动跟机械没什么差别。
(Pascal加法机,1642)
三十多年后,学术巨人Leibniz莱布尼茨发明四则运算器,影响了上百年。但是这些机器都受机械影响,功能性能都不怎么好。
到这个时候,用于计算的工具跟历史上别的发明仍然没有什么差别,依然只是完成某件事情的单一工具。只不过这些机器完成的工作是加减乘除。
真正的突破是十九世纪英国的数学家Babbage发明的分析机。Babbage意识到计算的工作是多种多样的,如果为某一个特定的工作就单独发明一台机器,那就太繁琐了,能不能有一种通用一点的机器,完成所有这些计算工作?于是Babbage想到了将一个任务的计算工作分解成原子步骤(加减),把这些步骤记录下来,让一台机器从某个地方拿这些步骤,然后顺序执行并可以跳转执行。这就是“程序”的萌芽。
为此,在分析机中,Babbage设计了存储装置,可以存储初始数据及中间结果数据,设计了运算装置,执行加减计算,设计了控制装置,通过指令控制操作步骤。这是一台可编程的计算机器(由于工艺限制,直到Babbage去世也没有造出来)。
有一个叫Ada的女孩(她是英国著名诗人Byron拜伦的女儿),还为这台机器写了程序计算伯努利数,并提出了循环和子程序的概念,她被称为世界上第一个程序员。
(Babbage分析机,后人根据设计方案制造)
(Ada Lovelace,1815.12.10-1852.11.27,第一个程序员)
到了这个时候,程序的概念就确立了。一台计算机器能做什么工作,取决于程序。给它乘法程序,它就执行乘法,给它音乐程序,它就播放音乐。所以我们就叫它为“可编程通用计算机”。看下图就知道了:
事实上,通用任务也是有范围的,并不是指世界上所有的工作,它的范围就是“计算”。不要以为这个范围很窄,也不要简单地把计算等同于加减乘除,按照现代观点,科学计算、文字处理、记账、绘图、播放音乐、视频以及网络通信这些工作都是计算。对于什么是计算,又是一百多年之后,到了Turing图灵他们这一代人才搞明白。
好了,到此,我们可以基于现代的概念来探讨“程序”“计算机”“数字”等等了。
我们先看“数字”。
平时我们就用0,1,2,3,4,5,6,7,8和9,这十个符号表示十个数字,通过位置表示权,我们叫它十进制系统。虽然日常生活中用得最多的是十进制,但是用任何进制都是可以的。我们其实还会用到别的进制,如12进制,60进制,在表示时间的时候我们会用到。而从理论上,任何进制都是等价的。为什么十进制会成为使用最广泛的进制?历史学家说只不过是因为人正好有十个手指头,用起来方便一点。
这是一本讲解编程的书,我尽量少用数学,不过还是会用到一点。从数学上表示进制是这样的:
S表示符号(比如十进制下的0-9十个符号),K表示位置,b表示进制基数(如10)。
这个表示的值为:
大家可以用十进制来测试计算一下上面的公式。
好,我们重点看二进制。
首先的一个疑问就是为什么要用二进制?为什么不采用我们人类习以为常的十进制?这个与计算机的构造有关系,开始用的是机械,后来用机电设备,再后来用电子元件。使用这些器件,表示两种状态比较方便简单,比如高低,比如开关,比如断通,天然地适合二进制。另外二进制的运算规则比较简单,使计算机硬件结构大大简化(一个明显的事实是十进制乘法九九口诀有55条公式,而二进制乘法只有四条规则。),所以二进制更经济。
我们来看一个二进制表示的数:11001
套用上面的求值公式,结果就是N=16+8+0+0+1=25 (用十进制表示的25)
再来看一个二进制表示的实数:101.11
套用上面的求值公式,结果就是N=4+0+1+0.5+0.25=5.75 (用十进制表示的5.75)
上面是正数,负数又如何表示呢?当然最直观的想法就是加上特殊符号表示正负数,相当于+和-。但是其实计算机内部并不是这么表示的,为了运算的方便,计算机内部采用了一种成为补码的方式表示正负。看一个例子:00110100,补码为11001100。规则简单地说,就是把每一位先取反(0变成1,1变成0),然后加1。
有了补码,计算机按照这个原则存储数字,先把十进制数变成二进制,然后看是不是正数,如果是,原封不动存储,如果是负数,则以补码存储。反过来,对存储的一个二进制数,如果最左边一位是1,则当成负数,求补码后得出原数,如果最左边一位是0,则是正数,直接得原数。
这种看起来奇奇怪怪的表示法,其实是一种相当聪明的办法。进一步的学习后,我们会明白,补码系统会简化运算。
实际的数字存储,需要考虑到计算机组成器件的限制,如整数有最大值限制,小数有精度,IEEE制定了标准,以一种确定的格式和长度来存储整数和实数,表示正负数。这些可以去查阅别的参考书追究细节。
除了二进制,我们使用比较多的还有八进制和十六进制。它们之间的转换比较简单,正好三位二进制数对应一个八进制数字,四位二进制数对应一个十六进制数字。
下面是几种进制之间的符号对照表:
如何把一个数在几种进制之间转换,别的进制转成十进制是比较简单的,就直接套用上面的公式。而反过来则比较麻烦,简单说起来就是整数部分连除小数部分连乘,这个手工做起来蛮麻烦的,后面章节我们会自己编写一段程序来进行转换。
你肯定会问,计算机用二进制表示数字,我们编写程序的时候真的要这么麻烦吗?答案是不用,我们的程序仍然是用我们习惯的十进制来编写。
那这是怎么回事?计算机怎么能认识十进制数并进行各种计算的?刚才不是说它只认识二进制吗?
我们现在的说法没有错,刚才讲解二进制的时候的说法同样没有错。
事实情况是,在计算机执行程序和人编写的程序中间有一个中间程序,叫“编译程序”或者“解释程序”,它负责把人编写的程序翻译成计算机识别的指令集合。这样做的目的是为了简化人类的工作(事实上,最早的程序员就是直接用的机器指令编写程序的,第一批程序员大部分是女孩,她们钻进计算机里面(当时的计算机很大,有一个房子那么大),拨弄开关、连线,通过这种方式编写程序)。程序员只要按照编译程序的规定写命令就行了,而随着时代的进步,这种写命令的方式越来越接近于人类的自然语言,因此,现在编程序的工作确实大大简化了。这些编译程序各有各的规定,也就是说它们规定了一种人造的语言:编程语言。现在使用的编程至少数百种,主流的不下十种,Python就是其中之一。
好,我们来看看Python里面怎么用数字。
使用Python3.7 IDLE,输入下面的程序(保存为一个文件test1.py):
test1.py
a=1+2
b=3
print(ab) 运行的结果是9. 读过中学代数的应该都能看得懂上面的程序,两个变量相乘。看到了吧,没那么可怕,有了Python,我们可以长吁一口气,不用直接用计算机的指令操作二进制数字了。自然,Python提供的语言不仅仅是这么一点,还有很多文法规则。本书不是语言教程,我不打算详细系统地讲解语言,你可以自己找本教材学习语言。好奇的你可能会想象一下在计算机内部实际是怎么执行c=a+b这程序的。这要进一步学习计算机组成才能明白。现在先不讲,后面我们会专门讲到。这是一个简单的程序,做很简单的工作。你可能会不以为然,不用计算机程序也会算出9来。如果程序只能做这么一点事情,那真的就不需要它了。我们随手给几个新的数值,把程序变成如下的样子:a=12345+23456 b=34567 print(ab)
如果你不是心算天才,估计一下子算不出来。程序运行还是瞬间出结果:1237533167。
不过不服气的你可能会想着用手持式计算器也一样嘛,在学校的时候可是人手一个的。那好,我们再用下面的数字计算:
a=1234567890+2345678901
b=3456789012
print(a*b)
在普通的计算器上计算,结果算不出来,出了错,这个数太大了,普通计算器算不了。程序运行呢?依然是瞬间出结果:12376157767377060492。
但是,能算多大的数,这并不是计算机与计算器的最大差别。通过程序,计算机能干的事情还多着呢。
首先表现为Python能识别处理的对象不仅仅是数字,还包括别的,字符,列表等等。列示如下:
Numbers(数字)
String(字符串)
List(列表)
Tuple(元组)
Dictionary(字典)
用编程的术语,我们叫“DataType数据类型”。
为了后面的讲解方便,我们先看看这几种数据类型的简单实例。
Numbers:有整数实数和复数。如:
A=123
B=123.45
String:字符串,字符的序列。如:
s = 'Hello World!'
字符串用双引号单引号均可,在Python中有两种位置标记法,从前往后(从0开始正数标注),从后往前(从-1开始负数标注)。如s[0]表示第一个字符,s[-1]表示倒数第一个字符。
s = 'Hello World!'
print (s) # 输出完整字符串
print (s[1]) # 输出字符串中的第一个字符
print (s[-2]) # 输出字符串中的倒数第二个字符
print (s[2:8]) # 输出字符串中第三个至第八个之间的字符串(不包含第八个)
print (s[2:]) # 输出从第三个字符开始的字符串
print (s * 2) # 输出字符串两次
print (s + "TEST") # 输出连接的字符串
输出结果为:
Hello World!
e
d
llo Wo
llo World!
Hello World!Hello World!
Hello World!TEST
List:数据集合,内部可以包含数字,字符串,子列表。如:
list = ['hello', 256 , 12.34, 'world', 70.2 ]
列表在Python中有两种位置标记法,从前往后(从0开始正数标注),从后往前(从-1开始负数标注)。如list[0]表示第一个元素hello,s[-1]表示倒数第一个元素70.2。
list1 = ['hello', 256 , 12.34, 'world', 70.2 ]
list2 = [123, 'test']
print (list1) # 输出完整列表
print (list1[0]) # 输出列表的第一个元素
print (list1[1]*2) # 输出列表的第二个元素的两倍,512
print (list1[-1]) # 输出列表的倒数第一个元素
print (list1[1:3]) # 输出第二个至第三个元素
print (list1[2:]) # 输出从第三个开始至列表末尾的所有元素
print (list2 * 2) # 输出列表两次
print (list1 + list2) # 打印组合的列表
运行程序输出:
['hello', 256, 12.34, 'world', 70.2]
hello
512
70.2
[256, 12.34]
[12.34, 'world', 70.2]
[123, 'test', 123, 'test']
['hello', 256, 12.34, 'world', 70.2, 123, 'test']
Tuple:相当于list,但是只能初始化的时候赋值,不能再赋值,是一个只读的list. 如:
tuple1 = ("hello", 256 , 12.34, 'world', 70.2 )
print (tuple1) # 输出完整元组
print (tuple1[0]) # 输出元组的第一个元素
print (tuple1[1]*2) # 输出元组的第二个元素的两倍
print (tuple1[-1]) # 输出元组的倒数第一个元素
运行程序输出:
('hello', 256, 12.34, 'world', 70.2)
hello
512
70.2
Dictionary:键值对的集合,通过key-value表示数据,无序存储。字典当中的元素是通过键来存取的,而不是通过位置存取。如:
dict1 = {}
dict1['one'] = "This is one"
dict1[2] = "This is two"
dict2 = {'name': 'john','code':6734, 'dept': 'sales'}
print (dict1['one']) # 输出键为'one' 的值
print (dict1[2]) # 输出键为 2 的值
print (dict2) # 输出完整的字典
print (dict2.keys()) # 输出所有键
print (dict2.values()) # 输出所有值
运行程序输出:
This is one
This is two
{'name': 'john', 'code': 6734, 'dept': 'sales'}
dict_keys(['name', 'code', 'dept'])
dict_values(['john', 6734, 'sales'])
上面的程序演示了Python支持哪些数据类型,以及基本的操作。程序简单,即使不熟悉Python的人也能差不多看明白。这里稍微说一下程序的写法。
一个Python程序可以保存为一个.py文件,内部是文本格式。
Python规定了一些关键字,有特殊含义,如上面的print,作用就是输出结果。这些关键字还有if, and, break, continue, def, else, for, import, in, is, not, or, return, while, with等等。
变量名字由字母数字和下划线组成,如list1, a等等。
字符串可以用单引号也可以用双引号。
列表用[], 元组用(),字典用{}
表示单行注释,方便人来阅读
‘’’用于注释多行
缩进,这是Python跟别的语言的不同,代码块不使用大括号 {} 来控制类,函数以及其他逻辑判断。python 最具特色的就是用缩进来写模块。如:
if True:
print ("Answer")
print ("True")
else:
print ("Answer")
print ("False")
有了这些语言基础知识,我们接下来可以动手编程序解决问题了。
好,开始我们的Python编程之旅!