Python 2-03 递推和递归

递推和递归

一、递推算法 Recursion method

递推算法是通过已知条件,利用特定关系得出中间推论,直至得到结果的算法。递推算法分为顺推和逆推两种。动态规划

1、顺推法

所谓顺推法是从已知条件出发,逐步推算出要解决的问题的方法叫顺推。

# n! 阶乘
def factorial(n):
    t = 1
    for i in range(1, n + 1):
        t *= i
    return t

print(factorial(4))

递推法,就是递增法,时间复杂度是 O(n)。

2、逆推法

逆推法是从已知问题的结果出发,用迭代表达式逐步推算出问题的开始的条件,即顺推法的逆过程,称为逆推。

年初准备一笔钱存入银行,保证每年年底取出 1000 元,到第 3 年刚好取完。假设银行一年整存零取月息为 0.31%,请问需存入银行多少钱?

假设第 3 年年初的银行存款为 x 元,则 x×(1+0.0031×12) = 1000,得 x = 1000/(1+0.0031×12)。同理可得前两年年初的银行存款,计算如下:
第 2 年年初的银行存款 = (第 3 年年初的银行存款+1000)/(1+0.0031×12)
第 1 年年初的银行存款 = (第2年年初的银行存款+1000)/(1+0.0031×12)

def foo():
    total = 0.0  # 从第三年年底倒推
    for _ in range(3):
        total = (total + 1000) / (1 + 0.0031 * 12)
    return total

print("第一次必须向银行存入%.2f元\n" % foo())

二、递归函数 recursion

1、递归算法

递归算法(recursion algorithm)是指一种通过重复将问题分解为同类的子问题而解决问题的方法。递归完全可以取代循环。

优点:结构层次很清晰,易于理解。
缺点:在调用的过程中,每一层调用都需要保存临时性变量和返回地址、传递参数,因此执行效率低,还有可能会造成栈溢出。

如果一个函数在内部调用了自身,这个函数就被称为递归函数。
注意,这里我加了Python的 @functools.lru_cache() ,他会缓存递归结果,避免重复计算从而加速递归实现。没有这个的话会超时。
在 functools 模块中,有 lru_cache 这个一个神奇的装饰器存在。functools.lru_cache 的作用主要是用来做缓存,他能把相对耗时的函数结果进行保存,避免传入相同的参数重复计算。同时,缓存并不会无限增长,不用的缓存会被释放。

# n! 阶乘函数:f(n) = n*f(n-1)
@functools.lru_cache() 
def factorial(n):
    return 1 if n == 0 else factorial(n - 1) * n

print(factorial(4))

执行过程:

factorial(4) 
factorial(3) * 4 
factorial(2) * 3 * 4 
factorial(1) * 2 * 3 * 4 
factorial(0) * 1 * 2 * 3 * 4 
1 * 1 * 2 * 3 * 4 
1 * 2 * 3 * 4 
2 * 3 * 4 
6 * 4 
24

写法最简洁,但是效率最低,会出现大量的重复计算,时间复杂度O(1.618^n),而且最深度1000。

使用递归函数需要注意防止递归深度溢出,在Python中,通常情况下,这个深度是1000层,超过将抛出异常。函数递归调用是通过栈(stack)实现的,每当进入一个递归时,栈就会加一层,每当函数返回一次,栈就会减一层。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。

可以试试 factorial(999):

RuntimeError: maximum recursion depth exceeded in comparison

2、尾递归

尾递归是指递归函数在调用自身时直接传递其状态值。在这些语言中尾部递归不会占用调用堆栈空间。

# 尾递归
def factorial(n, res=1):
    if n < 2:
        return res
    return factorial(n - 1, n * res)

print(factorial(4))

执行过程:

factorial(4, 1) 
factorial(3, 4) 
factorial(2, 12) 
factorial(1, 24) 
factorial(0, 24) 
24

factorial 函数在递归调用的时候是将状态保存在 res 这个变量中,不会产生一系列逐渐增多的中间变量。

