程序员的数学课15 递归:如何计算汉诺塔问题的移动步数?

递归是重要的程序开发思想,比如程序源代码缩进、树形数据结构、XML 语法、快速排序法等都有递归的影子。

那么,递归思维的本质到底是什么呢?递归的理念看似隐讳,实则非常清晰明了。

为了让你由浅入深地理解它,这一讲我会先从“汉诺塔问题”入手,带你找出“递归思维”,然后将其应用在两个经典问题中,让你感受递归的作用及其缺点。

最后,你便会发现递归与上一讲所学的循环有相似之处,我便会在这两者的对比辨析中,带你探讨它们的本质差异。

汉诺塔问题及其代码实现

我们先来看下汉诺塔问题的规则。

假设有 A、B、C 三根柱子。其中在 A 柱子上,从下往上有 N 个从大到小叠放的盘子。我们的目标是,希望用尽可能少的移动次数,把所有的盘子由 A 柱移动到 C 柱。过程中,每次只能移动一个盘子,且在任何时候,大盘子都不可以在小盘子上面。

图片1.png

1.汉诺塔问题解密

这个题目需要一定的窍门,否则只能碰运气去乱走了。

我们先脑补这样一个画面:假设 A 柱子上除了最后一个大盘子(代号“大盘子”)以外,其他的 N-1 个小盘子都合并起来,成为一个新的盘子(代号为“合并盘”)。那这个问题就简单了,只需要把“合并盘”移动到 B 柱,再把“大盘子”移动到 C 柱,最后把“合并盘”移动到 C 柱。

上述过程如下图所示:

动图GIF.gif

在这个过程中,问题由全部 N 个盘子由 A 移动到 C,转变为 N-1 个“合并盘”从 A 移动到 B 再移动 C。新的问题和原问题是完全一致的,但盘子数量由 N 个减少为 N-1 个。如果继续用上面的思想,就能把 N-1 个“合并盘”再度减少为 N-2 个,直到只剩一个。

我们用数学重写上面的过程:令 H(x) 表示把某个柱子上的全部 x 个盘子移动到另一个柱子上需要的步数,那么原问题 N 个盘子由 A 柱子移动到 C 柱子的数学表示就是 H(N)。

根据我们第一次的分解可知 H(N)=H(N-1)+1+H(N-1)

也就是,把 N 个盘子从 A 移动到 C=把合并盘从 A 移动到 B + 把大盘子从 A 移动到 C + 把合并盘从 B 移动到 C。

再继续分析,你还会得到 H(N-1)=H(N-2)+1+H(N-2)。

……

直到最终 H(2)=H(1)+1+H(1)=1+1+1=3。

我们把这个问题的计算过程整理到下面的表中,并尝试求解 H(n) 的表达式。
图片4.png

因为 H(N)=1+2H(N-1),所以可以得到 H(N-1)=1+2H(N-2),把这两个等式两边分别进行相减,则可以得到 H(N)-H(N-1)=2(H(N-1)-H(N-2))。

令 aN=H(N)-H(N-1),则有 aN=2aN-1,可见 {aN} 是个首项为 1、公比为 2 的等比数列,通项公式为 aN = 2N-1

接着利用这些信息,我们尝试去推导 H(N),则有
图片2.png

别忘了 H(1)=1,a1=1,所以 H(1)=a1,则有
图片3.png
因此如果盘子的数量是 5 个,将 5 代入这个 2N-1,则最少需要 31 步完成移动。

2.汉诺塔问题的代码实现

我们尝试用程序代码来实现汉诺塔问题。不难发现,这里最高频使用的是,把 n 个盘子从某个柱子 x,移动到另一个柱子 z。因此,考虑对这个功能进行函数化的封装,代码如下:

def hanoi(N,x,y,z):if N == 1:print x + '->' + zelse:hanoi(N - 1, x, z, y)print x + '->' + zhanoi(N - 1, y, x, z)

我们对代码进行走读。

