【python - 函数】

一、递归函数

如果函数体中直接或间接调用了函数本身,则函数称为递归(recursive)函数。也就是说,执行递归函数主体的过程中可能需要再次调用该函数。在 Python 中,递归函数不需要使用任何特殊语法,但它们确实需要一些努力来理解和创建。

我们将从编写一个对自然数的所有数字位求和的函数的样例开始。在设计递归函数时,我们需要找到可以将问题分解为更简单问题的方法。在这个示例中,可以使用运算符 % 和 // 将数字分为两部分:最后一位和除最后一位以外的所有数字。

>>> 18117 % 10
7
>>> 18117 // 10
1811

18117 的数字位之和是 1+8+1+1+7=18 。正如同我们可以拆分数字一样,我们可以将这个和分成最后一位数字 7 和除最后一位数字之外的所有数字的和 1+8+1+1=11 。这种拆分为我们提供了一种算法:要对数字 n 的数字位求和,就将其最后一位数字 n % 10 与 n // 10 的所有数字位之和相加。其中有一种特殊情况:如果数字只有一位,那么它的数字位之和就是它本身。该算法可以使用递归函数来实现。

>>> def sum_digits(n):
        """返回正整数 n 的所有数字位之和"""
        if n < 10:
            return n
        else:
            all_but_last, last = n // 10, n % 10
            return sum_digits(all_but_last) + last

这个 sum_digits 函数的定义是完整且准确的。虽然 sum_digits 函数在自己的函数体内被调用,但数字求和问题已被细分为两个步骤:先求出除最后一个数字外的所有数字总和,再加上最后一个数字的值。这两个步骤比原问题都更简单。由于第一个步骤与原问题相同,所以该函数被称为递归函数。也就是说,sum_digits 函数本身就是我们实现 sum_digits 所需要的函数。

>>> sum_digits(9)
9
>>> sum_digits(18117)
18
>>> sum_digits(9437184)
36
>>> sum_digits(11408855402054064613470328848384)
126

我们可以使用我们的计算环境模型来精确理解这个递归函数如何成功应用的,而且不需要添加新的规则。 

 

当执行 def 语句时,名称 sum_digits 被绑定到一个新函数,但该函数的主体尚未执行。因此,sum_digits 的循环特性(circular nature)暂时还不是一个问题。然后,sum_digits 被传入参数 738:

  1. 创建一个局部帧,将 n 绑定到 738,并在该帧作为起点的环境中执行 sum_digits 的函数体。
  2. 由于 738 不小于 10,会执行第 4 行的赋值语句,将 738 分为 73 和 8。
  3. 在下面的返回语句中,会以当前环境中 all_but_last 的值 73 调用 sum_digits
  4. 创建另一个将 n 绑定到 73 的局部帧,并在该帧作为起点的环境中再次执行 sum_digits 的函数体。
  5. 由于 73 也不小于 10,将 73 分为 7 和 3,并以 7 调用 sum_digits,即 all_but_last 在此帧中的值。
  6. 创建第三个局部帧,其中将 n 绑定到 7。
  7. 在从这个帧开始的环境中,表达式 n < 10 为真,因此返回 7。
  8. 在第二个局部帧中,将这个返回值 7 与 last 的值 3 相加,返回 10。
  9. 在第一个局部帧中,将这个返回值 10 与 last 的值 8 相加,返回 18。

尽管这个递归函数具有循环特性,但它使用了不同的参数正确地应用了两次。此外,第二次应用此程序的对象是一个比第一次更简单的数字求和问题。生成调用 sum_digits(18117) 的环境图,可以看到每次连续的 sum_digits 的调用都使用了比上次更小的参数,直到最后得到个位数的输入。

这个例子还说明了具有简单函数体的函数可以通过使用递归演变成具有复杂计算过程的函数。

二、递归函数剖析

