Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Python 递归函数

Python 递归函数

作者头像
全栈程序员站长
发布于 2022-09-07 05:38:43
发布于 2022-09-07 05:38:43
1.8K0
举报

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

递归函数

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

递归函数特性:

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

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

代码语言:javascript
AI代码解释
复制
# 循环方式 
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

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

一般递归

代码语言:javascript
AI代码解释
复制
def normal_recursion(n):
    if n == 1:
        return 1
    else:
        return n + normal_recursion(n-1)

执行:

代码语言:javascript
AI代码解释
复制
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

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

代码语言:javascript
AI代码解释
复制
def tail_recursion(n, total=0):
    if n == 0:
        return total
    else:
        return tail_recursion(n-1, total+n)

执行:

代码语言:javascript
AI代码解释
复制
tail_recursion(5)
tail_recursion(4, 5)
tail_recursion(3, 9)
tail_recursion(2, 12)
tail_recursion(1, 14)
tail_recursion(0, 15)
15

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

深入理解尾递归

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

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

代码语言:javascript
AI代码解释
复制
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语法)

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

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

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

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

存在的问题

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

一个牛人想出的解决办法

实现一个 tail_call_optimized 装饰器

代码语言:javascript
AI代码解释
复制
#!/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/>

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

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

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

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

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

代码语言:javascript
AI代码解释
复制
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 # 调用者的帧

补充

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