第 2、3 行,如果盘子数量为 1,则直接把盘子从 x 柱子移动到 z 柱子即可;若不为 1,则进行第 4~7 行的处理。

此时盘子数量超过了 1,则拆分为“合并盘”和“大盘子”两部分。

  • 首先,函数调用自己,把“合并盘”从 x 移动到 y;

  • 然后,把“大盘子”从 x 移动到 z;

  • 最后,函数再调用自己,把“合并盘”从 y 移动到 z。

想象着会很复杂的代码,实际上非常简单,在主函数中只要执行

hanoi(3, 'a', 'b', 'c')

就能打印出把 3 个盘子从 a 柱子移动到 c 柱子的详细步骤。

每一步的移动结果如下图,执行后需要 7 步,这和我们数学上的计算完全一致。

Drawing 4.png

递归——自己调用自己的程序开发思想

汉诺塔问题解法的核心步骤就是:移动全部盘子,等价于移动“合并盘”,加上移动“大盘子”,加上再移动“合并盘”,然后你需要重复执行这个步骤。

用函数表达这个过程,就是 f(全部盘子) = f(合并盘) + f(大盘子) + f(合并盘)。

为了代码实现这个功能,我们定义这个函数为hanoi(N,x,y,z), 并且在这个函数中,需要调用自己才能完成“合并盘”的移动,这种会调用自己的编码方式在程序开发中,就叫作递归

严格意义来说,递归并不是个算法,它是一种重要的程序开发思想,是某个算法的实现方式。

在使用递归进行程序开发时,需要注意下面两个关键问题。

  • 第一个问题,递归必须要有终止条件,否则程序就会进入不停调用自己的死循环。

有这样一个故事:从前有座山,山里有个庙,庙里有个和尚讲故事;故事是,从前有座山,山里有个庙,庙里有个和尚讲故事;故事是...

这就是一个典型的没有终止条件的递归。在汉诺塔问题中,我们的终止条件,就是当盘子数量为 1 时,直接从 x 移动到 z,而不用再递归调用自身。

  • 第二个问题,写代码之前需要先写出递归公式
    在汉诺塔问题中,递归公式是H(N)=H(N-1)+1+H(N-1),这也是递归函数代码中除了终止条件以外的部分。

对应于“循环结构”中的循环体,这部分代码对于“递归”而言,偶尔也被人称作“递归体”。

递归代码的基本结构如下:

def fun(N,x):if condition(N):xxxelse:fun(N1,x)

我们对这个代码结构进行解析。
对某个函数 fun(N,x) 而言,如果要用递归实现它,代码中至少包括终止条件递归体两部分。

  • 终止条件的判断基于某个入参 N,如果满足,则函数不再调用自己,终止递归;如果还不满足,则进入到递归体。

  • 在递归体中,终止条件判断的入参 N 一定会发生改变。通常而言,是变成比 N 小的一个数值N1。只有这样,递归才能慢慢向终止条件靠近。在递归体中,基于新的参数 N1,再调用函数自身 fun(N1,x),完成一次递归操作。

接着我们带着递归思维,去看一下“阶乘问题”和“斐波那契序列问题”。

递归思维的应用

1.阶乘问题

数学中,阶乘的定义公式为 n!=1×2×...×(n-2)×(n-1)×n。现在请你用递归来写一个函数,输入是某个正整数n,输出是 n 的阶乘。

利用递归写代码时,需要优先处理递归的两个关键问题,那就是终止条件和递归体。

  • 对于终止条件而言,当 n=1 时,返回的值为 1!=1。

  • 对于递归体而言,需要先写出递归公式。根据阶乘公式的定义可知,当 n>1 时,H(n)=n!=1×2×...×(n-2)×(n-1)×n= [1×2×...×(n-2)×(n-1)]×n=n×(n-1)!= n×H(n-1)。

有了这些信息后,我们可以尝试写出下面的代码:

def jiecheng(n):if n == 1:return 1else:return n * jiecheng(n-1)

