欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

递归算法理解

程序员文章站 2024-02-04 11:09:28
...

递归算法终极理解—用人脑理解递归算法

<div class="article-info-box">
    <div class="article-bar-top d-flex">
                                            <span class="time">2014年04月24日 20:51:39</span>
        <div class="float-right">
            <span class="read-count">阅读数:6806</span>
                                        </div>
    </div>
</div>
<article>
    <div id="article_content" class="article_content clearfix csdn-tracking-statistics" data-pid="blog" data-mod="popu_307" data-dsm="post">
                <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-62cd27f8b9.css">
                    <div class="htmledit_views">

特别声明:这篇文章是我无意中读到的,不是本人创作,感觉对递归算法的总结和理解已经到了一个很深的层次,非常喜爱,奇文大家赏。出处不好意思,由于我的IOS设备上发现的,网址打不开,所以就没写明出处,希望原文作者见谅!

递归真是个奇妙的思维方式。自打我大二学习递归以来,对一些简单的递归问题,我总是惊叹于递归描述问题和编写代码的简洁。但是总感觉没能融会贯通地理解递归,有时尝试用大脑去深入“递归”,层次较深时便常产生进不去,出不来的感觉。这种状态也导致我很难灵活地运用递归解决问题。有一天,我看到一句英文:“To Iterate is Human, to Recurse, Divine.”中文译为:“人理解迭代,神理解递归。”然后,我心安理得地放弃了对递归的深入理解。直到看到王垠谈程序语言最精华的原理时提到了递归,并说递归比循环表达能力强很多,而且效率几乎一样。再次唤醒了我对递归的理解探索。

我首先在知乎上发现了下面两个例子,对比了递归和循环。

递归:你打开面前这扇门,看到屋里面还有一扇门(这门可能跟前面打开的门一样大小(静),也可能门小了些(动)),你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开,。。。, 若干次之后,你打开面前一扇门,发现只有一间屋子,没有门了。 你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这钥匙开了几扇门。

循环:你打开面前这扇门,看到屋里面还有一扇门,(这门可能跟前面打开的门一样大小(静),也可能门小了些(动)),你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,(前面门如果一样,这门也是一样,第二扇门如果相比第一扇门变小了,这扇门也比第二扇门变小了(动静如一,要么没有变化,要么同样的变化)),你继续打开这扇门,。。。,一直这样走下去。 入口处的人始终等不到你回去告诉他答案。

该用户这么总结到:

递归就是有去(递去)有回(归来)。

具体来说,为什么可以”有去“? 
这要求递归的问题需要是可以用同样的解题思路来回答除了规模大小不同其他完全一样的问题。

为什么可以”有回“?
这要求这些问题不断从大到小,从近及远的过程中,会有一个终点,一个临界点,一个baseline,一个你到了那个点就不用再往更小,更远的地方走下去的点,然后从那个点开始,原路返回到原点。

上面的解释几乎回答了我已久的疑问:为什么我老是有递归没有真的在解决问题的感觉?
因为递是描述问题,归是解决问题。而我的大脑容易被递占据,只往远方去了,连尽头都没走到,何谈回的来。

《漫谈递归:递归的思想》这篇文章将递归思想归纳为:

递归的基本思想是把规模大的问题转化为规模小的相似的子问题来解决。在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况。另外这个解决问题的函数必须有明显的结束条件,这样就不会产生无限递归的情况了。

需注意的是,规模大转化为规模小是核心思想,但递归并非是只做这步转化,而是把规模大的问题分解为规模小的子问题和可以在子问题解决的基础上剩余的可以自行解决的部分。而后者就是归的精髓所在,是在实际解决问题的过程。

我试图把我理解到递归思想用递归用程序表达出来,确定了三个要素:递 + 结束条件 + 归。

  1. recursion(大规模)
  2. {
  3. if (end_condition)
  4. {
  5. end;
  6. }
  7. else
  8. { //先将问题全部描述展开,再由尽头“返回”依次解决每步中剩余部分的问题
  9. recursion(小规模); //go;
  10. solve; //back;
  11. }
  12. }

但是,我很容易发现这样描述遗漏了我经常会遇到的一种递归情况,比如递归遍历的二叉树的先序。我将这种情况用如下递归程序表达出来。

  1. recursion(大规模)
  2. {
  3. if (end_condition)
  4. {
  5. end;
  6. }
  7. else
  8. { //在将问题转换为子问题描述的每一步,都解决该步中剩余部分的问题。
  9. solve; //back;
  10. recursion(小规模); //go;
  11. }
  12. }

总结到这里,我突然发现递归是为了最能表达这种思想,所以用“递归”这个词,其实递归可以是“有去有回”,也可以是“有去无回”。但其根本是“由大往小地去,由近及远地去”。“递”是必需,“归”并非必需,依赖于要解决的问题,有的需要去的路上解决,有的需要回来的路上解决。有递无归的递归其实就是我们很容易理解的一种分治思想。

