递归
递归
前面深入介绍了如何创建和调用函数。你知道,函数可调用其他函数,但可能让你感到惊讶的是,函数还可调用自己。
如果你以前没有遇到这种情况,可能想知道递归是什么意思。简单地说,递归意味着引用(这里是调用)自身。下面是一个常见的递归定义(但必须承认,这种定义很愚蠢):
递归[名词]:参见“递归”。
如果你在网上搜索“递归”,将看到类似的定义。
递归式定义引用了当前定义的术语。递归可能难以理解,也可能非常简单,这取决于你对它的熟悉程度。
一般而言,你不想要递归式定义,因为这毫无意义:你查找“递归”,它告诉你去查找“递归”,如此这般没完没了。下面是一个递归式函数定义:
def recursion(): return recursion()
这个定义显然什么都没有做,与刚才的“递归”定义一样傻。如果你运行它,结果将如何呢?你将发现运行一段时间后,这个程序崩溃了(引发异常)。从理论上说,这个程序将不断运行下去,但每次调用函数时,都将消耗一些内存。因此函数调用次数达到一定的程度(且之前的函数调用未返回)后,将耗尽所有的内存空间,导致程序终止并显示错误消息“超过最大递归深度”。
这个函数中的递归称为无穷递归(就像以while True
打头且不包含break
和return
语句的循环被称为无限循环一样),因为它从理论上说永远不会结束。
你想要的是能对你有所帮助的递归函数,这样的递归函数通常包含下面两部分。
- 基线条件(针对最小的问题):满足这种条件时函数将直接返回一个值。
- 递归条件:包含一个或多个调用,这些调用旨在解决问题的一部分。
这里的关键是,通过将问题分解为较小的部分,可避免递归没完没了,因为问题终将被分解成基线条件可以解决的最小问题。
那么如何让函数调用自身呢?这没有看起来那么难懂。前面说过,每次调用函数时,都将为此创建一个新的命名空间。这意味着函数调用自身时,是两个不同的函数[更准确地说,是不同版本(即命名空间不同)的同一个函数]在交流。你可将此视为两个属于相同物种的动物在彼此交流。
两个经典案例:阶乘和幂
首先,假设你要计算数字n的阶乘。n的阶乘为n × (n - 1) × (n - 2) × … × 1,在数学领域的用途非常广泛。例如,计算将n个人排成一队有多少种方式。如何计算阶乘呢?可使用循环。
def factorial(n): result = n for i in range(1, n): result *= i return result
这种实现可行,而且直截了当。大致而言,它是这样做的:首先将result
设置为n,再将其依次乘以1到n - 1的每个数字,最后返回result
。但如果你愿意,可采取不同的做法。关键在于阶乘的数学定义,可表述如下。
- 1的阶乘为1。
- 对于大于1的数字n,其阶乘为n - 1的阶乘再乘以n。
如你所见,这个定义与本节开头的定义完全等价。
下面来考虑如何使用函数来实现这个定义。理解这个定义后,实现起来其实非常简单。
def factorial(n): if n == 1: return 1 else: return n * factorial(n - 1)
再来看一个示例。假设你要计算幂,就像内置函数pow
和运算符**
所做的那样。要定义一个数字的整数次幂,有多种方式,但先来看一个简单的定义:power(x, n)
(x
的n
次幂)是将数字x
自乘n - 1
次的结果,即将n
个x
相乘的结果。换而言之,power(2, 3)
是2
自乘两次的结果,即2 × 2 × 2 = 8。
这实现起来很容易。
>>> def power(x,n): result = x for i in range(1,n): x *=result return x >>> power(2,3) 8
这是一个非常简单的小型函数,但也可将定义修改成递归式的。
- 对于任何数字
x
,power(x, 0)
都为1
。 n>0
时,power(x, n)
为power(x, n-1)
与x
的乘积。
如你所见,这种定义提供的结果与更简单的迭代定义完全相同。理解定义是最难的,而实现起来很容易。
def power(x, n): if n == 0: return 1 else: return x * power(x, n - 1)
我再次将定义从较为正规的文字描述转换成了编程语言(Python)。
提示 如果函数或算法复杂难懂,在实现前用自己的话进行明确的定义将大有裨益。以这种“准编程语言”编写的程序通常称为伪代码。
上面的递归完全是利用了它们的循环次数作用,结合if-else语句进行循环调用和结束调用。
另一个经典案例:二分查找
下面来看看最后一个递归示例——二分查找算法。
你可能熟悉猜心游戏。这个游戏要求猜对对方心里想的是什么,且整个猜测过程提出的“是否”问题不能超过20个。为充分利用每个问题,你力图让每个问题的答案将可能的范围减半。例如,如果你知道对方心里想的是一个人,可能问:“你心里想的是个女人吗?”除非你有很强的第六感,不然不会一开始就问:“你心里想的是John Cleese吗?”对喜欢数字的人来说,这个游戏的另一个版本是猜数。例如,对方心里想着一个1~100的数字,你必须猜出是哪个。当然,猜100次肯定猜对,但最少需要猜多少次呢?
实际上只需猜7次。首先问:“这个数字大于50吗?”如果答案是肯定的,再问:“这个数字大于75吗?”不断将可能的区间减半,直到猜对为止。你无需过多地思考就能成功。
这种策略适用于众多其他不同的情形。一个常见的问题是:指定的数字是否包含在已排序的序列中?如果包含,在什么位置?为解决这个问题,可采取同样的策略:“这个数字是否在序列中央的右边?”如果答案是否定的,再问:“它是否在序列的第二个四分之一区间内(左半部分的右边)?”依此类推。明确数字所处区间的上限和下限,并且每一个问题都将区间分成两半。
这里的关键是,这种算法自然而然地引出了递归式定义和实现。先来回顾一下定义,确保你知道该如何做。
- 如果上限和下限相同,就说明它们都指向数字所在的位置,因此将这个数字返回。
- 否则,找出区间的中间位置(上限和下限的平均值),再确定数字在左半部分还是右半部分。然后在继续在数字所在的那部分中查找。
在这个递归案例中,关键在于元素是经过排序的。找出中间的元素后,只需将其与要查找的数字进行比较即可。如果要查找的数字更大,肯定在右边;如果更小,它必然在左边。递归部分为“继续在数字所在的那部分中查找”,因为查找方式与定义所指定的完全相同。(请注意,这种查找算法返回数字应该在的位置。如果这个数字不在序列中,那么这个位置上的自然是另一个数字。)现在可以实现二分查找了。
def search(sequence, number, lower, upper):
if lower == upper:
assert number == sequence[upper]
return upper
else:
middle = (lower + upper) // 2
if number > sequence[middle]:
return search(sequence, number, middle \
+ 1, upper)
else:
return search(sequence, number, lower, middle)
这些代码所做的与定义完全一致:如果lower == upper
,就返回upper
,即上限。请注意,你假设(断言)找到的确实是要找的数字(number == sequence[upper]
)。如果还未达到基线条件,就找出中间位置,确定数字在它左边还是右边,再使用新的上限和下限递归地调用search
。为方便调用,还可将上限和下限设置为可选的。为此,只需给参数lower
和upper
指定默认值,并在函数开头添加如下条件语句:
def search(sequence, number, lower=0, upper=None):
if upper is None: upper = len(sequence) - 1
...
现在,如果你没有提供上限和下限,它们将分别设置为序列的第一个位置和最后一个位置。下面来看看这是否可行。
>>> seq = [34, 67, 8, 123, 4, 100, 95] >>> seq.sort() >>> seq [4, 8, 34, 67, 95, 100, 123] >>> search(seq, 34) 2 >>> search(seq, 100) 5
然而,为何要如此麻烦呢?首先,你可使用列表方法index
来查找。其次,即便你要自己实现这种功能,也可创建一个循环,让它从序列开头开始迭代,直至找到指定的数字。
确实,使用index
挺好,但使用简单循环可能效率低下。前面说过,要在100个数字中找到指定的数字,只需问7次;但使用循环时,在最糟的情况下需要问100次。你可能觉得“没什么大不了的”。但如果列表包含100 000 000 000 000 000 000 000 000 000 000 000个元素(对Python列表来说,这样的长度可能不现实),使用循环也将需要问这么多次,情况开始变得“很大”了。然而,如果使用二分查找,只需问117次。
效率非常高吧?
提示 实际上,模块bisect
提供了标准的二分查找实现。
bisect模块包含两个主要函数,bisect和insort,两个函数都利用二分查找算法来在有序序列中查找或插入元素。
bisect默认作用和bisect_right相同, insort默认作用和insort_right相同.
在查找方面,python中有list.index()的方法,此方法简单,但是复杂度高,建议使用bisect.
>>> import bisect >>> number = [1,2,5,3,5,7,4,8,9] >>> number.sort() #排序,因为为了直观看,在bisect内部会自动对列表进行排序,升序 >>> print(number) [1,2,3,4,5,5,7,8,9] >>> left = bisect,bisect_left(number , 3) #返回3在列表在左边的位置,没有就返回应该插入的位置 >>> print(left) 2
相关资料连接:https://blog.csdn.net/qq_41853758/article/details/83341655