我们对代码进行走读。这段代码的代码量非常少,第 2、3 行判断 n 是否为 1。如果是,则返回1;否则,则跳转到第 5 行,根据递归公式返回 n×(n-1)!,即 n×jiecheng(n-1)。

题目中限定了输入参数 n 为正整数,所以一些异常判断可以被忽略。但如果你追求代码的工程完备性,还可以补充 n 为 0、n 为负数、甚至 n 为小数的一些异常判断。

在这里,我们就不展开了。

2.斐波那契序列问题

在数学上,斐波那契数列定义为 1、1、2、3、5、8、13、21、34…… 。简而言之,在斐波那契数列中,除了前两项以外,后续的每一项都是前面两项之和,而前两项的值都定义为 1。

我们用 F(n) 表示斐波那契数列中的第 n 项的值,例如:

F(1)=1

F(2)=1

F(3)=1+1=2

F(4)=1+2=3

现在希望你用递归来写代码,实现的功能是,输入某个正整数 n,输出斐波那契数列中第 n 项的值。

你可以假设输入的 n 都是合法的,不用做异常判断。

围绕递归的开发逻辑,关键问题仍然是终止条件和递归体:

  • 斐波那契数列的终止条件很显然,就是当 n 为 1 或 2 时,返回值就是 1;

  • 而它的递归体可以根据斐波那契数列的定义得到,也就是 F(n)=F(n-1)+F(n-2)。

我们把以上定义直接翻译成代码,则有

def fib(n):if n == 1 or n == 2:return 1else:return fib(n-1) + fib(n-2)

我们对代码进行走读:

  • 在第 2 行,判断 n 是否为 1 或 2。

  • 如果是,则第 3 行返回 1;

  • 反之,则跳转到第 5 行,返回前两项之和,即 fib(n-1)+fib(n-2)。

基于这段代码,主函数中执行 print fib(10),即计算斐波那契数列的第 10 位,如下图所示,运行结果为 55。
图片5.png

而我们手动计算斐波那契数列的前 10 位发现,结果也是 55,说明我们刚刚的代码实现是正确的。
图片6.png

递归的优缺点

讲完了递归思维在“阶乘问题”和“斐波那契序列问题”中的应用后,我们总结以下递归的优缺点。

递归有很多优势,例如代码结构简单、代码量少、阅读方便、维护简单等;然而递归也有一些缺陷和不足,一个明显的问题就是,递归的计算量非常大,而且存在重复计算的可能性。

我们以斐波那契数列问题为例,把代码进行如下修改:

def fib(n):if n == 1 or n == 2:return 1else:print "fib: " + str(n-1)print "fib: " + str(n-2)return fib(n-1) + fib(n-2)

其中,在第 5、6 行插入两个打印的动作。它们的功能,是每次执行递归体之前,打印出要递归计算的内容。

这样,在主函数运行 fib(10) 时,你会看到下面的部分运行结果:
Drawing 6.png

很简单,在执行 fib(9) 时,需要递归计算 fib(8) 和 fib(7);而 fib(8) 的计算,又需要递归计算 fib(7) 和 fib(6)。很可惜,在得到 fib(7) 的时候,结果并不会进行保存;而另一边,也要计算 fib(7),这只能再整体进行一次递归计算。

所以,上图中我们能看到计算 fib(10) 的过程中,存在大量重复的递归计算。

重复计算是递归的一个问题,但也并不是绝对会发生,这就需要程序员去综合分析你遇到的具体问题了。

在后面的《17 | 动态规划:如何利用最优子结构解决问题?》我会采用“设置全局变量来缓存中间结果”的方式来避免重复计算,减少计算量。

小结——递归与循环

学完这一讲,你可能会发现,递归和循环比较相像。确实,递归和循环都是通过解决若干个简单问题来解决复杂问题的,它们也都有自己的终止条件和循环体/递归体,都是重复进行某个步骤。

然而,它们也有很多差异性,主要体现在以下两方面。

迭代次数

  • 循环对于迭代的次数更敏感,绝大多数场景会定义一个用来计数的变量 i,来控制循环的次数;

  • 递归对于迭代次数不敏感,取决于什么时候满足终止条件。

