若有侵权,立即删除

1. 问题

一大早,前同事在微信上给出了个题: 一只青蛙上台阶,一次只能上一个或两个台阶,如果总共有3个台阶,那么有三种上法:

  1. 111 — 每次上一个台阶
  2. 21 — 先上两个台阶,再上一个台阶
  3. 12 — 先上一个台阶,再上两个台阶

那么对于 n 个台阶,有多少种上法呢?

python动态手势识别系统代码_递归

2. 思路

乍一看觉得并不难,只要简单的罗列就好了,题目描述不也是这么描述的嘛?但是仔细一想,罗列的情况十分复杂。 突然想到单源最短路问题,其实这就是经典的动态规划问题 — 单源最短路问题的一个变种,我们如果把每个台阶想象成一张有向加权图的点,每个点都由他前面两个点指向他,权重分别为1和2,这就转化成了一个经典动态规划问题了 — 由点0到点n有多少种走法。

python动态手势识别系统代码_代理模式_02

显然,f(n)=f(n-1)+f(n-2),同时,f(1)=1,f(2)=2。 写代码就简单多了,用一个递归即可。

3. 代码实现

# -*- coding:utf-8 -*-
import time

def jumpFloor(number):
    if number == 0:
        return 0
    if number == 1:
        return 1
    if number == 2:
        return 2
    return jumpFloor(number - 1) + jumpFloor(number - 2)

if __name__ == '__main__':
    starttime = time.time()
    print(jumpFloor(40))
    print('耗时:' + str(time.time() - starttime))

复制

执行结果:

165580141 耗时:30.301055192947388

  • 什么?仅仅40个台阶,就要花30多秒来计算,再继续增加台阶数,时间将会成倍增长,这怎么受得了呢?

4. 尾递归

4.1. 讲解

上述递归过程之所以耗时,是因为每一次递归都需要在栈中开辟新的调用栈执行子方法,在栈的反复开辟、销毁过程中,不仅耗时激增,最为关键的是,反复的栈开辟让内存占用率急剧增长。 在 C 语言中,编译器有一个概念 — 尾递归,当编译器检测到一个函数调用的递归是函数的最后一行,那么编译器就覆盖当前的栈而不是在当前栈中去创建一个新的栈。 下面我们将上述代码改为尾递归的方式,基本思路是通过一个参数来保存上次执行结果,另一个参数保存累计值。

4.2. 代码

# -*- coding:utf-8 -*-
import time

def jumpFloor(number):
    if number == 0:
        return 0
    if number == 1:
        return 1
    if number == 2:
        return 2
    return solute(number, 0, 1)

def solute(number, a, b):
    if number == 0:
        return b
    return solute(number-1, b, a+b)

if __name__ == '__main__':
    starttime = time.time()
    print(jumpFloor(400))
    print('耗时:' + str(time.time() - starttime))

程序定义了两个函数:jumpFloor和solute。

jumpFloor函数接收一个参数number,并返回solute函数的结果。在solute函数中,又调用了jumpFloor函数,形成了一个递归调用。

solute函数接收三个参数:number,a和b。在每次递归调用中,number减1,a和b的值互换,并且b的值变为a+b。

如果我们以jumpFloor(3)为例,那么:

  1. 首次调用jumpFloor(3)时,会返回solute(3, 0, 1)的结果。
  2. 在solute(3, 0, 1)中,会返回solute(2, 1, 1)的结果。
  3. 在solute(2, 1, 1)中,会返回solute(1, 1, 2)的结果。
  4. 在solute(1, 1, 2)中,会返回solute(0, 2, 3)的结果。
  5. 在solute(0, 2, 3)中,会返回b的值,即3。

因此,jumpFloor(3)的结果为3。

但是需要注意的是,这个程序计算的并非简单的斐波那契数列(即0、1、1、2、3、5、8...),而是将斐波那契数列的索引从0开始计数(即0、1、2、3、4、5...),并且返回的是斐波那契数列的索引+1的值。

4.3. 运行结果

284812298108489611757988937681460995615380088782304890986477195645969271404032323901 耗时:0.0005254745483398438

我们看到,运行 400 级台阶的计算结果都只有 0.5 毫秒,优化效果相当明显。

5. 手动实现 python 的尾递归优化

上述代码如果将台阶层数增加到几千就会抛出异常:

RecursionError: maximum recursion depth exceeded in comparison

我们调试一下:

python动态手势识别系统代码_递归_03

可以看到,python 解释器并不会像 C 语言编译器一样对尾递归进行优化。 上述代码之所以能够让时间复杂度、空间复杂度大幅下降,其唯一的原因是将原有的两次创建栈、压栈、销毁的一系列工作变为了一次,而问题并没有从根本上解决。 怎么才能像 C 语言一样,在每次递归调用完成后,自动清理原有的栈呢? 在捕获异常后,作为异常处理的一个环节,python 解释器会自动清理原有的栈,那么通过 python 的异常机制,我们就可以实现上述功能。

