AICon议程上新60%,阿里国际、360智脑、科大讯飞、蔚来汽车分享大模型探索与实践 了解详情
写点什么

函数的递归

  • 2020-05-25
  • 本文字数:4231 字

    阅读完需:约 14 分钟

函数的递归

本文节选自张玉宏著的《Python 极简讲义:一本书入门数据分析与机器学习》,由电子工业出版社授权分享。


调用通常发生在彼此不同的函数之间。其实,函数还有一种特殊的调用方式,那就是自己调用自己,这种方式称为函数递归调用。递归,在程序设计中也是一个常用的技巧,甚至是一种思维方式,非常值得我们掌握。

感性认识递归

在讲解“递归”这个抽象概念之前,让我们来重温一下昔日往事。小时候,当我们在缠着长辈讲故事时,长辈们可能就用下面的故事来“忽悠”我们:从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事!故事是什么呢?从前有座山,山里有座庙,庙里有个老和尚正在给小和尚讲故事!故事是什么呢……


除非讲故事的人自己停下来不讲了,不然这个故事可以“无限”讲下去,原因就是“故事”嵌套的“故事”就是“故事”本身,这就是语言上“递归”的例子。


但是,由于这个故事并没有一个终止的条件,因此,它实际上是陷入了一种有头无尾的死循环,因此并不符合程序设计领域中定义的“递归”。在程序设计领域,递归是指函数(或方法)直接或间接调用自身的一种操作,如下图所示。递归调用的好处在于,它能够大大减少代码量,将原本复杂的问题简化成一个简单的基础操作来完成。在编写程序的过程中,“递归调用”是一个非常实用的技巧。



递归示意图


从上图中可以看出,函数不论是直接调用自身,还是间接调用自身,都是一种无终止的过程。


在程序设计中,显然不能出现这种无终止的调用。因此,在编写递归算法时,读者要特别注意,所有递归一定要有终止条件,这又被称作递归出口。如果一个递归函数缺少递归出口,执行时就会陷入死循环。递归出口通常可用 if 语句来设置,在满足某种条件时不再继续,调用某个值,结束递归。


谷歌公司有世界上最聪明的程序员。他们不光聪明,还很有自己的“冷幽默”,别出心裁。比如说,假设你不懂得什么是“递归”,不妨去谷歌搜索一下这个关键词。然后你会发现,除了给出必要的搜索结果,谷歌还给出了一条提示语“您是不是要找:递归”,如下图所示。



谷歌程序员的“冷幽默”


乍一看,你可能会觉得,这谷歌搜索是不是有问题啊?我的确、明明、丝毫无误地查询的就是“递归”,还提示什么啊?其实,这正是谷歌搜索引擎背后程序员们的“冷幽默”所在:如果你点击了那个提示“递归”,搜索引擎将再次搜索“递归”——相当于自己调用自己——这不正是递归的精髓吗?


或许你懂了,会心一笑,但可能还会疑惑:这也不对啊,所有的递归都有终止条件,如果我们一直点击这个提示词“递归”,查询岂不是会无限循环下去?


放心,你一定不会一直点击下去。因为这个递归的出口正是,查询的人终于懂得什么是递归而不再查询。而你就是那个懂得的人。

思维与递归思维

递归(recurse)在计算机领域被广泛应用,它不仅是一种计算方法,更是一种思维方式。科技作家吴军博士认为:递归思维是人与计算机思维最大的差别之一。著名计算机科学家彼得·多伊奇(L. Peter Deutsch)甚至认为,To iterate is human, to recurse divine(迭代是人,递归是神)。


对于计算机从业者来说,想成为顶级人才,在做计算机相关工作时,必须具有递归思维。对于普通人来讲,这种思维方式也很有启发。因此,不论从哪个角度,递归思维都值得我们培养和掌握。


人的常规思维被称为递推(iterate)思维。在中文里,“递推”和“递归”只有一字之差,但在英文世界里,它们的差别可大了去了,可谓“差之毫厘,谬以千里”。


我们先来说说递推。比如小时候我们学习数数,从 1、2、3 一直数到 100,就是典型的递推。类似地,我们在学习过程中循序渐进,如水到而渠成,出发点都是正向的,由易到难,由小到大,由局部到整体。


递推是人类本能的正向思维,于我们而言,可谓熟稔于心。而“递归”则有一定的反常识。


下面我们以计算一个整数的阶乘为例来说明两种思维的差别。如果用人类常用递推方式计算一个整数的阶乘,比如 5!=1×2×3×4×5,那么做法是从小到大一个数一个数接连相乘。如果计算 10 的阶乘(10!),过程也是类似的,即从 1 乘到 10。在生活中,这种做法不仅合情合理,而且浑然天成。事实上,在中学里学的数学归纳法(利用当成立时的结论,推导)就是递推方法。


