1 自顶向下的程序设计
1.1 问题分解和自顶向下的的程序设计方法
自顶向下的程序设计方法,即根据主程序的需要来确定子程序的功能和界面。也就是说,先在主程序中确定子程序要干什么以及怎么与主程序交互,然后让子程序去决定怎么干。这就是著名的分治法。表现在程序设计上,就应该是先编写主程序,接着编写主程序要调用的子程序,再编写这些子程序要调用的子程序,以此类推。这种方法称为自顶向下的程序设计方法,又称为先整体后局部的程序设计方法。
总体来说,编程要求我们学会分析问题,学会从整体和全局看待问题。
1.2 例:扑克牌问题
问题描述:
有一副只有点数没有花色的扑克牌,点数分别是1,2,3,…,20,一共20张。我们把所有牌正面朝下堆成一叠放在手上,然后用另一只手翻开第一张,发现是1点。把这张1点牌放在一边,然后摸下一张牌。这是我们并不看它的点数,而是把它放在这叠牌的末尾,使之成为最后一张牌。接着,翻开下一张牌,发现是2点。把这张2点牌也放在一边,然后按顺序摸两张牌,不看点数,而是把它们放在这叠牌的末尾。注意,把它们一起放在末尾和把它们一张一张放在末尾的结果是一样的。再翻开下一张牌,发现是3点。这张3点牌也放在一边,再一张一张地摸三张牌,并按顺序放在末尾。以此类推,直到把所有牌都翻开并放在一边。检查被翻开的牌的点数,发现其顺序分别是1,2,3,…,20。问:最初牌的顺序是什么?
解决这个问题的思路便是自上而下、先整体后局部的编程方法。若采用逆向思维,我们可以先准备一叠牌。一开始这叠牌里一张扑克牌都没有,然后把20点牌插入这叠牌种去,怎么插入的,我们不关心,待会只需要设计一个子程序来完成这个插入操作即可。然后按顺序把19点牌,…,2点牌,1点牌插入到折叠牌种。所以,主程序是这样的:
def get_pokers(num):
result = []
for k in range(num, 0, -1):
_insert(k, result) #把扑克牌k插入到序列result中
print(result)
return result
if __name__ == '__main__':
print(get_pokers(20))
其中的_insert()函数就是用来把扑克牌插入到列表中去的。接下来,我们来实现这个子程序。假设这张被插入的牌的点数是。在正向思维下,它被翻开拿到一边后我们又摸了张牌,并放到了这叠牌的底下。现在用逆向思维,在把这张牌插入到列表中去之前,应该从这叠牌的地下一张一张地抽出k张牌,并按顺序放在这叠牌的顶端。我们用子程序_move_last_to_top()来实现这个功能。现在在_insert()的函数体中,我们只管调用它,哪怕它还没有实现,代码如下:
def _insert(k, pokers):
if len(pokers) > 0:
for _ in range (k):
_move_last_to_top(pokers)
pokers.insert(0, k)
return pokers
最后我们再实现_move_last_to_top()函数。很简单,只需从列表中删除最后一个元素,然后把它插入到顶端即可。
def _move_last_to_top(pokers):
last = pokers[-1]
del pokers[-1]
pokers.insert(0, last)
至此我们就解决了这个问题。其中关键就在于:把困难的、大的问题分解为若干小问题,再把每个小问题分解为更小的问题,直到每个最小的问题可以很容易解决为止。
2 递归程序设计
2.1 递归原理说明
递归程序设计是自顶向下的特殊情况,一般自顶向下是指一个函数对另一个函数的调用,而递归是指函数对自己的调用。
我们认为,递归的实质是数学归纳法。什么是数学归纳法?例如,如果要证明前个奇数的和等于,如 ,,也就是说 ,若使用数学归纳法进行证明:
第一步,称为归纳边界。即当 时看看定理是否成立。
第二步,称为归纳假设。假设 时定理成立,即前个奇数的和等于。
第三步,称为递归推导。看看 时定理是否成立。由 ,可见, 时定理成立。
综合上述三步,我们认为前个奇数的和等于,这就是数学归纳法。递归实际上也是按照这三步来的,分别称为递归边界、递归假设和递归推导。不同的地方仅仅在于,数学归纳法是试图证明一个定理成立,而递归的目的是根据输入生成输出。而后,将通过实例来说明递归的实质是数学归纳法。
2.2 河内塔问题
递归程序的一个著名例子是河内塔(Hanoi塔,也译为汉诺塔)问题。
有3根插在地上的杆子 、、。 杆上套有个中间有孔的碟子,碟子的直径从上到下分别是1,2,...,,如图1-1所示。现在我们试图把所有碟子从 杆移到 杆,条件是:
- 任意一根杆子上只有最上面的碟子可以移动。
- 任何情况下,大碟子都不能放在小碟子上面。
- 杆可以当作中转用。
解决此类问题的第一件事情就是确定问题的输入和输出。河内塔问题的输出很明确。输入是上面?可能有人认为输入是碟子的个数,例如。这个想法是错误的,因为我们并不清楚这个碟子在哪根杆子上。所以输入参数除了之外,应该还有 、、 3个参数,分别表示”来源“、”中转“、”目的地“。注意,、、 的初值分别是 、、 。
确定了输入和输出之后,就可以按照递归的方法解决这个问题了。既然递归的实质是数学归纳法,并且数学归纳法是按照3个步骤进行思考的,那么与之对应,递归也应该是按照3个步骤进行思考。
第一步,递归边界。所谓递归边界就是输入参数要满足一个条件,在这个条件下,原问题可以很容易解决。确定河内塔问题的边界在哪里,就是确定输入参数在什么情况下问题可以很容易得到解决。显然,当输入参数 时,问题最好解决,因为此时 上仅有一个碟子,直接把这个碟子从 移到 即可。
第二步,递归假设。即假设 个碟子可以从 、、 中的任一杆子移到另外任意杆子上。此处需要注意,编程也可以假设,这正是递归程序设计神奇而具有魅力的地方。
第三步,递归推导。就是利用河内塔问题在 位置处的解来推导问题在处的解。既然 个碟子可以从任意一根杆子移动到另外任意一根杆子上,那么我们可以这样移动:以 作为中转,先把 最上面的 个碟子移到 杆上,再把 杆剩下的直径为 的那个碟子移到 杆上,最后把 杆上的 个碟子移到 杆上。
递归程序设计的原则就是:首先用一个 语句判断当前参数是不是处于递归边界。如果是,就按递归边界处理;否则,利用递归假设和递归推导进行编程即可。解决河内塔问题的递归程序如下:
def hanoi(panes, a, b, c):
#把n个碟子从a移到c,b作中转
if panes == 1: #递归边界,一个碟子,直接移动
print('Move 1 from %s ==> %s ' %(a, c))
else:
hanoi(panes - 1, a, c, b) #先把n-1个碟子从a移到b上,c作中转
print('Move %d from %s ==> %s ' %(panes, a, c)) #把最大的碟子从a移到c上
hanoi(panes - 1, b, a, c) #最后把n-1个碟子从b移到c上,a作中转
if __name__ == '__main__':
hanoi(4, 'A', 'B', 'C')
从本质上来讲,递归程序设计方法仍然是一种自顶向下的程序设计。在编写一个递归程序的函数体时,被调用的子函数就是正在被实现的函数本身。既然子程序都可以先调用后定义,那么递归子程序自然也是可以先调用后定义的。唯一的区别就是一般自顶向下调用的是其他函数,而递归调用的是当前这个函数本身。
3 面向对象的程序设计
面向对象方法的核心有两个,一个是封装,另一个是继承。简单来说,封装就是把程序和数据绑定在一起,所以调用一个函数的时候,一般要指明调用的是哪个对象的函数。继承就是让子类可以做两件事:第一,定义新的函数;第二,重新定义父类中的函数。
3.1 方法重定义和分数
一般计算机语言中没有分数类型,Python也不例外。不过,我们可以创建一个分数类Fraction,然后实现分数的加减乘除。这个类的框架如下:
class Fraction:
def __init__(self, num, denom=1): #构造函数,num:分子,denom:分母
self.num = num
self.denom = denom
def __repr__(self): #将这个分数转成字符串
return '(%s/%s)'% (self.num, self.denom)
if __name__ == '__main__':
f1 = Fraction(3, 4)
print(f1)
print(str(f1))
在主程序中,我们首先调用Fraction(3,4),一边创建一个分数对象3/4。这时,Python约定会调用Fraction类中定义的构造函数_init_(),并传入相应的参数3和4。在Python中构造函数是_init_(),而在Java和C++中构造函数约定就是类的名字。另外,在Python中,任何函数(包括普通函数、内部函数、类的成员函数、类的静态函数和构造函数)不能重复定义。如果重复定义,Python不会报错,但会用最后一个同名函数替换。也就是说,Python不支持面向对象方法中所谓的重载(Overload)。
下面,则是在类中对基本运算符加法进行重定义,我们只需要在Fraction类中定义函数_add_()即可。代码如下:
class Fraction:
def __init__(self, num, denom=1): #构造函数,num:分子,denom:分母
self.num = num
self.denom = denom
def __repr__(self): #将这个分数转成字符串
return '(%s/%s)'% (self.num, self.denom)
def __add__(self, other): #重新定义运算符+
num = self.num * other.denom + self.denom * other.num
denom = self.denom * other.denom
return Fraction(num, denom)
if __name__ == '__main__':
f1 = Fraction(3, 4)
f2 = Fraction(2, 3)
print(f1 + f2)
4 总结
本文主要总结了自顶向下、递归和面向对象3种程序设计方法。自顶向下主要是将原问题分解为若干子问题,子问题在进一步分解为它自己的子问题。而递归主要将原问题分解为规模小一点、参数靠近边界的原问题。面向对象的程序设计方法的核心则是继承,继承的目的则是重用代码。