许多递归函数的函数体中存在着一种常见的模式。函数体会以一个基线条件(base case)开始(这是一种条件语句),它为最容易处理的输入定义了函数的行为。对于 sum_digits 函数而言,基线条件是接收到任意一位数的参数,我们只需返回该参数。有些递归函数会有多个基线条件。

然后,在基线条件之后,会有一个或多个递归调用。递归调用总是有一个特点:它们简化了原始问题。递归函数通过逐步简化问题来表达计算。例如,对 7 的数字求和比对 73 的数字求和更简单,而对 73 求和又比对 738 更简单。对于每个后续调用,剩余的计算量都会减少。

递归函数解决问题的方法通常不同于我们之前使用的迭代方法。思考一个计算 n 的阶乘的函数 fact,其中 fact(4) 会计算为 4!=4⋅3⋅2⋅1=24 。

一种自然的实现方式是使用 while 语句将每个小于等于 n 的正整数都相乘得到结果。

>>> def fact_iter(n):
        total, k = 1, 1
        while k <= n:
            total, k = total * k, k + 1
        return total

>>> fact_iter(4)
24

另一方面,阶乘的递归实现可以用 fact(n-1) 来表达 fact(n),就是一个更简单的问题了。这个递归的基线条件是问题最简单的形式:fact(1) 是 1。

这两个阶乘函数在概念上有所不同。迭代函数通过在每一项中连续相乘,来构造从基线条件 1 到最终总数 n 的结果。另一方面,递归函数直接从最终项 n 和更简单的问题 fact(n-1) 的结果来构造出最终结果。

递归会通过 fact 函数的连续应用,逐步“展开 unwinds”为越来越简单的问题实例,最后从基线条件开始构造出结果。它通过将参数 1 传递给 fact 来结束递归;每次调用的结果取决于下一次调用,直到达到基线条件。

从阶乘的标准数学函数定义中,很容易验证这个递归函数的正确性:

(𝑛−1)!=(𝑛−1)⋅(𝑛−2)⋯⋯1𝑛!=𝑛⋅(𝑛−1)⋅(𝑛−2)⋯⋯1𝑛!=𝑛⋅(𝑛−1)!

虽然我们可以使用计算模型来展开递归,但将递归调用(recursive calls)视为函数抽象会更容易理解一点。也就是说,我们不用在意 fact(n-1) 在 fact 的函数体中是怎么实现的;我们只需要相信它能计算 n-1 的阶乘就好了。将递归调用看作一种函数抽象这一思想,就是所谓“递归的信仰之跃(recursive leap of faith)”。我们根据函数自身来定义一个函数,但在验证函数的正确性时,我们只需相信在更简单的情况下,函数同样能正确工作。在这个示例中,我们假设 fact(n-1) 能够正确计算 (𝑛−1)! ;如果假设成立,我们只需要检查 𝑛! 是否被正确计算即可。这样,验证递归函数的正确性实际上变成了一种归纳法(induction)的证明形式。

函数 fact_iter 和 fact 也有所不同,因为前者必须引入两个额外的名称 total 和 k,这在递归实现中是不需要的。一般来说,迭代函数必须在计算过程中维护一些会变化的局部状态。在迭代中的任何时刻,该状态都可以表示已完成计算的结果和剩余的待计算的量。例如,当 k = 3total = 2 时,仍然有两项需要处理,即 3 和 4。另一方面,fact 的特征是它的单一参数 n。计算的状态完全嵌入在环境的结构中,它的返回值扮演 total 的角色,并将 n 绑定到不同帧中的不同值,而不是显式地跟踪 k

递归函数利用调用表达式求值的规则将名称绑定到值,通常避免了在迭代期间正确分配局部名称的麻烦。由于这个原因,我们可能更容易正确地定义递归函数。但是,学着识别由递归函数演化而来的计算过程需要一定的实践练习。

三、互递归

当一个递归过程被划分到两个相互调用的函数中时,这两个函数被称为是互递归的(mutually recursive)。例如,思考以下非负整数的偶数和奇数定义:

  • 如果一个数比一个奇数大 1,那它就是偶数
  • 如果一个数比一个偶数大 1,那它就是奇数
  • 0 是偶数