问题复杂性

不管是循环还是递归,每一轮迭代处理的问题类型都是非常趋同的,但问题的复杂性却不一样。

  • 对于循环而言,每一轮处理的问题难度几乎是一样的;

  • 递归则是缩小搜索范围(例如二分查找)的思路,一般而言,每轮处理的问题相对上一轮而言是更简单的。

最后,我们留一个练习题:利用递归写出下面的函数,输入是个正整数 n,输出是从 3 到 n 的求和。

下一讲,我将介绍“二分法:如何利用指数爆炸优化程序?”别忘来听课~


精选评论

**柠檬:

static long fun(int n) { if (n == 1) { return 6; } else if (n == 2) { return 5; } else if (n == 3) { return 3; } else { return n + fun(n - 1); }}

    讲师回复:

    递归体的代码逻辑没问题,但终止条件不太对。题目中是3到n的求和,可以假设n为1或2是非法输入。

*峰:

ifa==3elseretur 2n(n-1)+1

    讲师回复:

    不太对。我们需要用递归来实现。需要定一个调用自己的函数。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

程序员的底层思维:逻辑思维

更多关于思维能力的内容,尽在我的新书《程序员必备的思维能力》 “你讲话要有逻辑!” “你这逻辑不对!” “你的底层逻辑是什么?” “说说你的逻辑思维能力体现在哪儿?” 在日常交流中,我们会频繁的使用…

《经典递归问题:汉罗塔》

🌠作者:TheMythWS. 🎆专栏:《JavaSE》 🎇座右铭:不走心的努力都是在敷衍自己,让自己所做的选择,熠熠发光。 目录 ✨汉罗塔的介绍 图解游戏​ ✨N层汉罗塔需移动的次数 ✨汉罗塔的…

用类比方式学习编程中函数递归(个人理解仅供参考)(内含汉诺塔问题的求解)

目录 1.前言 2.递归的数学模型 3.相关的c语法 4.将递归的数学模型写成编程语言 5.利用类比方法将实际问题的代码写成函数递归的形式 例1: 例2: 6.汉诺塔问题的求解 1.前言 本人在学习函数递归编程方法的过程中,发现用类比的方式学习递归法可帮助我们在各种编…

我想,有间花房

你带我走进你的花房,我无法逃脱花的清香,我不知不觉忘记了方向,你说我世上最坚强,我说你世上最善良,你不知不觉和花儿一样 也许每一位 爱花的姑娘,都想有一间属于自己 的花 房 ,在悠闲的午后&am…

花房集团CEO于丹内部信:上市即暴富年代已一去不复返

雷递网 乐天 12月12日 花椒母公司花房集团(股票代码为:“03611”)今日在港交所上市,发行价为2.8港元,募资净额为7240万港元。 花房集团开盘价为3.29港元,较发行价上涨17.5%;截至目前&#xff0c…

花房集团上市:市值超30亿港元 周鸿祎连收两个香港IPO

雷递网 雷建平 12月12日 花椒母公司花房集团(股票代码为:“03611”)今日在港交所上市,发行价为2.8港元,募资净额为7240万港元。 花房集团开盘价为3.29港元,较发行价上涨17.5%;截至目前&#xff…

花房集团上市,走向元宇宙新征程

12月12日,花房集团在港交所成功上市,首日便受到追捧,当日最高涨幅达28.75%。 继360、360数科、鲁大师后,这是“红衣教主”周鸿祎收获的第四个IPO。 花房集团作为直播界元老之一,两年内三次申请IPO,终于在…

花房集团:直播老将终赴IPO

寒冬之下,花房集团如何破局? 12月12日,直播界“元老”花房集团(下称“花房”,03611.HK)正式挂牌港交所。 花房此次IPO发行价为2.8港元/股,募资净额为7240万港元,开盘价为3.29港元/…

如果金融男和IT男同时追你,你选谁?