为了简单起见,我们还是用前面求阶乘的简单例子来说明递归的原理。计算机是怎么计算阶乘的呢?它是倒着来的。比如要算 5!,计算机就把它变成 5×4!(即 5 乘以 4 的阶乘)。当然,我们可能会质疑,4!还不知道呢!但没有关系,计算机会采用同样的方法,把 4!变成 4×3!。至于 3!,则用同样的算法处理。最后做到 1!时,计算机知道 1!=1(这就是递归的终止条件),自此便不再往下扩展了。


接下来,就是倒推回所有的结果。因为知道了 1!,顺水推舟,就知道了 2!,然后可知 3!、4!和 5!。从上面描述的递归过程可以看出,递归的方法论可归结为两步:先从上向下层层展开,再从下到上一步步回溯。

递归调用的函数

你可能会问,计算机为何要这么算?这么算有何优势?答案并不复杂,利用递归可以使算法的逻辑变得非常简单。因为递归过程的每一步用的都是同一个算法,计算机只需要自顶向下不断重复即可。


具体到阶乘的计算,无非就是某个数字的阶乘,变成这个数乘以的阶乘。因此,递归的法则就两条:一是自顶而下(从目标直接出发),二是不断重复。


递归的另一个特点在于,它只关心自己下一层的细节,而并不关心更下层的细节。你可以理解为,递归的简单源自它只关注“当下”,把握“小趋势”,虽然每一步都简单,但一直追寻下去,也能获得自己独特的精彩。


下面我们就以计算阶乘为例,分别使用递推和递归方式实现,大家可体会二者的区别。


【范例】利用递推和递归方式分别计算


01   #用正向递推的方式计算阶乘02   def iterative_fact( n ):  03       fact = 104       for i in range(1, n + 1):05           fact *= i06       return fact07     08   #用逆向递归的方式计算阶乘09   def recursive_fact( n ):10       if n <= 1 :11           return n;12       return n * recursive_fact(n - 1)13     14   #调用递推方法计算15   num = 516   result = iterative_fact( num );17   print("递推方法:{}!= {}".format(num, result))18   #调用递归方法计算19   result = recursive_fact(num)20   print("递归方法:{}!= {}".format(num, result))
复制代码


【运行结果】


递推方法:5!= 120递归方法:5!= 120
复制代码


递归函数的优点在于,定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但正向递推(即循环)的逻辑不如逆向递归的逻辑清晰。


需要注意的是,虽然递归有许多的优点,但缺点也很明显。那就是,使用递归方式需要函数做大量的压栈和弹栈操作,由于压栈和弹栈涉及函数执行上下文(context)的现场保存和现场恢复,所以程序的运行速度比不用递归实现要慢。


此外,大量的堆栈操作消耗的内存资源要比非递归调用多。而且,过深的递归调用还可能会导致堆栈溢出。如果操作不慎,还容易出现死循环。因此读者编写代码时需要多加注意,一定要设置递归操作的终止条件。

谷歌公司关于递归的面试题

有这么一个游戏:有两个人,第一个人先从 1 和 2 中挑一个数字,第二个人可以在对方的基础上选择加 1 或者加 2,然后又轮到第一个人,他也可以选择加 1 或者加 2,之后再把选择权交给对方,就这样双方交替地选择加 1 或者加 2,谁先加到 20,谁就赢了。对于这个游戏,你用什么策略保证一定能赢?


【案例分析】


如果用正向的递推思维(比如说穷举法),并不容易想清楚,而且还容易漏掉合理的解。但如果用逆向的递归思维,问题的解就非常容易推导出来。我们先从结果出发,如果要想抢到 20,就需要抢到 17,因为抢到了 17,无论对方是加 1 还是加 2,你都可以加到 20。而要想抢到 17,就要抢到 14,以此类推,就必须抢到 11、8、5 和 2。


因此对于这道题,只要第一个人抢到了 2,他就赢定了。这是因为,无论对方选择加 1 还是加 2,他都可以让这一轮两个人加起来的数值等于 5。同样的道理,在当前和为 5 的基础上,无论对方选择加 1 或加 2,他都能让和向着 8 进发。以此类推,整个过程都被他牢牢控制,最终的数列之和,毫无悬念地被他锁定在 20。


当然谷歌的面试题并非这么简单,如果你答对第一道题,那么紧接着就会有下一道题。


按照上述方法,在不考虑谁输谁赢的情况下,从开始(以 1 或 2 为起点)加到 20,有多少种不同的递加过程?比如 1,4,7,10,12,15,18,20 算一种;2,5,8,11,14,17,20 又是一种。那么一共会有多少种这样的过程呢?


【案例分析】


