Python 递归函数

Python 递归函数递归函数在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。递归函数特性:必须有一个明确的结束条件; 每次进入更深一层递归时,问题规模相比上次递归都应有所减少 相邻两次重复之间有紧密的联系,前一次要为后一次做准备(通常前一次的输出就作为后一次的输入)。 递归效率不高,递归层次过多会导致栈溢出(在计算机中,函数调用是通过栈(stack)这种数据结构实…

大家好,又见面了,我是你们的朋友全栈君。

递归函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

递归函数特性:

  1. 必须有一个明确的结束条件;
  2. 每次进入更深一层递归时,问题规模相比上次递归都应有所减少
  3. 相邻两次重复之间有紧密的联系,前一次要为后一次做准备(通常前一次的输出就作为后一次的输入)。
  4. 递归效率不高,递归层次过多会导致栈溢出(在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出)

先举个简单的例子:计算1到100之间相加之和;通过循环和递归两种方式实现

# 循环方式 
def sum_cycle(n): 
    sum = 0 
    for i in range(1,n+1) : 
        sum += i print(sum)

# 递归方式 
def sum_recu(n): 
    if n>0: 
       return n +sum_recu(n-1) 
    else: 
       return 0 

sum_cycle(100) 
sum = sum_recu(100) print(sum)

结果:

5050
5050

 

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

***使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。

把上面的递归求和函数的参数改成10000就导致栈溢出!

RecursionError: maximum recursion depth exceeded in comparison

**解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。

 

一般递归

def normal_recursion(n):
    if n == 1:
        return 1
    else:
        return n + normal_recursion(n-1)

执行:

normal_recursion(5)
5 + normal_recursion(4)
5 + 4 + normal_recursion(3)
5 + 4 + 3 + normal_recursion(2)
5 + 4 + 3 + 2 + normal_recursion(1)
5 + 4 + 3 + 3
5 + 4 + 6
5 + 10
15

可以看到, 一般递归, 每一级递归都需要调用函数, 会创建新的栈,随着递归深度的增加, 创建的栈越来越多, 造成爆栈:boom:

 

尾递归(http://www.open-open.com/lib/view/open1480494663229.html

尾递归基于函数的尾调用, 每一级调用直接返回函数的返回值更新调用栈,而不用创建新的调用栈, 类似迭代的实现, 时间和空间上均优化了一般递归!

def tail_recursion(n, total=0):
    if n == 0:
        return total
    else:
        return tail_recursion(n-1, total+n)

执行:

tail_recursion(5)
tail_recursion(4, 5)
tail_recursion(3, 9)
tail_recursion(2, 12)
tail_recursion(1, 14)
tail_recursion(0, 15)
15

可以看到, 每一级递归的函数调用变成”线性”的形式.

深入理解尾递归

呃, 所以呢? 是不是感觉还不够过瘾… 谁说尾递归调用就不用创建新的栈呢?

还是让我们去底层一探究竟吧

int tail_recursion(int n, int total) {
    if (n == 0) {
        return total;
    }
    else {
        return tail_recursion(n-1, total+n);
    }
}

int main(void) {
    int total = 0, n = 4;
    tail_recursion(n, total);
    return 0;
}

反汇编

  • $ gcc -S tail_recursion.c -o normal_recursion.S

  • $ gcc -S -O2 tail_recursion.c -o tail_recursion.S gcc开启尾递归优化

对比反汇编代码如下(AT&T语法)

Python 递归函数

可以看到, 开启尾递归优化前, 使用call调用函数, 创建了新的调用栈(LBB0_3);

而开启尾递归优化后, 就没有新的调用栈生成了, 而是直接pop

bp指向的 _tail_recursion 函数的地址(pushq %rbp)然后返回,

Python 递归函数

仍旧用的是同一个调用栈!

存在的问题

虽然尾递归优化很好, 但python 不支持尾递归,递归深度超过1000时会报错

一个牛人想出的解决办法

实现一个 tail_call_optimized 装饰器

#!/usr/bin/env python2.4
# This program shows off a python decorator(
# which implements tail call optimization. It
# does this by throwing an exception if it is
# it's own grandparent, and catching such
# exceptions to recall the stack.

import sys

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

def tail_call_optimized(g):
    """
    This function decorates a function with tail call
    optimization. It does this by throwing an exception
    if it is it's own grandparent, and catching such
    exceptions to fake the tail call optimization.

    This function fails if the decorated
    function recurses in a non-tail context.
    """
    def func(*args, **kwargs):
        f = sys._getframe()
        # 为什么是grandparent, 函数默认的第一层递归是父调用,
        # 对于尾递归, 不希望产生新的函数调用(即:祖父调用),
        # 所以这里抛出异常, 拿到参数, 退出被修饰函数的递归调用栈!(后面有动图分析)
        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, e:
                    # 捕获异常, 拿到参数, 退出被修饰函数的递归调用栈
                    args = e.args
                    kwargs = e.kwargs
    func.__doc__ = g.__doc__
    return func

@tail_call_optimized
def factorial(n, acc=1):
    "calculate a factorial"
    if n == 0:
        return acc
    return factorial(n-1, n*acc)

print factorial(10000)

为了更清晰的展示开启尾递归优化前、后调用栈的变化和tail_call_optimized装饰器抛异常退出递归调用栈的作用, 我这里利用 pudb调试工具 做了动图 <br/>

开启尾递归优化前的调用栈

Python 递归函数

开启尾递归优化后(tail_call_optimized装饰器)的调用栈

Python 递归函数

通过pudb右边栏的stack, 可以很清晰的看到调用栈的变化.

因为尾递归没有调用栈的嵌套, 所以Python也不会报 RuntimeError: maximum recursion depth exceeded 错误了!

这里解释一下 sys._getframe() 函数:

sys._getframe([depth]):
Return a frame object from the call stack.
If optional integer depth is given, return the frame object that many calls below the top of the stack.
If that is deeper than the call stack, ValueEfror is raised. The default for depth is zero,
returning the frame at the top of the call stack.

即返回depth深度调用的栈帧对象.

import sys

def get_cur_info():
    print sys._getframe().f_code.co_filename  # 当前文件名
    print sys._getframe().f_code.co_name  # 当前函数名
    print sys._getframe().f_lineno # 当前行号
    print sys._getframe().f_back # 调用者的帧

补充

二分法查找大家应该听说过;就是一种快速查找的方法,时间复杂度低,逻辑简单易懂,总的来说就是不断的找出中间值,用中间值对比你需要找的实际值;若中间值大,则继续找左边;若中间值小,则继续找右边;可以看出二分法就是不断重复此上过程,所以就可以通过递归方式来实现二分法查找了!

#The binary search function

def  Binary_Search(data_source,find_n):
    #判断列表长度是否大于1,小于1就是一个值
    if len(data_source) >= 1: 
        #获取列表中间索引;奇数长度列表长度除以2会得到小数,通过int将转换整型     
        mid = int(len(data_source)/2) 
        #判断查找值是否超出最大值   
        if find_n > data_source[-1]:                                    
            print('{}查找值不存在!'.format(find_n))
            exit()
        #判断查找值是否超出最小值
        elif find_n < data_source[0]:                                   
            print('{}查找值不存在!'.format(find_n))
            exit()
        #判断列表中间值是否大于查找值
        if data_source[mid]  > find_n:                                  
            print('查找值在 {} 左边'.format(data_source[mid]))
            #调用自己,并将中间值左边所有元素做参数
            Binary_Search(data_source[:mid],find_n)  
        #判断列表中间值是否小于查找值                   
        elif data_source[mid] < find_n:                                 
            #print('查找值在 {} 右边'.format(data_source[mid]))     
            #调用自己,并将中间值右边所有元素做参数      
            Binary_Search(data_source[mid:],find_n)
        else:
            #找到查找值
            print('找到查找值',data_source[mid])                          
    else:
        #特殊情况,返回查找不到
        print('{}查找值不存在!'.format(find_n))                          

Data = [22,12,41,99,101,323,1009,232,887,97]
#列表从小到大排序
Data.sort()       
#查找323                                                      
Binary_Search(Data,323)   
                                              
执行结果:
找到查找值 323
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/147628.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • Oracle11g软硬件基本要求,Oracle 11g的安装

    Oracle11g软硬件基本要求,Oracle 11g的安装Oracle11g有基本安装和高级安装两种方式。两种方式对硬件要求也不相同,oracle11g软件非常大,对硬件要求很高。目前只是讲述在windows环境下的安装,Linux环境下安装以后会讲,下表给出了安装Oracle11g所需的硬件配置。系统要求说明CPU最低主频550MHZ以上内存1GB以上虚拟内存物理内存的2倍磁盘空间基本安装需4.55G,高级安装需4.92G一、Windows环境下安装…

    2022年7月25日
    30
  • 新式单片机视频教程下载

    新式单片机视频教程下载新式单片机视频教程下载点击此处进入下载页面【文件名称】——国内首创新式单片机视频教程【文件描述】单片机学习资料,新手绝佳教程….【注意事项】压缩包内为种子文件需使用BT类软件下载;转载自btpig.com。–注:1.本人发资源纯为与诸位共享,发布前均做过测试保证可用。2.如果下载后打不开请重新再试,可能是网络传输问题。3.如有疑问请访问【http://blog.csdn.net/soft

    2022年4月28日
    49
  • vscode创建html文件夹_vscode怎么新建js文件

    vscode创建html文件夹_vscode怎么新建js文件vscode:创建html文件一.创建html文件:1.创建后缀名为.html文档2.创建html的文档结构

    2022年8月22日
    7
  • ISO IEC 27001 企业信息安全管理要求[通俗易懂]

    ISO IEC 27001 企业信息安全管理要求[通俗易懂]ISO27001和ISO20000认证已经成为企业核心竞争力的重要标志。ISOIEC270012013中文版-制造文档类资源-CSDN下载ISOIEC270012013中文版更多下载资源、学习资料请访问CSDN下载频道.https://download.csdn.net/download/std86021/83901501?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164816627616780255250066%2522%252C%

    2025年6月6日
    3
  • pycharm2021专业版激活码【最新永久激活】

    (pycharm2021专业版激活码)最近有小伙伴私信我,问我这边有没有免费的intellijIdea的激活码,然后我将全栈君台教程分享给他了。激活成功之后他一直表示感谢,哈哈~https://javaforall.net/100143.htmlIntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,上面是详细链接哦~1STL…

    2022年3月27日
    1.1K
  • C语言指针的初始化和赋值

    1、指针的初始化指针初始化时,“=”的右操作数必须为内存中数据的地址,不可以是变量,也不可以直接用整型地址值(但是int*p=0;除外,该语句表示指针为空)。此时,*p只是表示定义的是个指针变量,并没有间接取值的意思。例如:inta=25;int*ptr=&a;intb[10];int*point=b;  int*p=&b[0];

    2022年4月18日
    57

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号