尾递归就是把当前的运算结果(或路径)放在参数里传给下层函数,如果在递归函数中,递归调用返回的结果总被直接返回,则称为尾递归。

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递归的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)

import time

def fib_recursion(num):
    '''
    直接使用递归法求解斐波那契数量的第 num 个数字
    '''
    if num <= 2:
        return 1
    return fib_recursion(num - 1) + fib_recursion(num - 2)

def fib_tail_recursion(num, res=0, temp=1):
    '''
    使用尾递归法求解斐波那契数量的第 num 个数字
    '''
    if num == 0:
        return res
    else:
        return fib_tail_recursion(num - 1, temp, res + temp)

def fib_loop(num):
    '''
    直接使用循环来求解
    '''
    a, b = 0, 1
    for i in range(1, num):
        a, b = b, a + b
        
    # return b
	# 生成器  
	yield b
	
if __name__ == '__main__':
    num_list = [5, 10, 20, 30, 40, 50]
    for num in num_list:
        start_time = time.time()
        print(fib_recursion(num))
        end_time = time.time()
        print(fib_tail_recursion(num))
        end_time2 = time.time()
        print(fib_loop(num))
        end_time3 = time.time()
        print('正在求解的斐波那契数字下标为 %s' % num)
        print('直接递归耗时为 :', end_time - start_time)
        print('尾递归调用耗时为:', end_time2 - end_time)
        print('直接使用循环耗时为:', end_time3 - end_time2)

尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。

遗憾的是,大多数编程语言没有针对尾递归做优化,Python 解释器也没有做优化,所以,即使把上面的 fib_recursion(n) 函数改成尾递归方式,也会导致栈溢出。

递推与递归的比较

使用递推写法的计算方式是自底向上,即从边界开始,不断向上解决问题,直到解决了目标问题;
而使用递归写法的计算方式是自顶向下, 即从目标问题开始,将它分解成子问题的组合,直到分解至边界为止。
相对于递归算法,递推算法免除了数据进出栈的过程,也就是说,不需要函数不断的向边界值靠拢,而直接从边界出发,直到求出函数值。递推的效率要高一些,在可能的情况下应尽量使用递推。

练习爬楼梯

题目:一次只能爬一阶或者两阶台阶,求爬到 n 阶有多少种走法。

假定 n = 10,要想到达第十级台阶,最后一步一定是从第八级或者第九级台阶开始。假设从地面到第八级台阶一共有 X 种走法,从地面到第九级台阶一共有 Y 种走法,那么从地面到第十级台阶一共有 X + Y 种走法。即 F(10) = F(9) + F(8)

和斐波那契数列比较看看有什么不同?

def climbStairs(x):
	if x <= 2:
		return x
	a, b = 1, 2
	for i in range(3, x + 1):
		a, b = b, a + b	
	return b

if __name__ == '__main__':
    for i in range(20):
        print(climbStairs(i))

题目:一栋楼有N阶楼梯,兔子每次可以跳1、2 或 3 阶,问一共有多少种走法?

# 递归法
def climbStairs(stairs):
     basic_num = {1:1,2:2,3:4} # 1、2、3 个台阶分别有 1、2、4 种方法
     if stairs in basic_num.keys():
         return basic_num[stairs]
     else:
     	# 从第四台阶开始,下一阶是前三阶的和。
         return climbStairs(stairs-1) + climbStairs(stairs-2) + climbStairs(stairs-3) 

# 递推法
def climbStairs(stairs):    
    basic_num = {1: 1, 2: 2, 3: 4}
    if stairs in basic_num.keys():
        return basic_num[stairs]
    else:
        h1, h2, h3, i = 1, 2, 4, 3
        while (i := i + 1) <= stairs:
            h1, h2, h3 = h2, h3, h1 + h2 + h3  # 从第四台阶开始,下一阶是前三阶的和。
        return h3

小结

遇到需要进行多层循环或者根本不清楚循环层数的场景,递归就很有用了,只要确定了终止条件和递归方程就可以实现遍历。

针对尾递归优化的语言可以通过尾递归防止栈溢出,尾递归事实上和循环是等价的。

Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。

