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
     
     
  • 相关阅读:
    李永乐说股票
    01 什么是基金
    基金分享
    02 基金分类
    03 买什么基金合适
    06丨MongoDB基本操作
    详解大端模式和小端模式
    在UIViewController中获得Container View里的embed viewController的引用
    iphone Dev 开发实例10:How To Add a Slide-out Sidebar Menu in Your Apps
    iphone Dev 开发实例9:Create Grid Layout Using UICollectionView in iOS 6
  • 原文地址:https://www.cnblogs.com/xpc51/p/11709113.html
Copyright © 2011-2022 走看看