# -*- coding:utf-8 -*-
import sys
import time

def jumpFloor(number):
    if number == 0:
        return 0
    if number == 1:
        return 1
    if number == 2:
        return 2
    return solute(number, 0, 1)

class TailRecurseException(Exception):
    def __init__(self, args, kwargs):
        self.args = args
        self.kwargs = kwargs

def tail_call_optimized(g):
    def func(*args, **kwargs):
        f = sys._getframe()
        if f.f_back and f.f_back.f_back and f.f_back.f_back.f_code == f.f_code:
            raise TailRecurseException(args, kwargs)
        else:
            while 1:
                try:
                    return g(*args, **kwargs)
                except TailRecurseException as e:
                    args = e.args
                    kwargs = e.kwargs
    return func

@tail_call_optimized
def solute(number, a, b):
    if number == 0:
        return b
    return solute(number-1, b, a+b)

if __name__ == '__main__':
    starttime = time.time()
    print(jumpFloor(4000))
    print('耗时:' + str(time.time() - starttime))

这是一个使用Python编写的程序,它主要实现了斐波那契数列的计算,并通过尾递归优化来避免递归过程中的栈溢出问题。接下来我将详细解释程序的运行过程。

首先,定义了一个名为jumpFloor的函数,这个函数接收一个参数number,如果number为0、1或2,则直接返回对应的值。否则,它会调用solute函数,并将number、0和1作为参数传入。

接着,定义了一个名为TailRecurseException的异常类,用于在尾递归优化中抛出异常。

然后,定义了一个名为tail_call_optimized的函数,这个函数用于实现尾递归优化。它接收一个函数g作为参数,并返回一个新的函数funcfunc函数会检查当前调用栈中的代码是否与上一个调用栈中的代码相同,如果相同,则抛出TailRecurseException异常,否则,它会不断尝试调用g函数,并在抛出异常时捕获并更新参数。

接着,定义了一个名为solute的函数,它接收三个参数:numberab。如果number为0,则返回b,否则,它会调用自身并将number-1ba+b作为参数传入。这个函数被装饰器tail_call_optimized装饰,因此它会进行尾递归优化。

最后,在主函数中,首先记录了当前的时间,然后调用jumpFloor函数并传入参数4000,打印函数的返回值和计算所花费的时间。

总的来说,这个程序通过尾递归优化避免了斐波那契数列计算中的栈溢出问题,并计算了斐波那契数列的第4000个值。

5.1. 讲解

执行结果:

64574884490948173531376949015369595644413900640151342708407577598177210359034088914449477807287241743760741523783818897499227009742183152482019062763550798743704275106856470216307593623057388506776767202069670477506088895294300509291166023947866841763853953813982281703936665369922709095308006821399524780721049955829191407029943622087779296459174012610148659520381170452591141331949336080577141708645783606636081941915217355115810993973945783493983844592749672661361548061615756595818944317619922097369917676974058206341892088144549337974422952140132621568340701016273422727827762726153066303093052982051757444742428033107522419466219655780413101759505231617222578292486081002391218785189299675757766920269402348733644662725774717740924068828300186439425921761082545463164628807702653752619616157324434040342057336683279284098590801501 耗时:0.014540433883666992

我们通过装饰器的一层封装,每一次祖父调用与当前调用相同时就标志着递归的发生,因为父调用是装饰器中的调用,祖父调用与当前调用都是原代码中的函数调用,相同就说明了递归的发生。 通过抛出异常清理栈空间,同时异常类中记录了当前参数,捕获异常后继续使用当前参数调用,这就解决了栈深度不断增加的问题。

python动态手势识别系统代码_代理模式_04

  • 需要注意的是,原代码必须是尾递归的方式才可以用该装饰器优化,否则将导致后续代码无法执行,从而得到错误的结果

6. 终极优化 — 迭代

6.1. 思路

上述的所有问题其实都是递归引起的,而任何一个递归的方法都可以转换为迭代法,尤其是我们本文的这个问题:

f(n)=f(n-1)+f(n-2)

这不就是斐波那契数列吗? 哈哈,可是如果我们开篇就使用迭代来完成,那就无法引出这么多优化的思路了。

6.2. 代码

# -*- coding:utf-8 -*-
import time

def jumpFloor(number):
    i = 1
    j = 2
    if number == 0:
        return 0
    if number == 1:
        return i
    if number == 2:
        return j
    r = 0
    for x in range(3, number + 1):
        r = i + j
        if x % 2:
            i = r
        else:
            j = r
    return r

if __name__ == '__main__':
    starttime = time.time()
    print(jumpFloor(40))
    print('耗时:' + str(time.time() - starttime))

复制

6.3. 执行结果

165580141 耗时:0.0

所以还是迭代的方法简单明了,同时也拥有着最高的性能,推荐大家尽量使用迭代的方法来解决问题。 虽然有些问题通过递归的方法可以更容易理解,但先写出递归的代码,再转化为迭代的方法也非常简单。