使用这个定义,我们可以实现一个互递归函数来确定一个数字是偶数还是奇数:

通过打破两个函数之间的抽象边界,可以将互递归函数转换为单个递归函数。在这个例子中,可以将 is_odd 的函数体合并到 is_even 的函数体中,确保将 is_odd 函数体中的 n 替换为 n-1 以反映传递给它的参数:

>>> def is_even(n):
        if n == 0:
            return True
        else:
            if (n-1) == 0:
                return False
            else:
                return is_even((n-1)-1)

因此,互递归并不比简单递归更神秘或更强大,它只是提供了一种在复杂递归程序中维护抽象的机制。

The Luhn Algorithm(模10算法):用于验证信用卡号码.

①从最右边的数字(即校验位)开始,向左移动,每第二位数字的值加倍;如果此加倍运算的乘积大于 9(例如,7 *2=14),则乘积的数字相和(例如,10:1 + 0 = 1,14:1 + 4 = 5)。

②取所有位数的总和。

③有效信用卡号码的Luhn和是10的倍数。

我们可以写出这个算法的实现,函数和函数之间互相影响和存在对方,这就是相互递归。

接下来我们写一个上面函数"sum_digits"的迭代实现:

它们的结果是一样的。

四、递归函数中的打印 

通过对 print 函数的调用,递归函数的计算过程通常可以可视化。作为示例,我们将实现一个 cascade 函数,该函数按从大到小再到大的顺序,打印一个数字的所有前缀。