其实理解递归可能没有“归”,只有去(分治)的情况后,我们应该想到递归也许可以既不需要在“去”的路上解决问题,也不需要在“归”的路上解决问题,只需在路的尽头解决问题,即在满足停止条件时解决问题。递归的分治思想不一定是要把问题规模递归到最小,还可以是将问题递归穷举其所有的情形,这时通常递归的表达力体现在将无法书写的嵌套循环(不确定数量的嵌套循环)通过递归表达出来。
将这种递归情形用递归程序描述如下:

  1. recursion()
  2. {
  3. if (end_condition)
  4. {
  5. solve;
  6. }
  7. else
  8. { //在将问题转换为子问题描述的每一步,都解决该步中剩余部分的问题。
  9. for () { recursion(); //go; }
  10. }
  11. }

例如,字符串的全排列就可以用这种递归简洁地表达出来,如下:

  1. void permute(const string &prefix, const string &str)
  2. {
  3. if(str.length() == 0)
  4. cout << prefix << endl;
  5. else
  6. {
  7. for(int i = 0; i < str.length(); i++)
  8. permute(prefix+str[i], str.substr(0,i)+str.substr(i+1,str.length()));
  9. }
  10. }

由这个例子,可以发现这种递归对递归函数参数出现了设计要求,即便递归到尽头,组合的字符串规模(长度)也没有变小,规模变小的是递归函数的一个参数。可见,这种变化似乎一下将递归的灵活性大大地扩展了,所谓的大规模转换为小规模需要有一个更为广义的理解了。

对递归的理解就暂时到这里了,可以看出文章中提到关于“打开一扇门”的递归例子来解释递归并不准确,例子只描述了递归的一种情况。而“递归就是有去(递去)有回(归来)”的论断同样不够准确。要为只读了文章前半部分的读者惋惜了。我也给出自己对递归思想的总结吧:

递归的基本思想是广义地把规模大的问题转化为规模小的相似的子问题或者相似的子问题集合来解决。广义针对规模的,规模的缩小具体可以是指递归函数的参数,也可以是其参数之一。相似是指解决大问题的方法和解决小问题的方法往往是同一个方法,还可以是指解决子问题集的各子问题的方法是同一个方法。解决大问题的方法可以是由解决次规模问题的方法和解决剩余部分的方法组成,也可以是由一系列解决次规模问题的方法组成。

转载自https://blog.csdn.net/theknotyouknow/article/details/24435291

写在前面

最近在重新学习《算法导论》这本书,在看到介绍递归的时候,想起了当时学习算法时的痛苦,递归是一种让人有爱与恨的算法理念,之所以爱是因为其的使用思路很清晰且算法复杂度等接近最优,恨就是因为它的思想太过于抽象了。。。

虽然现在对递归的理解仍然算不上透彻,但是也能算运用自如了,相信大家看完本篇博客后,学习我的“另类”方法,一定会豁然开朗(可能有些观点并不正确,但是有时候错误的观点反而能够让理解变得轻松,大家知道这些观点是错误的就行。)

对递归算法的基本理解

所谓递归,就是包含递推和回归两个自然过程,一方面要由外到里地深入,一方面又要由里到外地回到原点,这是我的基本看法,递推过程是决定整个算法的逐步计算过程,而回归就是将每一步计算的结果慢慢地进行总结,最后就能够得到我们想要的结果,说起来也还是比较抽象的,等看完下面的观点再回来看这一段基本理解就ok的。

实践是检验真理的唯一标准

我先上一个最经典的递归例子—求n的阶乘(java版):

public int get(int n){
    if(n<1){
    return 1;}
return n*get(n-1);
}
  • 1
  • 2
  • 3
  • 4
  • 5

所谓麻雀虽小,五脏俱全,上述的递归算法虽然简单,但是具备了递归的所有元素,任何复杂的算法都是由简单的算法构成的,递归也不例外,递归的元素总共有三类:

  1. 初始值
  2. 结束条件
  3. 算法

首先不难看出:初始值就是传入的参数n,它决定了你递归算法的输入值和最终结果;结束条件就是if语句中的判断,它决定递归算法的结束条件和下限;算法就是计算方法,也就是n*get(n-1)这部分,它的数学模型其实就是!n=n*(n-1)(n-2)(n-3)……*1(学好数学很重要。。。),它决定的就是具体的计算过程,也间接地决定这最终的结果。

在使用递归算法的时候,可以把整个递归函数想象成一个黑盒子,这个黑盒子的功能就是“求传入值的阶乘”,而阶乘的求法无非就是n乘上(n-1)的阶乘,以此类推。也就是说:一个黑盒子=黑盒子的值*比值小1的黑盒子的值。

后记

我看网上很多博客都是一大篇理论啊,然后分析啊之类的,我觉得没这么复杂。。毕竟程序语言就是一种工具而已,会用就行了,在运用之中遇到了问题或者运用不熟练的情况再去学习原理就ok啦。由于时间有限,暂时就写这么多,有空再来补上递归算法的内存模型,加深对递归的理解。

        <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css">
            </div>

转载自https://blog.csdn.net/qq_34773981/article/details/79225588

相关标签: 编程思想