用非递归方法实现递归算法时_学习递归的另一种方法

用非递归方法实现递归算法时

每个学期,我都会通过一项调查,以获取有关我的教学的一些反馈。 上学期终于有人给我写一篇新文章的想法。 特别是,他们想了解有关递归的更多信息,所以我认为我会综合一些技巧。

递归概述

对于那些可能是第一次学习递归的人,我想我会提供一些有关概念的概述。

特别地,递归是一种依赖于解决较小子问题的问题解决技术。 换句话说,不是继续直接解决问题,而是继续分解问题,直到找到可以实际解决的较小问题为止。 然后,我们使用最小子问题的答案来进行倒推,直到获得原始问题的答案为止。

例如,假设我们要计算2 6 。 通常,我们会将其分解为重复乘法。 换句话说,我们将2乘以6乘以自身,得出64。在计算机科学中,我们将此称为问题解决技术迭代,通常以循环的形式看到它:

 def iterative_power(base, exponent): 
   product = 1 
   for i in range(exponent): 
     product *= base 
   return product  print(iterative_power( 2 , 6 )) 

当然,如果我们已经知道一些较小的子问题的答案,该怎么办。 例如,如果我们知道2 5是32怎么办? 然后,我们可以通过将2 5乘以2来直接计算2 6。这就是递归的思想:

 def recursive_power(base, exponent): 
   if exponent == 0 : 
     return 1 
   else : 
     return recursive_power(base, exponent - 1 ) * base 

在本文中,我们将介绍递归的另一种思考方式。 希望它可以帮助您比以前更好地理解概念。

为什么递归如此困难?

虽然函数式编程目前正在卷土重来,但是它对我们学习编码的方式并没有产生太大的影响。 结果,大多数人开始使用类似C的命令式编程语言进行编码。 例如,我开始使用Java并转入C,C ++,C#和Python等语言。

不幸的是,学习像C这样的语言的弊端在于,我们在解决问题的思维方式上受到了一定的限制。 换句话说,以这些语言提供给我们的工具集在很大程度上偏向分支(即,如果语句和循环)。

结果,递归背后的直觉并不是从一开始就建立的。 取而代之的是,我们被迫使用与迭代解决方案相关的人为示例引入递归。 否则,我们将如何联系。

另外,像C这样的命令性语言在引入递归之前需要大量的开销。 例如,您可能甚至在开始考虑递归之前就必须学习变量,分支和函数等概念。 实际上,直到我第一次学习数据结构课程时,我才亲自介绍这个概念。

总而言之,可能很难掌握递归,因为它需要您对编码的理解成为现实。 幸运的是,有很多方法可以打破这种循环( 绝对是双关语)。

通过合同设计学习递归

最近,我在俄亥俄州立大学接受培训以教授软件组件课程,而该课程涵盖的主题之一是递归。 当然,那时我已经非常熟悉递归,但是我认为他们教这个主题的方式确实很优雅。 结果,我认为我可以将该方法传递给您。 也就是说,我们需要首先解决按合同设计。

合同设计概论

如果我在过去写过一些有关“按合同设计”的文章( 主要是指JUnit测试) ,请再说一遍我深表歉意。 自然地,我认为我现在是一个稍微好一点的作家,但也可以随时参考该文章。

无论如何, 按合同设计 (DbC)是一种编程哲学,在该哲学中,我们根据合同来考虑功能。 换句话说,当用户使用我们的功能之一时,我们希望为他们提供某种保证。 特别是,我们要求用户在一定条件下(又称为前提条件)执行每个功能。 因此,我们保证每个函数都将以某种特定方式运行(也称为后置条件)。

DbC在递归中很重要,因为它允许我们精确地指定函数在特定条件下将执行的操作。 例如,只要我们指定一些先决条件,我们先前定义的幂函数将起作用:

  • 两个参数都必须是整数(可以通过一些类型提示来解决)
  • power参数不能为负

结果,我们保证这两个函数都将使加数的base恢复一定的power 。 如果用户输入了无效的输入,我们将不会做出任何承诺,因为他们显然违反了合同。 当然,我们总是可以提供某种输入验证来防止令人讨厌的错误,但这超出了DbC的范围。

全民免费午餐

在OSU中,我们通过完全忽略递归的思想来介绍递归。 取而代之的是,我们利用对合同设计的了解,开始隐式地实现递归函数。

例如,让我们再看看我们一直在谈论的这个幂函数:

 def power(base: int , exponent: int ) -> int : 
   "" "Computes the base ^ exponent. 
   Precondition: exponent >= 0 
   Postcondition: base ^ exponent 
   "" " 
   return FreeLunch.power(base, exponent) 

在此示例中,我们从名为FreeLunch的神奇库中引入了新的幂函数。 就我们而言,这一新的幂函数与我们编写的函数完全相同,即相同的合同。

现在,为了争辩,我们假设这个FreeLunch幂函数有一个要求:其输入必须比幂函数的输入“小”。 我们怎么做才能确保这项工作有效?

好吧,如果我们减小指数,我们可以通过乘以底数(即x 5 = x 4 * x)将其加回去。 让我们尝试一下:

 def power(base: int , exponent: int ) -> int : 
   "" "Computes the base ^ exponent. 
   Precondition: exponent >= 0 
   Postcondition: base ^ exponent 
   "" " 
   return FreeLunch.power(base, exponent - 1 ) * base 

