2016.4.7Python递归函数

来源:互联网 发布:js动态删除指定tr 编辑:程序博客网 时间:2024/05/21 07:01

如果一个函数在内部调用自身本身,这个函数就是递归函数。

举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n,用函数fact(n)表示,可以看出:

fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n

所以,fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。

于是,fact(n)用递归的方式写出来就是:

def fact(n):    if n==1:        return 1    return n * fact(n - 1)

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000)

>>> fact(1000)Traceback (most recent call last):  File "<stdin>", line 1, in <module>  File "<stdin>", line 4, in fact  ...  File "<stdin>", line 4, in factRuntimeError: maximum recursion depth exceeded in comparison

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。

尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,任何递归函数都存在栈溢出的问题。


今天我实现了汉诺塔移动的代码,它的本质就是一个递归函数。

汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

为了简单起见,我们叫这三个圆柱小花,小明,小李。我们的目的就是以一根柱子做辅助和过渡,把一些数量可以被随机指定的盘子从一根柱移动到另一根柱子上。听起来好简单的样子,来分析一下这其中的逻辑吧。

首先思考一个问题,充当过渡的圆柱是一定的吗?仔细想想,肯定不是啊!

那么我们接着来看。

小花是一个高傲的小花,我们把n-1个盘子从她身上拿走了,全部简单粗暴的从小到大给了小明,然后把最大的那个直接从小花给到了小李。注意!在小花给小明从小到大的n-1个盘子的过程中,我们是怎么做的?我们其实是让小李充当了那个第一次过渡盘子的人啊!

现在,小花什么都没了,她的n-1个盘子最初通过小李给了小明,最后一个又给了小李。而为了让小李真正成为人生赢家,我们还需要把小明的n-1个盘子也给小李,我们是怎么做的呢?这个时候我们发现小花成了那个新的过渡人了!

好了,再回到最初的梗,究竟我们是怎么简单粗暴的把n-1个盘子从小花那里从小到大的给了小明呢?不妨看看上一段,唯一不同的是最初简单粗暴移动的盘子数量变成n-2个了。聪明的你看到这里一定明白这整个的逻辑了,我们定义了一个函数,参数之间的转化见红字逻辑,我们对这个函数重重递归,就可以回到第一个盘子的情况。

现在,你理解了什么是递归函数,什么是自己调用自己,什么是程序员思维了吗?

让我们来看看我写的代码吧:


看看运行结果:是不是很神奇呢?



我们只是理出了事物之间的逻辑关系,并由此定义了一个函数,巨大的计算就由电脑来完成,而且Python中的语言是如此的贴近英语,代码行数又如此之少,真是那什么啊。

下面再说明几点问题:

1.

当我们自己定义一个函数时,通常使用def语句,其语法形式如下所示:

def 函数名(参数列表): 函数体

其中,函数名可以是任何有效的Python标识符;参数列表是调用该函数时传递给它的值,可以由多个、一个或零个参数组成,当有多个参数时各个参数 由逗号分隔;圆括号是必不可少的,即使没有参数也不能没有它;函数体是函数每次被调用时执行的代码,可以由一个语句或多个语句组成,函数体一定要注意缩 进。此外,初学者经常忘记圆括号后面的冒号,这会导致语法错误。

这里介绍一下形式参数和实际参数,在定义函数时函数名后面圆括号中的变量名称叫做“形式参数”,或简称为“形参”;在调用函数时,函数名后面圆括号中的变量名称叫做“实际参数”,或简称为“实参”。


2.写=是赋值符号,表示把右边的值赋给左边;写==是相等符号。


3.方法(函数)定义后需要调用,对象(类)定义后需要实例化(创建)。

因为方法就是一个对象的一部分,对象里面定义了好多方法,只有在需要使用时调用后方可运行,就好像人一样,有脚有手,但手和脚可不是随时都在动只有你给他下达命令时才会动的。

一个程序里写的所有类和方法都是这个程序的功能,但这些功能是不会自己运行的,必须在需要时用函数名来调用他们,我的第一行代码就是定义了函数的名称叫move,形参是n,a,b,c,最后写函数名调用了这个函数,而将不需要的赋值的a,b,c改名成了大写来区分。我的输出也就成了大写了。


4.可能有人理解了逻辑,但是不懂我的代码,move(n-1,a,c,b)就是说把n-1个盘子通过c从a移动到b。print(a,'-->',c)表示是先从a到c的,
                move(n-1,b,a,c)就是这n-1个盘子通过a从b给了c。而每移动一次,某个上就会多一个,某个上就会少一个,在两行move里面来回运行,输出也被改变了,直至送完最后一个盘子。层层递归得到结果。


0 0