程序设计方法学习记录

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。在正向思维下,它被翻开拿到一边后我们又摸了k张牌,并放到了这叠牌的底下。现在用逆向思维,在把这张牌插入到列表中去之前,应该从这叠牌的地下一张一张地抽出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 递归原理说明

        递归程序设计是自顶向下的特殊情况,一般自顶向下是指一个函数对另一个函数的调用,而递归是指函数对自己的调用。

        我们认为,递归的实质是数学归纳法。什么是数学归纳法?例如,如果要证明前n个奇数的和等于n^{2},如 1+3+5=3^{2}1+3+5+7=4^{2},也就是说 1+3+... +(2n-1) = n^{2},若使用数学归纳法进行证明:

        第一步,称为归纳边界。即当 n = 1 时看看定理是否成立。

        第二步,称为归纳假设。假设 n = k 时定理成立,即前k个奇数的和等于k^{2}

        第三步,称为递归推导。看看 n = k+1 时定理是否成立。由 1 + 3 + ... +(2k-1) + [2(k+1)-1] = k^{2} + 2k + 1 = (k+1)^{2},可见,n = k+1 时定理成立。

        综合上述三步,我们认为前n个奇数的和等于n^{2},这就是数学归纳法。递归实际上也是按照这三步来的,分别称为递归边界递归假设递归推导。不同的地方仅仅在于,数学归纳法是试图证明一个定理成立,而递归的目的是根据输入生成输出。而后,将通过实例来说明递归的实质是数学归纳法。

2.2 河内塔问题

        递归程序的一个著名例子是河内塔(Hanoi塔,也译为汉诺塔)问题。

        有3根插在地上的杆子 ABCA 杆上套有n个中间有孔的碟子,碟子的直径从上到下分别是1,2,...,n,如图1-1所示。现在我们试图把所有碟子从 A 杆移到 C 杆,条件是:

  • 任意一根杆子上只有最上面的碟子可以移动。
  • 任何情况下,大碟子都不能放在小碟子上面。
  • B 杆可以当作中转用。

        解决此类问题的第一件事情就是确定问题的输入和输出。河内塔问题的输出很明确。输入是上面?可能有人认为输入是碟子的个数,例如n。这个想法是错误的,因为我们并不清楚这n个碟子在哪根杆子上。所以输入参数除了n之外,应该还有 abc 3个参数,分别表示”来源“、”中转“、”目的地“。注意,abc 的初值分别是 ABC 。

        确定了输入和输出之后,就可以按照递归的方法解决这个问题了。既然递归的实质是数学归纳法,并且数学归纳法是按照3个步骤进行思考的,那么与之对应,递归也应该是按照3个步骤进行思考。

        第一步,递归边界。所谓递归边界就是输入参数要满足一个条件,在这个条件下,原问题可以很容易解决。确定河内塔问题的边界在哪里,就是确定输入参数在什么情况下问题可以很容易得到解决。显然,当输入参数 n = 1 时,问题最好解决,因为此时 a 上仅有一个碟子,直接把这个碟子从 a 移到 c 即可。

        第二步,递归假设。即假设 n-1 个碟子可以从 abc 中的任一杆子移到另外任意杆子上。此处需要注意,编程也可以假设,这正是递归程序设计神奇而具有魅力的地方。

        第三步,递归推导。就是利用河内塔问题在 n-1 位置处的解来推导问题在n处的解。既然 n-1 个碟子可以从任意一根杆子移动到另外任意一根杆子上,那么我们可以这样移动:以 c 作为中转,先把 a 最上面的 n-1 个碟子移到 b 杆上,再把 a 杆剩下的直径为 n 的那个碟子移到 c 杆上,最后把 b 杆上的 n-1 个碟子移到 c 杆上。

        递归程序设计的原则就是:首先用一个 if 语句判断当前参数是不是处于递归边界。如果是,就按递归边界处理;否则,利用递归假设和递归推导进行编程即可。解决河内塔问题的递归程序如下:

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种程序设计方法。自顶向下主要是将原问题分解为若干子问题,子问题在进一步分解为它自己的子问题。而递归主要将原问题分解为规模小一点、参数靠近边界的原问题。面向对象的程序设计方法的核心则是继承,继承的目的则是重用代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值