代码语言:javascript
AI代码解释
复制
#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.cn/147628.html原文链接:https://javaforall.cn

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年6月2,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Python学习——尾递归及装饰器优化
尾递归与一般的递归不同在于对内存的占用:普通递归创建stack累积而后计算收缩,尾递归只会占用恒量的内存。
Sparkle^
2022/03/26
1.1K0
经典动态规划问题 -- 青蛙上台阶与 python 的递归优化
一大早,前同事在微信上给出了个题: 一只青蛙上台阶,一次只能上一个或两个台阶,如果总共有3个台阶,那么有三种上法:
用户3147702
2022/06/27
8350
经典动态规划问题 -- 青蛙上台阶与 python 的递归优化
Python中的尾递归
尾递归的原理:当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
K同学啊
2019/01/22
1.6K0
Python入门之三元表达式\列表推导式\生成器表达式\递归匿名函数\内置函数
本章目录:     一、三元表达式、列表推导式、生成器表达式     二、递归调用和二分法     三、匿名函数     四、内置函数 ================================
Jetpropelledsnake21
2018/05/02
1.3K0
Python入门之三元表达式\列表推导式\生成器表达式\递归匿名函数\内置函数
Python 中的递归,你真的懂了吗?
出现的效果就是,这个函数在不断的调用自己,每次调用就n+1,相当于循环了。  可是为何执行了900多次就出错了呢?还说超过了最大递归深度限制,为什么要限制呢?
用户7886150
2021/01/21
8360
Python学习 Day 4 函数 切片 迭代 列表生成式 生成器
TypeError: my_abs() takes exactly 1argument (2 given)#参数个数不对
Ai学习的老章
2019/04/10
4890
11月6日排序函数,匿名函数,回调函数,递归函数, zip函数
##### 排序sort, sorted的区别: list.sort(func=None, key=None, reverse=False(or True)) 对于reverse这个bool类型参数,当reverse=False时:为正向排序;当reverse=True时:为方向排序。默认为False。 执行完后会改变原来的list,如果你不需要原来的list,这种效率稍微高点 >>> list = [2,8,4,6,9,1,3] >>> list.sort() >>> list [1, 2, 3, 4, 6, 8, 9]
武军超
2019/02/25
1.1K0
Python递归函数
https://www.python-course.eu/recursive_functions.php
py3study
2020/01/20
1.5K0
Python中的栈溢出及解决办法
1.递归函数 在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。 举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n,用函数fact(n)表示,可以看出: fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n 所以,fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。 于是,fact(n)用递归的方式写出来就是: def fact(n):
用户1214487
2018/03/28
1.7K0
漫谈递归转非递归
一:递归的思想       之前面试腾讯,面试官问了一个问题:说说递归和循环的区别?当时没有答出问题的本质,只是简单地解释了这两个词的意思,囧,今天就借由这篇文章来谈谈自己对递归的理解。       我们一般对递归的印象就是一个函数反复的“自己调用自己”,代码精炼,便于阅读。但是,从本质上来说,递归并不是简单的自己调用自己,而是一种分析和解决问题的方法和思想。简单来说,递归思想就是:把问题分解成规模更小,但和原问题有着相同解法的问题。典型的问题有汉诺塔问题,斐波那契数列,二分查找问题,快速排序问题等。PS:
Linux云计算网络
2018/01/11
2.1K0
漫谈递归转非递归
一篇文章带你了解Python递归函数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
Python进阶者
2021/04/19
6530
一篇文章带你了解Python递归函数
3 Python 基础: Python函数及递归函数知识点梳理
函数的英文是function,所以,通俗地来讲,函数就是功能的意思。函数是用来封装特定功能的,比如,在Python里面,len()是一个函数,len()这个函数实现的功能是返回一个字符串的长度,所以说len()这个函数他的特定功能就是返回长度,再比如,我们可以自己定义一个函数,然后编写这个函数的功能,之后要使用的时候再调用这个函数。所以函数分为两种类型,一种是系统自带的不用我们编写其功能系统自己就有的,比如len()这种函数,另一种函数是我们自定义的,需要我们编写其功能的,这种函数自由度高,叫做自定义函数,需要使用的时候直接调用该函数。
野原测试开发
2019/07/24
1.2K0
3 Python 基础: Python函数及递归函数知识点梳理
python基础——匿名函数及递归函数
匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。
菲宇
2019/06/13
5460
python基础——匿名函数及递归函数
Python之递归函数
Python之递归函数 好久没有更新内容了,也好久没有给大家打个招呼了,小白想死你们了。今天跟大家说说Python中的递归函数。 Python是支持递归函数的。简单地说,一个递归函数就是直接或间接地调用自身的函数,并且要有退出条件。枯燥的概念令人生厌,我们直接来个例子看看递归函数是如何工作的。 例如我们对一个数字列表进行求和计算,我们可以使用内置的sum函数或者自己写一个函数来完成计算工作,接下来我们看看如何使用递归来完成求和运算: In[1]: def mysum(L): ...: if no
1846122963
2018/03/09
1.2K0
Python栈溢出
python3.5.4 递归函数最恶心的时候莫非栈溢出(Stack overflow)。
py3study
2020/01/13
1.9K0
3 Python 基础: Python函数及递归函数知识点梳理
函数的英文是function,所以,通俗地来讲,函数就是功能的意思。函数是用来封装特定功能的,比如,在Python里面,len()是一个函数,len()这个函数实现的功能是返回一个字符串的长度,所以说len()这个函数他的特定功能就是返回长度,再比如,我们可以自己定义一个函数,然后编写这个函数的功能,之后要使用的时候再调用这个函数。所以函数分为两种类型,一种是系统自带的不用我们编写其功能系统自己就有的,比如len()这种函数,另一种函数是我们自定义的,需要我们编写其功能的,这种函数自由度高,叫做自定义函数,需要使用的时候直接调用该函数。
野原测试开发
2019/09/10
7940
3 Python 基础: Python函数及递归函数知识点梳理
朋友你听说过尾递归吗
本文介绍了尾递归和尾调用优化,尾递归是指在函数尾递归调用时不会创建新的调用帧,而是直接在原调用帧上进行递归。尾调用优化是指函数在调用时不会创建新的调用帧,而是直接在原调用帧上进行调用。这种优化可以节省内存空间和提高程序的运行速度。
IMWeb前端团队
2017/12/29
1.3K0
朋友你听说过尾递归吗
学习Javascript之尾调用
总括: 本文介绍了尾调用,尾递归的概念,结合实例解释了什么是尾调用优化,并阐述了尾调用优化如今的现状。
Damonare
2020/02/23
1.4K0
学习Javascript之尾调用
7-函数
当存在多个默认参数的时候,调用的时候,既可以按顺序提供默认参数,比如调用enroll('Bob', 'M', 7),意思是,除了name,gender这两个参数外,最后1个参数应用在参数age上,city参数由于没有提供,仍然使用默认值。
用户3106371
2019/03/11
8540
7-函数
递归函数实现 HelloWorld 的详细推理及实际示例
实现 HelloWorld 通常是每一个程序员学习编程语言的第一步,但在这里,我们要挑战一种非常规的方法,那就是使用递归函数来完成。递归,顾名思义,就是一个函数不断地调用自身的过程,直到达成某种基准条件。那么如何用这种自调用的方式,实现一个看似简单的 HelloWorld 输出呢?这需要从递归的核心概念、递归的基准条件、实际的递归函数编写,以及其中可能涉及到的一些底层原理来一步步详细解释。
编程小妖女
2024/12/17
2760
递归函数实现 HelloWorld 的详细推理及实际示例
相关推荐
Python学习——尾递归及装饰器优化
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档