zoukankan      html  css  js  c++  java
  • Python递归函数

    函数执行流程

    要明白递归首先需要明白函数的执行流程。

    比如下面的这段程序。

    def foo1(b, b1=3):
        print("foo1 called", b, b1)
    
    def foo2(c):
        foo3(c)
        print("foo2 called", c)
    
    def foo3(d):
        print("foo3 called", d)
    def main():
        print("main called")
        foo1(100, 101)
        foo2(200)
        print("main ending")
    
    main()
    
    结果为:
    main called
    foo1 called 100 101
    foo3 called 200
    foo2 called 200
    main ending

    上面的代码执行流程为:

    1. 首先在全局帧中生成foo1、foo2、foo3、main函数对象。
    2. 然后main函数调用。
    3. main函数中查找内建函数print压栈,然后将常量字符串压栈,然后调用函数,弹出栈顶。
    4. main函数中全局查找函数foo1压栈,然后将常量100,101压栈,调用函数foo1,创建栈帧。然后字符串和变量b,b1压栈,调用函数,弹出栈顶,返回值。
    5. main函数中全局查找函数foo2函数压栈,然后将常量200压栈,调用foo2,创建栈帧。foo3函数压栈,变量c引用压栈,调用foo3,创建栈帧。foo3完成print函数调用后返回。foo2恢复调用,执行print后,返回值。main中foo2调用结束弹出栈顶,main继续执行print函数调用,弹出栈顶,main函数返回。

    递归

    所谓的递归也就是函数直接或者间接调用自身。需要注意的是递归一定要有边界条件(不然就就是死循环),递归前进段、递归返回段,当边界条件不满足的时候,递归前进,而当递归条件满足的时候,递归返回。

    递归相比于循环,只是让解决方案更加清晰,并没有性能上的优势。在有些情况下,如果使用循环,程序的性能可能会更高,而如果使用递归的话,程序可能更容易理解。

    比如要生成一个斐波拉契数列,1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ... ߧ如果设f(n)为该数列的第n项,那么可以推导出这样的公式:f(n)=f(n-1)+f(n-2).

    使用循环,生成斐波拉契数列,代码为:

    pre = 0
    cur = 1#第一个
    print(pre,cur,end=" ")
    n = 4
    for i in range(n-1):
        pre,cur = cur,pre+cur
        print(cur,end = " ")
    
    结果为:
    
    0 1 1 2 3 
    
    将它抽象出函数:
    
    def fib(n):
        pre = 0
        cur = 1
        print(cur,end=" ")
        for i in range(n-1):
            pre,cur = cur,pre+cur
            print(cur,end = " ")
    fib(10)
    
    结果为:
    
    1 1 2 3 5 8 13 21 34 55 

    而如果使用递归的话,代码可以很简洁:

    #第n项的数字为:
    
    def fib(n):
        if n<2:
            return 1
        else:
            return fib(n-1)+fib(n-2)
    fib(10)
    
    上面的代码可以简化为:
    def fib(n):
        return 1 if n<2 else fib(n-1)+fib(n-2)
    fib(10)

    而生成数列的代码为:

    def fib(n):
        return 1 if n <2 else fib(n-1) + fib(n-2)
    
    print(fib(10))
    
    for i in range(11):
        print(fib(i), end=' ')
    
    结果为:
    89
    1 1 2 3 5 8 13 21 34 55 89 

    由上面的例子可以看到,递归一定要有退出条件,而递归调用一定会执行到这个退出条件,如果没有退出条件的递归调用,就是无限调用,会陷入无限循环,而且递归调用的深度不宜过深,Python对递归调用的深度做了限制(1000)以保护解释器,超过递归深度的限制,会抛出recursionerror错误,同时可以使用sys.getrecursionlimil()设置递归的深度。

    递归的性能

    循环稍微复杂一些,但是只要不是死循环,就可以多次迭代算出结果,递归函数的代码极简易懂,但是只能获取到最外层的函数调用,内部的递归结果都是中间结果,而且给定一个N都要进行近2N次递归,深度越深,效率就会越低,为了获取斐波拉契数列需要再在外面套一个n的循环,效率就更低了。如果递归复杂,函数会反复的压栈,这样栈的内存很快就会溢出。所以会有递归深度限制。如下面的代码:

    import datetime
    start = datetime.datetime.now()
    pre = 0
    cur = 1 # No1
    print(pre, cur, end=' ')
    n = 35
    for i in range(n-1):
        pre, cur = cur, pre + cur
        print(cur, end=' ')
        delta = (datetime.datetime.now() -start).total_seconds()
    
    print(delta)
    
    结果为:
    
    0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 832040 1346269 2178309 3524578 5702887 9227465 0.001
    
    import datetime
    n = 35
    start = datetime.datetime.now()
    def fib(n):
        return 1 if n < 2 else fib(n-1) + fib(n-2)
    for i in range(n):
        print(fib(i), end=' ')
        delta = (datetime.datetime.now() - start).total_seconds()
    print(delta)
    
    结果为:
    
    1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 832040 1346269 2178309 3524578 5702887 9227465 9.371536

    上面的例子可以看到for循环,只需要0.01秒就可以很快算出35个斐波拉契数列,而递归的话则需要9秒多。效率特别低,这个时候有什么可以改进的吗?

    pre = 0
    cur = 1#第一个
    print(pre,cur,end=" ")
    def fib(n,pre = 0,cur = 1):
        pre,cur = cur,pre+cur
        print(cur,end=" ")
        if n ==2:
            return
        fib(n-1,pre,cur)
        
    fib(10)
    
    结果为:
    
    0 1 1 2 3 5 8 13 21 34 55 

    上面的这个递归函数就和循环的思想很类似了,参数n是边界条件,用n来计数,上一次的计算结果直接用作函数的实参,这样的话,效率更高,和循环相比较的话,性能更近。

    所以递归并不一样都是效率低下,但是它有深度限制。

    可以对比下三个斐波拉契数列生成函数的效率。如下:

    import datetime
    # Fib Seq
    start = datetime.datetime.now()
    pre = 0
    cur = 1 # No1
    print(pre, cur, end=' ')
    n = 35
    # loop
    for i in range(n-1):
        pre, cur = cur, pre + cur
        print(cur, end=' ')
    delta = (datetime.datetime.now() - start).total_seconds()
    print(delta)
    
    
    # Fib Seq
    start = datetime.datetime.now()
    pre = 0
    cur = 1 # No1
    print(pre, cur, end=' ')
    # recursion
    def fib1(n, pre=0,cur=1):
        pre, cur = cur, pre + cur
        print(cur, end=' ')
        if n == 2:
            return
        fib1(n-1, pre, cur)
    
    fib1(35)
    delta = (datetime.datetime.now() - start).total_seconds()
    print(delta)
    
    
    start = datetime.datetime.now()
    def fib2(n):
        if n < 2:
            return 1
        return fib2(n-1) + fib2(n-2)
    
    for i in range(n):
        print(fib2(i), end=' ')
    delta = (datetime.datetime.now() - start).total_seconds()
    print(delta)

    前面两个性能相当,而第三个效率特特别低下。写递归应该尽量避免写成第三个这样的。

    间接递归

    所谓的间接递归,就是通过别的函数间接调用了函数自身,但是,如果构成了循环递归调用就是件非常危险的事情,而且这种情况在代码复杂的情况下, 很还可能发生。所以一定要规范代码,来避免发生这种调用的发生。比如下面这个例子。

    def foo1():
        foo2()
    def foo2():
        foo1()
    foo1()

    上面的代码就会陷入死循环。应该尽量的避免。

    递归总结:

    1. 递归是一种很自然的表达,符合逻辑思维。
    2. 递归相对来讲运行效率低,每一次调用函数都要开辟栈帧。
    3. 递归有深度限制,如果递归的层次太深,函数反复的压栈,栈内存很快就会溢出。
    4. 如果是有限次的递归,可以使用递归调用,或者使用循环代码,循环代码稍微复杂一些,但是只要不是死循环,可以多次迭代直至算出结果。
    5. 绝大多数递归,都可以使用循环来实现。
    6. 即便递归代码简洁,但是能不用则不用递归。

    练习题:

    1. 求n的阶乘!
    2. 将一个数逆序放入列表中,比如1234--[4,3,2,1]
    3. 解决猴子吃桃问题:猴子第一天摘下若干个桃子,马上吃了一半,还不过瘾,又多吃了一个,第二天早上又将剩下的桃子吃掉了一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想吃的时候,只剩下一个桃子,求第一天共摘了多少个桃子。
    #求阶乘
    def fac(n):#性能最好
        if n==1:
            return 1
        else:
            return n*fac(n-1)
    fac(10)
    
    结果为:
    
    3628800
    
    def fac1(n,p=1):
        if n==1:
            return p
        p*= n
        print(p)
        fac1(n-1,p)
        return p
    
    fac1(10)
    
    结果为:
    
    10
    90
    720
    5040
    30240
    151200
    604800
    1814400
    3628800
    10
    
    def fac2(n,p=None):
        if p is None:
            p = [1]
        if n == 1:
            return p[0]
        p[0]*=n
        #print(p[0])
        fac2(n-1,p)
        return p[0]
    fac2(10)
    
    结果为:
    3628800
    
    def fac3(n,p=1):
        p*=n
        if n==1:
            return p
        else:
            return fac4(n-1,p)
    
    fac3(10)
    
    结果为:
    3628800

    2.将一个数逆序放入列表中

    data = str(1234)
    
    def revert(x):
        if x==-1:
            return " "
        return data[x]+revert(x-1)
    
    print(revert(len(data)-1))
    
    结果为:
    4321 
    
    def revert1(n,lst = None):
        if lst is None:
            lst = []
            
        lst.append(n%10)
        if n//10 ==0:
            return lst
        return revert1(n//10,lst)
    revert1(12345)
    
    结果为:
    
    [5, 4, 3, 2, 1]
    
    def revert2(n,lst = None):
        if lst is None:
            lst = []
            
        x,y=divmod(n,10)
        lst.append(y)
        if x==0:
            return lst
        return revert2(x,lst)
    
    revert2(12345)
    
    结果为:
    [5, 4, 3, 2, 1]
    
    num = 12354
    
    def revert3(num,target=[]):
        if num:
            target.append(num[len(num)-1])#target.append(num[-1:])
            revert3(num[:len(num)-1])
        return target
    print(revert3(str(num)))
    
    结果为:
    ['4', '5', '3', '2', '1']

    猴子吃桃问题:

    def peach(day= 9,sum = 1):
        sum=2*(sum+1)
        day-=1
        if day ==0:
            return sum
        return peach(day,sum)
    print(peach())
    
    结果为:
    1534
    
    def monkey(n):
        if n==1:
            return 1
        return 2*monkey(n-1)+2
    monkey(10)
    
    结果为:
    1534

    这个猴子吃桃的问题,可以假设猴子摘了x个桃子

    d1: x//2-1

    d2:d1//2-1

    d3:d2//2-1

    ……

    d9:d8//2-1

    d10:1

    那么:

    def peach(day = 10):if day==1:
            return 1
        return (peach(day-1)+1)*2
    print(peach())
    
    结果为:
    1534

    应该注意的是,这里必须是10,因为return (peach(day-1)+1)*2立即拿不到结果,必须通过再一次进入函数时判断是不是到了最后一天。也就是当前使用的值是由下一次函数调用得到的,所以要执行10次函数调用。换一种方式表达:

    def peach(days = 1):
        if days==10:
            return 1
        return (peach(days+1)+1)*2
    print(peach())
    
    结果为:
    1534
     
     
  • 相关阅读:
    896. Monotonic Array单调数组
    865. Smallest Subtree with all the Deepest Nodes 有最深节点的最小子树
    489. Robot Room Cleaner扫地机器人
    JavaFX
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
  • 原文地址:https://www.cnblogs.com/xpc51/p/11709113.html
Copyright © 2011-2022 走看看