在这一点上,我们如何去验证它是否有效? 好吧,让我们尝试一些输入。 例如,我们可以再次尝试2 6 。 如果我们遍历代码,则会注意到我们调用了FreeLunch.power(2, 5) ,这是一个完全有效的输入。 换句话说,我们将得到32,然后乘以2得到64,这是正确的答案。

也就是说,是否有任何输入会失败? 绝对! 由于FreeLunch功效函数与我们的FreeLunch函数具有相同的合同,因此存在无效的输入。 例如,我们应该避免FreeLunch幂函数的值小于0的任何情况:

 def power(base: int , exponent: int ) -> int : 
   "" "Computes the base ^ exponent. 
   Precondition: exponent >= 0 
   Postcondition: base ^ exponent 
   "" " 
   if exponent == 0 : 
     return 1 
   else : 
     return FreeLunch.power(base, exponent - 1 ) * base 

现在,可以确定我们的幂函数可以工作了。

天下没有免费的午餐

您可能会想到,以这种复杂的方式实现幂函数只是一种欺骗您使用递归的练习。 换句话说,我们可以完全删除上面的FreeLunch类,并且我们将拥有一个功能齐全的递归解决方案:

 def power(base: int , exponent: int ) -> int : 
   "" "Computes the base ^ exponent. 
   Precondition: exponent >= 0 
   Postcondition: base ^ exponent 
   "" " 
   if exponent == 0 : 
     return 1 
   else : 
     return power(base, exponent - 1 ) * base 

现在的问题是:为什么我们要经历所有这些努力来引入递归? 毕竟,几乎所有其他递归教程都利用某种堆栈。 我们为什么不这样做呢?

虽然使用堆栈教递归完全有效,但对学生而言通常很麻烦。 例如,想象一下在不使用我们的FreeLunch技巧的情况下通过上面的幂函数进行跟踪。 如果我们提供两个随机输入(例如2的底数和6的指数),则必须跟踪每个幂函数调用,直到达到最小的子问题为止。 在纸面上,这是一个简单示例的六种痕迹!

通过这种方式进行授课时,学生在尝试各种程序输入时经常会发现自己迷失在递归中。 如果他们转而放弃对合同的怀疑和信任,他们可以一次证明自己的功能有效。 当然,他们将需要特别注意其输入( 请参阅数学归纳法 )。

递归根

现在,我们已经看到了一些示例,我想谈一谈递归的来源。 毕竟,递归通常是一个令人恐惧的话题,在方便的计算机科学讲座中似乎会出现。 幸运的是,它的历史远不止于此。

还记得我们之前看过的幂函数吗? 那不是偶然。 实际上,存在大量可以递归编写的数学公式。 例如,幂公式如下所示:

a n = a n-1 * a(如果n> 0)

自然地, n可以连续分解,直到指数为1或0为止,这取决于我们希望终止递归的方式。 在上面的示例中,当n = 1时我们忽略了,因为n = 0给我们1很好。 换句话说,为n = 1添加一个额外的情况对结果没有影响。

像幂一样,您可能已经知道其他一些递归公式。 例如,斐波那契数列由递归公式定义:

a n = a n-1 + a n-2

为确保此公式有效,我们必须定义前两个术语。 然后,一切正常。 根据您询问的人,前两个术语可以是0和1或1和1。无论如何,这两对数字都可以工作。

如果要随后计算序列中的任意项,则可以分解原始函数,直到遇到任意一种基本情况。 例如,下图说明了如何计算斐波那契数列中的第五项:

我们整天都可以争论递归算法的效率,但这是解决问题的有效方法。 而不是逐步解决问题,我们将问题分解为较小的问题,直到找到可以解决的问题为止。 然后,我们使用该结果向后工作,直到获得最终答案。

递归结构

既然我们已经从数学和按合同设计的角度研究了递归,那么问题就变成了:我们如何认识到递归有益的问题? 换句话说,是否存在某种形式的递归结构问题? 让我们来谈谈它。

事实证明,我们周围都有递归结构。 例如,斐波那契数列被明确定义为递归公式。 当然,还有其他类型的递归结构。 例如,上面的树图是一个递归结构。 毕竟,每次对“ fib”的调用都会创建另一棵树。 换句话说,树内有树木。

在技​​术之外,还有诸如分形之类的递归结构的其他示例,这些分形是几何形状的集合,这些形状在任何尺度上都保持其结构。 对于熟悉Triforce的人来说 ,这是一个简单的分形的完美示例:一个由三角形组成的三角形。

同样,每天都有几种递归结构,例如目录(包含文件夹的文件夹)和洋葱(包含洋葱的洋葱)。 换句话说,层次结构和嵌套结构通常是递归的良好指示。

从这些示例中,我们可以识别出任何类型的模式吗? 嗯,当然! 这里的关键是观察问题的结构。 问题本身是否由类似的问题组成? 如果是这样,则递归可能是可行的方法。 如果没有,尝试其他方法可能更有意义。

著名的递归算法

综上所述,我认为将一小段著名的递归算法用于启发目的很有帮助。 随时在评论中分享您的一些收藏夹:

至此,我认为我们涵盖了所有内容。 如果您有本文未涉及的任何问题,请随时将其引向下面的评论。

参见递归

如果您想了解有关递归的更多信息,我建议您阅读这篇非常出色的文章,名为“ 另一种学习递归的方法” (对不起,我至少要开一个递归的笑话)。 否则,谢谢您的光临!

如果您想坚持下去,我还有很多其他文章适合您:

再次感谢您的支持。 每一点都走很长的路!

翻译自: https://www.javacodegeeks.com/2019/08/yet-another-way-learn-recursion.html

用非递归方法实现递归算法时

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值