练习

汉诺塔(Hanoi)游戏相传出现在古印度圣庙中。游戏的规则是:

在一块铜板装置上,有三根编号分别为A、B、C 的杆,在A杆上按自下而上、由大到小按顺序放置着 64 个金盘,每次只能移动一个金盘,并且在移动过程中三根杆上都始终保持大盘在下、小盘在上,操作过程中盘子可以置于A、B、C任意一杆上,如何把A杆上的金盘全部移到C杆上?

请编写 move(n, a, b, c)函数,它接收参数n,表示3个柱子A、B、C中第1个柱子A的盘子数量,然后打印出把所有盘子从A借助B移动到C的方法,例如:

def move(n, a, b, c):
  if n == 1:
        print(a, '-->', c)

# 期待输出:
# A --> C
# A --> B
# C --> B
# A --> C
# B --> A
# B --> C
# A --> C

move(3, 'A', 'B', 'C')

分析三个金盘的情况:rohanTower(3, ‘A’, ‘B’, ‘C’)
首先:把前 2 个借助于 C 移到 B 即:A–>C A–>B C–>B # 注意 交换柱子 递归 rohanTower(2, x, z, y)
其次:最后一个移到 C 即:A–>C
再次:把 B 柱 2 个借助于 A 移到 C 即:B–>A B–>C A–>C # rohanTower(2, y, x, z)

def rohanTower(n, x, y, z):
    if n == 1:
        print(f'{x}-->{z}')
    else:
        rohanTower(n - 1, x,z,y) # 把前 n-1 个借助于 c 移到 b
        print(f'{x}-->{z}') # 最后一个移到 c
        rohanTower(n - 1, y,x,z) # 把 b 柱 n-1 个借助于 a 移到 c


rohanTower(3, 'A', 'B', 'C')
# 只考虑移动次数
def hanoi(n):
    if n == 1:
        return 1
    else:
        return 2 * hanoi(n - 1) + 1

hanoi(3)

三、函数总结

第一、函数的使用可以重用代码。

第二、函数能封装内部实现,保护内部数据。很多时候,把函数看做“黑盒子”,即对应一定的输入会产生特定的结果或返回某个对象。往往函数的使用者并不是函数的编写者,函数的使用者对黑盒子的内部行为并不需要考虑,可以把精力投入到自身业务逻辑的设计而不是函数的实现细节。只有函数的设计者或者说编写者,才需要考虑函数内部实现的细节,如何暴露对外的接口,返回什么样的数据,也就是 API 的设计。

第三、即使某种功能在程序中只使用一次,将其以函数的形式实现也是有必要的,因为函数使得程序模块化,从“一团散沙”变成“整齐方队”,从而有利于程序的阅读、调用、修改和完善。

70. 爬楼梯

class Solution:
    def climbStairs(self, n: int) -> int:
        # def f(m):
        #     if m < 2:
        #         return 1
        #     if cache[m] != -1:
        #         return cache[m]
            
        #     cache[m] = f(m - 1) + f(m - 2)
        #     return cache[m]
        
        # cache = [-1] * 46
        # return f(n)

        @cache
        def f(m):
            if m < 2:
                return 1
            return f(m - 1) + f(m - 2)
        return f(n)

509. 斐波那契数

class Solution:
    @cache
    def f(self, n):
        if n < 2:
            return n
        return self.f(n - 1) + self.f(n - 2)

    def fib(self, n: int) -> int:
        return self.f(n)

面试题 08.01. 三步问题

class Solution:
    @cache
    def waysToStep(self, n: int) -> int:
        MOD = 1000000007;
        # a, b, c = 1, 1, 2
        # for i in range(1, n + 1):
        #     a, b, c = b, c, (a + b + c) % MOD
        # return a % MOD
        dp = [0] * (n + 1)
        dp[1] = 1
        dp[2] = 2
        for i in range(1, n + 1):
            dp[i] = (dp[i - 1] + dp[i - 2] + dp[i - 3]) % MOD
        return dp[n]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值