对于金融女心仪对象排行榜的前两名, 金融男和IT男的地位是不可动摇了, 要说两个行业的不同之处 , 最大的区别在于一个是经济领域, 一个是产业领域 而另一个区别呢? 当然是从事这两种行业的人群不同啦, 具…

会泡妞的程序员都是怎么撩妹子的?

来自:这个好玩吗 链接:https://www.cnblogs.com/lzjtdxfxl/p/5493039.html 传说,每一个程序员上辈子都是折翼的天使 身体好、智商高、逻辑思维能力强 挣得多、花得少、死得还比对方早 王者级的程序员是有情怀的 在他(她&#xff0…

软考必备资料大放送,全科目软考资料都给你备好了!

软考作为IT领域的国家级证书 可以积分落户、评职称、抵个税…含金量高 现在有越来越多的人加入了软考的备考大军 新一轮的软考考试马上要开始了 你准备好了吗? 下面是给大家整理好的软考备考资料包 上面展示的资料只是冰山一角,软考科目众多&#…

【广工考试笔记】计算机网络考试速成笔记

范围 网络协议,服务 网络四层协议的功能与包含的协议 数据链路层 差错检测 ,物理地址,以太网,局域网的扩展, 物理层 数据传输速度,频率,波长,速度,编码 网络层&…

《软考填涂答题卡须知》

考试临近,今天主要是给大家说下答题卡的填涂注意事项,希望考生们看完后可以避免一些小失误。除信息处理技术员和多媒体应用制作技术员采取笔试与上机操作考试相结合的形式外,其他各种考试都采用笔试形式。考试实行全国统一大纲、统一试题、统…

考试管理系统

开发工具(eclipse/idea/vscode等): 数据库(sqlite/mysql/sqlserver等): 功能模块(请用文字描述,至少200字): 模块划分:老师模块、班级模块、学生模块、课程模块、试题模块、试卷模块、 组卷模块、考试模块、答题模块 管…

【CSP考前复习】关于考试时的注意事项

前言 作为一名已经服役4年的老年OI选手,经历过的考试已经是数以百计了。在这么多次考试中,爆零垫底是常有的事,运气好了靠暴力得到好名次的事也经常发生。那么,CSP临近,我想有必要好好整理一下这些问题和经验。 一、…

CKA OFFCIAL TEST准备工作考试说明练习题

文章目录 1. 准备工作1.1 证书详情1.2 考试注意事项 2. 考试说明2.1 考试环境2.2 考试期间允许的资源2.3 考试技术说明2.4 可接受的测试地点2.5 其他资源2.A 考试小提示 3. 练习题Task 1. RBAC - role based access control(4/25%)Task 2. drain - highl…

CCNA考试流程、考试费用及考场介绍

4月15日CCNAHCIA新一轮班级开班 CCNA(Cisco Certified Network Associate)思科认证网络工程师 一、CCNA认证考试流程 ccna考试认证,先学习CCNA理论知识,然后下载CCNA考试题库,预约考试,参加并通过考试。 C…

股票ctp交易接口是什么?

股票ctp交易接口是什么?CTP接口除官方发布的C动态库外,很多人也提供了Python、Java、C#等各主流语言的接口,可搜索相应的开发方法。一个连接能够查询的股票数量是有限的,性能也不高,但是可以创建多个api实例并发工作&a…

美股投资指南 – 网上美股开户其实很简单

美股开户教程 – 从香港招行到Firstrade 犹豫再三,觉得还是把这帖子贴在这里吧,文章是我写在自己新开的博客里的,分上下两篇,记录自己在美股开户中碰到的问题,写的不咋怎么样都是粗记录,既然成文就公布吧&a…

阿里云Redis之:为阿里云Redis申请公网地址(十九)

1.申请公网地址 1)点击实例信息—>找到连接信息—>找到公网访问—>申请连接地址 2)填写外网连接地址,然后填写端口号,最后点击确定。 3)外网地址申请成功 2.通过Redis客户端连接云Redis 阿里云官方反馈公网地址不能通过Redis Desktop Manager连接,存在兼容性问…