>>> def cascade(n):
        """打印数字 n 的前缀的级联"""
        if n < 10:
            print(n)
        else:
            print(n)
            cascade(n//10)
            print(n)

>>> cascade(2013)
2013
201
20
2
20
201
2013

在这个递归函数中,基线条件是打印出来个位数。否则,就在两个 print 调用之间使用递归调用。

在递归调用之前写出基线条件表达式并不是一个严格的要求。实际上,通过观察可以看到 print(n) 在条件语句的两个子句中重复出现,我们可以将其前置从而更简洁地表达这个函数。

>>> def cascade(n):
        """Print a cascade of prefixes of n."""
        print(n)
        if n >= 10:
            cascade(n//10)
            print(n)

我们可以编写一个打印反向级联的函数:

实现逆级联就是使用增长函数,数字先增长然后打印,就得到从小到大的数字,然后再将其先收缩再打印。

作为另一个互递归的例子,请思考一个两人博弈的情景,桌子上最初有 n 个石子,玩家轮流从桌面上拿走一个或两个石子,拿走最后一个石子的玩家获胜。假设 Alice 和 Bob 在玩这个游戏,两个人都使用一个简单的策略:

  • Alice 总是取走一个石子
  • 如果桌子上有偶数个石子,Bob 就拿走两个石子,否则就拿走一个石子

给定 n 个初始石子且 Alice 先开始拿,谁会赢得游戏?

该问题的一个自然分解是将每个策略封装在其自己的函数中。这使我们可以修改一个策略而不会影响其他策略,保持两者之间的抽象界限(abstraction barrier)。为了融入游戏的回合制性质,这两个函数在每个回合结束时互相调用。

>>> def play_alice(n):
        if n == 0:
            print("Bob wins!")
        else:
            play_bob(n-1)

>>> def play_bob(n):
        if n == 0:
            print("Alice wins!")
        elif is_even(n):
            play_alice(n-2)
        else:
            play_alice(n-1)

>>> play_alice(20)
Bob wins!

在函数 play_bob 中,我们看到多个递归调用可能会出现一个函数体中。虽然在这个例子中,每次调用 play_bob 最多只会调用一次 play_alice

五、树递归 

另一种常见的计算模式称为树递归(tree recursion),在这种模式中,函数会多次调用自己。例如计算斐波那契数列,其中的每个数都是前两个数的和。

相对于我们之前的尝试,这个递归定义非常吸引人:它完全反映了我们熟悉的斐波那契数的定义。具有多个递归调用的函数称为树递归,因为每个调用都会分成多个较小的调用,每个较小的调用又会分成更小的调用,就像树枝从树干伸出一样,变得更小但数量更多。

其实这并不是有效的计算斐波那契数列的有效方法,因为这个递归树里面有大量的重复值计算。 

def move_disk(disk_number , from_peg , to_peg):
    print("Move dist " + str(disk_number) + " from peg " \
          + str(from_peg) + " to peg " + str(to_peg) + ".")

def slove_hanoi(n , start_peg , end_peg):
    if n == 1:
        move_disk(n , start_peg , end_peg)
    else:
        spare_peg = 6 - start_peg - end_peg     # spare_peg的含义是:现在我们有三个放盘子的塔针,我们可以用这个式子找到每次空闲的塔针,
                                                # 因为塔针编号依次为1、2、3,故用三者之和减去正在使用的塔针编号和即为空闲
        slove_hanoi(n - 1 , start_peg , spare_peg)  # 递归的汉诺塔将会减少处理的麻烦度,次之则为n - 1,我们需要将上面的n - 1个盘子从开始点移到空闲点,
                                                    # 因为我们最终的目的地是三号针,就要借助中间的针来递归移动
        move_disk(n , start_peg , end_peg)
        slove_hanoi(n - 1,spare_peg , end_peg)  # 将最底下的盘子移动到最终目的地后,就要将n - 1个盘子以相同的方式从空闲处,
                                                # 借助开始针(此时开始针就成为空闲针)移动到最后针

我们可以看到每每增加一个盘,将会使动作增加为2^n - 1次(指数增长)。

六、示例:分割数 

求正整数 n 的分割数,最大部分为 m,即 n 可以分割为不大于 m 的正整数的和,并且按递增顺序排列。例如,使用 4 作为最大数对 6 进行分割的方式有 9 种。

1.  6 = 2 + 4
2.  6 = 1 + 1 + 4
3.  6 = 3 + 3
4.  6 = 1 + 2 + 3
5.  6 = 1 + 1 + 1 + 3
6.  6 = 2 + 2 + 2
7.  6 = 1 + 1 + 2 + 2
8.  6 = 1 + 1 + 1 + 1 + 2
9.  6 = 1 + 1 + 1 + 1 + 1 + 1

我们将定义一个名为 count_partitions(n, m) 的函数,该函数返回使用 m 作为最大部分对 n 进行分割的方式的数量。这个函数有一个使用树递归的简单的解法,它基于以下的观察结果:

使用最大数为 m 的整数分割 n 的方式的数量等于

  1. 使用最大数为 m 的整数分割 n-m 的方式的数量,加上
  2. 使用最大数为 m-1 的整数分割 n 的方式的数量

要理解为什么上面的方法是正确的,我们可以将 n 的所有分割方式分为两组:至少包含一个 m 的和不包含 m 的。此外,第一组中的每次分割都是对 n-m 的分割,然后在最后加上 m。在上面的实例中,前两种拆分包含 4,而其余的不包含。

因此,我们可以递归地将使用最大数为 m 的整数分割 n 的问题转化为两个较简单的问题:① 使用最大数为 m 的整数分割更小的数字 n-m,以及 ② 使用最大数为 m-1 的整数分割 n。

为了实现它,我们需要指定以下的基线情况:

  1. 整数 0 只有一种分割方式
  2. 负整数 n 无法分割,即 0 种方式
  3. 任何大于 0 的正整数 n 使用 0 或更小的部分进行分割的方式数量为 0
>>> def count_partitions(n, m):
        """计算使用最大数 m 的整数分割 n 的方式的数量"""
        if n == 0:
            return 1
        elif n < 0:
            return 0
        elif m == 0:
            return 0
        else:
            return count_partitions(n-m, m) + count_partitions(n, m-1)

>>> count_partitions(6, 4)
9
>>> count_partitions(5, 5)
7
>>> count_partitions(10, 10)
42
>>> count_partitions(15, 15)
176
>>> count_partitions(20, 20)
627

我们可以将树递归函数视为探索不同的可能性。在这种情况下,我们探讨了使用大小为 m 的部分以及不使用这部分的可能性。第一次和第二次递归调用即对应着这些可能性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/727932.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

智慧消防新篇章:可视化数据分析平台引领未来

一、什么是智慧消防可视化数据分析平台&#xff1f; 智慧消防可视化数据分析平台&#xff0c;运用大数据、云计算、物联网等先进技术&#xff0c;将消防信息以直观、易懂的图形化方式展示出来。它不仅能够实时监控消防设备的运行状态&#xff0c;还能对火灾风险进行预测和评估…

大数据助力电商发展||电商API接口接入

伴随互联网尤其是移动互联网的高速发展&#xff0c;电子商务已经成为人们生活中不可或缺的一部分&#xff0c;人们的购物理念和消费模式正在发生颠覆性的转变。基于天然的数据优势&#xff0c;电子商务平台利用大数据计算技术不断实施数据的累积、分析和处理&#xff0c;消费者…

如何设计一个点赞系统

首先我们定义出一个点赞系统需要对外提供哪些接口&#xff1a; 1.用户对特定的消息进行点赞&#xff1b; 2.用户查看自己发布的某条消息点赞数量以及被哪些人赞过&#xff1b; 3.用户查看自己给哪些消息点赞过&#xff1b; 这里假设每条消息都有一个message_id, 每一个用户都…

[17] 使用Opencv_CUDA 进行滤波操作

使用Opencv_CUDA 进行滤波操作 邻域处理操作 > 滤波操作&#xff0c;拒绝或者允许某特定频段通过如果图像某处的灰度级变化缓慢&#xff0c;那么就是低频区域&#xff0c;如果灰度级变化剧烈&#xff0c;就是高频区域邻域滤波即卷积操作形态学处理&#xff1a;膨胀&#xf…

【论文通读】VideoGUI: A Benchmark for GUI Automation from Instructional Videos

VideoGUI: A Benchmark for GUI Automation from Instructional Videos 前言AbstractMotivationVideoGUIPipelineEvaluation ExperimentsMain ResultsAnalysis Conclusion 前言 数字智能体的探索又来到了新的阶段&#xff0c;除了常见的桌面工具如PPT&#xff0c;Word&#xf…

HTML(15)——盒子模型

盒子模型组成 内容区域 -width&height内边距-padding &#xff08;出现在内容与盒子边缘之间&#xff09;边框线-border外边距-margin &#xff08;出现在盒子外面&#xff09; div { width: 200px; height: 200px; background-color: rgb(85, 226, 193); padding: 20px; …

【项目实践】Ulike充电牙刷拆解

前言 用了一段时间的充电牙刷&#xff0c;某一次突然没电了&#xff0c;按键也没有反应。无奈只能废弃。最近略微得了些空闲&#xff0c;想着把它拆解看看里面的结构和电路。以下是鼓捣过程记录。 为什么不能直接抽出来&#xff1f; 在网上看到很多拆解视频&#xff0c;都是打开…

基于Windows API DialogBox的对话框

在C中&#xff0c;DialogBox函数是Windows API的一部分&#xff0c;它用于在Win32应用程序中创建并显示一个模态对话框。DialogBox函数是USER32.DLL中的一个导出函数&#xff0c;因此你需要在你的C Win32应用程序中链接到这个库。 #include "framework.h" #include …

修改a-menu菜单图标icon

1.通过触摸元素可知 这个箭头icon其实是通过::before和::after伪元素组合写出来的 2.模仿使用伪元素书写 同理&#xff0c;我们也使用伪元素写icon即可 ::v-deep .ant-menu{// 折叠.ant-menu-submenu-inline > .ant-menu-submenu-title .ant-menu-submenu-arrow::after{wi…

五十一、openlayers官网示例Layer Min/Max Resolution解析——设置图层最大分辨率,超过最大值换另一个图层显示

使用minResolution、maxResolution分辨率来设置图层显示最大分辨率。 <template><div class"box"><h1>Layer Min/Max Resolution</h1><div id"map" class"map"></div></div> </template><…

conda创建虚拟环境报错解决

1.报错截图 2.解决办法 查看当前所有虚拟环境 conda env list 解决办法 解决方法 bash conda config --add channels conda-forge conda config --set channel_priority strict conda config --set channel_priority flexible

20240620每日后端---------Spring Boot中的 5 大设计模式最佳实践和示例 这些是我经常使用的设计模式并且非常喜欢

在本文中&#xff0c;我们将深入探讨五种基本设计模式&#xff0c;并探讨在 Spring Boot 项目中有效应用它们的最佳实践。每个模式都将附有一个实际示例来演示其实现。 单例模式 Singleton 模式确保一个类只有一个实例&#xff0c;并提供对它的全局访问点。这对于管理资源&am…

SpringBoot 实现全局异常处理

为什么要使用全局异常处理&#xff1f; 减少冗余代码&#xff1a; 在不使用全局异常处理器的情况下&#xff0c;项目中各层可能会出现大量的try {…} catch {…} finally {…}代码块&#xff0c;这些代码块不仅冗余&#xff0c;还影响代码的可读性。全局异常处理器允许我们在一…

常说的云VR是什么意思?与传统vr的区别

虚拟现实&#xff08;Virtual Reality&#xff0c;简称VR&#xff09;是一种利用计算机技术模拟产生一个三维空间的虚拟世界&#xff0c;让用户通过视觉、听觉、触觉等感官&#xff0c;获得与现实世界类似或超越的体验。VR技术发展历程可追溯至上世纪&#xff0c;经历概念提出、…

计算机网络实验之单交换机互联终端实验

1.网线 4对&#xff0c;8根&#xff0c;RJ-45连接器&#xff08;水晶头&#xff09;&#xff1b; &#xff08;1&#xff09;直通线 双绞线缆两端按照EIA/TIA568B规格连接水晶头&#xff0c;该双绞线为直通线。 橘白1&#xff0c;橘2&#xff0c;绿白3&#xff0c;蓝4&#…

【vue3|第11期】Vue3中的ref属性:让元素引用变得简单

日期&#xff1a;2024年6月19日 作者&#xff1a;Commas 签名&#xff1a;(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释&#xff1a;如果您觉得有所帮助&#xff0c;帮忙点个赞&#xff0c;也可以关注我&#xff0c;我们一起成长&#xff1b;如果有不对的地方&#xf…

仿真模拟--telnet服务两种认证模式(自作)

自己做的笔记,有问题或看不懂请见解一下~ 目录 两个路由器间实现telnet服务(password认证模式) server client 两个路由器间实现telnet服务(aaa认证模式) server client 改名 tab键补齐 不会就扣问号 ? save 两个路由器间实现telnet服务…

【IDEA】Spring项目build失败

通常因为环境不匹配需要在file->projectstructure里面调整一下。

2024/6/20 驱动day7GPIO子系统

GPIO子系统点六盏灯 #include <linux/init.h> #include <linux/module.h> #include <linux/of.h> #include <linux/gpio.h> #include <linux/of_gpio.h> struct device_node* node; struct device_node* child_node1; struct device_node* child…

嵌入式实验---实验三 定时器实验

一、实验目的 1、掌握STM32F103定时器程序设计流程&#xff1b; 2、熟悉STM32固件库的基本使用。 二、实验原理 1、使用SysTick定时方式控制LED闪烁&#xff1b; 2、使用通用定时器产生PWM脉冲&#xff0c;通过调整占空比实现两个目标&#xff1a; &#xff08;1&#xf…