首页 > 编程语言 >Python 2-03 递推和递归

Python 2-03 递推和递归

时间:2023-05-22 11:05:18浏览次数:40  
标签:03 return 函数 递归 Python factorial 递推 def


递推和递归

一、递推算法 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() ,他会缓存递归结果,避免重复计算从而加速递归实现。没有这个的话会超时。

# 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 2-03 递推和递归_递归

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

遗憾的是,大多数编程语言没有针对尾递归做优化,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的设计。

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


标签:03,return,函数,递归,Python,factorial,递推,def
From: https://blog.51cto.com/u_1439909/6321618

相关文章

  • Python 2-07 装饰器 @decorator
    Python装饰器@decoratorPython装饰器其实就是对函数的包装,函数作为参数,在不修改函数源代码的基础上,并对函数做一些包装,然后返回增加了包装的函数,即生成了一个新函数。登录校验,权限校验,日志记录等,这些功能在各个环节都可能需要,但又十分雷同,可以通过装饰器来抽象、剥离这部分代码......
  • Python 4-09 time
    time 在 Python 中与时间处理有关的模块包括 time,datetime 以及 calendar。在 Python 中,用三种方式来表示时间,分别是时间戳、格式化时间字符串和结构化时间。时间戳(timestamp):1970年1月1日之后的秒,可以通过 time.time() 获得。时间戳是一个浮点数,可以进行加减运算,但......
  • Python 1-18 字典
    Python1-18字典Python的字典数据类型采用键值对(key:value)的形式,根据key的值计算value的地址,具有非常快的查取和插入速度。例如,用list实现成绩单:#给定一个名字,要查找对应的成绩,就先要在names中找到对应的位置,再从scores取出对应的成绩,list越长,耗时越长。names=......
  • Python 1-17 元组
    Python1-17元组classtuple([iterable])tuple是一个不可变的序列类型。>>>s='abc'>>>l=[1,2]>>>t=1,2>>>d=dict(a=1,b=2)>>>set={'a','b'}1、元组创建>>>tup=()#创建空元组>>......
  • Python 2-02 命名空间和作用域
    命名空间和作用域一、命名空间命名空间(Namespace)是从名称到对象的映射,一般用Python字典来实现。为了解决项目中名字冲突的问题引入了命名空间的概念,命名空间可以嵌套。1、命名空间分类:内置名称(built-innames),Python语言内置的名称,比如函数名abs、char和异常名称Exception......
  • Python 2-01 函数
    一、函数定义def函数名(参数列表):函数体判断一个数是不是素数?#方法一:for循环判断素数num=int(input('请输入一个正整数:'))foriinrange(2,int(num**0.5)+1):ifnotnum%i:print(f'{num}不是素数')breakelse: print(f'{num}是素数')......
  • Python 2-06 闭包
    闭包Closures嵌套函数(nestedfunction),内函数引用了外函数的临时变量,并且外函数返回内函数的引用,这样就构成了一个闭包。defouter():x,y,z=10,'abc',[1,2]definner():print(x,y)returninnerf=outer()print(f.__closure__)#celltuple......
  • Python 1-24 练习五 综合练习
    1、无重复字符的最长子串给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。#substr向右扩展一个元素,如果该元素已经在substr中,则需要将其及其前面的元素去掉。#可通过substr.index(c)定位元素或substr.split(c)[1]分割成子串#发现有重复字符时,可......
  • Python 2-05 高阶函数
    一、函数式编程函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。而函数式编程(请注意多了一个“式”字)——FunctionalProgrammi......
  • Python 3-11 异常处理
    异常处理一、错误和异常1、句法错误句法错误又称解析错误:>>>whileTrueprint('Helloworld')File"<stdin>",line1whileTrueprint('Helloworld')^SyntaxError:invalidsyntax解析器会复现出现句法错误的代码行,并用小“箭头”指向行里检测到的第一......