这道题显然并不简单,通过正向的穷举法很难完备遍历。解这道题的技巧还是要使用递归。我们假定数到 20 有种不同的路径,那么到达 20 这个数字,前一步只有两个可能的情况,即从 18 直接跳到 20,或者从 19 数到 20。


由于从 18 跳到 20 和从 19 到 20 是不同的,因此达到 20 的路径数量,其实就是达到 18 的路径数量,加上达到 19 的路径数量,也就是说,。类似地,。这就是递推公式。


最后,只有一个可能,就是 1,有两个可能,要么直接跳到 2,要么从 1 达到 2。知道了,就可以知道。知道,就可以知道,因为,以此类推,一直到即可。


聪慧如你,你一定看出来了,这就是著名的斐波那契数列,如果我们认为也等于 1,那么这个数列就长成这样:这个数列几乎按照几何级数的速度增长,到了,就已经是 10946 了。因此,仅仅靠正向的穷举法,基本上是不可能把所有情况都列举出来的。


上述面试题来自曾就职于谷歌公司的吴军博士。吴军博士在分析这道面试题时指出,在数学和计算机上,等价性原则是一个非常重要的原则。很多问题的表象看起来纷繁复杂,但抽丝剥茧之后,其本质是等价的。比如说,如果一个楼梯有 20 阶,你每次可以爬一阶歇一会,也可以爬两阶歇一会,爬到 20 阶一共有多少种歇息法?这个问题的解,其实和“谁先抢到 20”是一样的,也是一个斐波那契数列。


从某种程度上来看,递归思维是一种以结果为导向,反向追寻,直到追寻到原点(递归的终止条件)的思维方式,一旦原点问题得以解决,其后的问题都会迎刃而解。


图书简介


https://u.jd.com/9WncFq


2020-05-25 10:001916

评论

发布
暂无评论
发现更多内容

SD-WAN解决电商企业海外业务网络难题

Ogcloud

SD-WAN 企业网络 SD-WAN组网 SD-WAN服务商 SDWAN

中科院院士:借鉴美国超级计算机安腾 探索我国技术新路径

Geek_2d6073

国际标准图查询语言 GQL 正式发布,悦数图数据库业界首家原生支持

最新动态

FotoJet Designer(图形设计软件)特别版下载

iMac小白

架构设计|基于 raft-listener 实现实时同步的主备集群

快乐非自愿限量之名

Java 架构 运维

win版DxO FilmPack 7(照片模拟胶片效果处理)v7.6.0.515 (x64)特别版

iMac小白

DxO FilmPack 7 DxO FilmPack 7下载 DxO FilmPack 7破解版

利用SD-WAN技术优化企业网络

Ogcloud

网络 SD-WAN sdn 企业组网 SD-WAN组网

「布道师系列文章」解析 AutoMQ 对象存储中的文件存储格式

AutoMQ

大数据 kafka 云原生 知乎 AutoMQ

库存领域核心能力--库存预占 建设实践

京东科技开发者

微服务架构下如何通过弱依赖原则保障系统高可用

京东科技开发者

去中心化交易所开发 AI策略交易

区块链软件开发推广运营

dapp开发 区块链开发 链游开发 NFT开发 公链开发

ETLCloud中多并行分支运行的设计技巧

RestCloud

ETL 数据集成 多并行分支

Axure RP 9 for mac激活版 交互式产品原型设计工具

iMac小白

Axure RP 9汉化 Axure RP 9授权码 Axure RP 9破解版

App测试中,强制等待和隐式等待谁更强?

霍格沃兹测试开发学社

NL2SQL进阶系列(5):论文解读业界前沿方案(DIN-SQL、C3-SQL、DAIL-SQL)、新一代数据集BIRD-SQL解读

汀丶人工智能

自然语言处理 大模型 NL2SQL

软件测试学习笔记丨显式等待的高级使用

测试人

软件测试 自动化测试 测试开发

Disk Drill for Mac(数据恢复软件) v5.5.1515中文激活版

iMac小白

Disk Drill for Mac Disk Drill下载 Disk Drill mac

剪切板复制粘贴管理工具 Paste for mac中文激活版

iMac小白

Paste for Mac paste mac破解版 Paste 下载

Icecream Screen Recorder Pro(屏幕录像软件)

iMac小白

信创里程碑:Tapdata 同时通过华为云 GaussDB 及鲲鹏兼容互认证,全面支持基础设施自主创新建设

tapdata

华为云 GaussDB 华为云公有云平台(鲲鹏)

NL2SQL实践系列(1):深入解析Prompt工程在text2sql中的应用技巧

汀丶人工智能

大模型 text2sql NL2SQL

函数的递归_AI&大模型_张玉宏_InfoQ精选文章