为什么说递归是码农的一道分水岭?

为什么这一篇要先写递归这种思想呢?初衷主要是因为最近在写设计模式中的组合模式。这个设计模式的实现呢,需要使用到树形数据结构,而树形结构又是天生的递归结构,所以这一篇主要是给大家打基础,顺便也从个人的一些开发经历来给大家安利一波递归。

递归在码农界有这么一种说法,人理解迭代,神理解递归。在我看来,递归确实应该算是码农编程能力的一道分水岭。所谓分水岭,就是说能轻松理解递归而且灵活运用的程序员往往思维方式是异于常人的(大部分情况也优于常人),这样子的程序员在本质上跟不能理解递归、甚至厌恶递归的程序员其实不是同一个level的开发者了。大家先别急着喷我,我有以下几点理由:

  • 不能理解递归,意味着归并、快排、二叉树、回溯、贪心、动态规划等人类花了几十数百年总结出来的这些优秀算法,对你而言只是个躺在书本上毫无价值的死代码。

  • 不能理解递归,意味着编译原理的词法分析、语法分析等,你将一无所知。虽然大部分人都没机会接触编译器底层机制,但是天天使用着各种各样的编译器,却对它内部一无所知,是个小遗憾。

  • 不能理解递归,意味着在大规模复杂情景下的开发,不得不忍受迭代的繁琐,以及无比臃肿丑陋的代码。

不过递归并不是万能的,要全面地认识它的优缺点。那就是递归实现的程序,虽然结构清晰、思路明了,但是递归的执行过程蛮让人费解的。人一旦陷进去递归的细节很容易就会被绕晕。递归程序的调试也很不方便,尤其在嵌入式设备等内存资源紧张的场合,容易发生堆栈溢出。

递归的哲学

那么递归是什么呢?我们的老祖宗其实在几千年前就已经告诉我们了。<<大学>>有曰:

古之欲明明德于天下者,

先治其国;

欲治其国者,先齐其家;

欲齐其家者,先修其身;

欲修其身者,先正其心;

欲正其心者,先诚其意;

欲诚其意者,先致其知,

致知在格物。

物格而后知至,知至而后意诚,

意诚而后心正,心正而后身修,

身修而后家齐,家齐而后国治,

国治而后天下平。

简单翻译一下这段话,主要意思是说:

古时候,要想使天下人都发扬光明正大的德行,就要先治好自己的国家;想要先治好自己的国家,就要先管理好自己的家庭;要想管理好自己的家庭,就要先修养自己的身心;要修养自己的身心,就要先端正自己的心志;要想端正自己的心志,就要先使自己的意念真诚,就要先丰富自己的知识;要想丰富自己的知识,就得深入研究事物的原理。

通过对万事万物的认识,研究后才能获得知识;获得知识后意念才能真诚;意念真诚后心思才能端正;心思端正后才能修养品性;品性修养后才能管理好家庭和家族;管理好家庭和家族后才能治理好国家;治理好国家后天下才能太平。

这段翻译完美地描述了递归的哲学,那就是要先达成一个大目标,必须先逐步把它分解成小目标,直到小目标是自己能够达到的为止。然后通过不断地达成小目标,最终实现大目标。

理解递归的关键

谨记递归的两个过程

以下是引用知乎网友的一段描述,讲得非常形象易懂:

递归是静中有动,有去有回。
循环是动静如一,有去无回。

举个例子,给你一把钥匙,你站在门前面,问你用这把钥匙能打开几扇门。

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

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

递归思想递归就是有去(递去)有回(归来)。具体来说,为什么可以”有去“?这要求递归的问题需要是可以用同样的解题思路来回答类似但略有不同的问题(上面例子中的那一把钥匙可以开后面门上的锁)。为什么可以”有回“?这要求这些问题不断从大到小,从近及远的过程中,会有一个终点,一个临界点,一个baseline,一个你到了那个点就不用再往更小,更远的地方走下去的点,然后从那个点开始,原路返回到原点。

一个例子

我们来看下最简单的递归--阶乘算法,这个例子的巧妙解析也是出自知乎网友。

int f(int n)
{
    int sum = 0;
    if (0 == n)
        return 1;
    else
        sum = n * f(n-1);
    return sum;
}

这里面用f函数调用了自己,这该怎么理解?
我们先来直接展开一个计算结果看看:

f(4)=>4*f(3)
f(4)=>4*(3*f(2))
f(4)=>4*(3*(2*f(1)))
f(4)=>4*(3*(2*(1*f(0))))
f(4)=>4*(3*(2*1))
f(4)=>4*(3*2)
f(4)=>4*6
f(4)=>24

这里面非常清晰地展示了递归的过程,如下图:

这个例子就是非常典型的,在归来的过程中解决问题。

我们思考一下:

为什么在归来过程可以解决问题?

这是因为我们先寻找到了最小的目标,这个最小目标是我们可以直接完成的(也就是递归的终止条件)。等实现了最小的目标后,接着再逐级反过来实现更大的目标。直到完成我们最初定下的大目标为止。

到这里真的不得不感概,书中自由颜如玉、书中自有黄金屋。古人诚不欺我矣!

按照这样子的思路,可以总结出归来过程解决问题的递归模型:

 function recursion(大规模){
    if (end_condition){      // 明确的递归终止条件
        end;   // 简单情景
    }else{            // 先将问题全部描述展开,再由尽头“返回”依次解决每步中剩余部分的问题
        recursion(小规模);     // 递去
        solve;                // 归来
    }
}

因此只要在递归抓住最小目标是什么,以及思考怎么由最小目标反过来逐步实现更大的目标(其实就是在归来过程,怎么解决当前的问题),那么对于大部分的递归算法你就能不再畏惧,理解起来也不再吃力了。

类似更复杂的多重递归(比如汉诺塔、树的遍历等等),也可以套用这种思想,只要脑海中清晰地知道多种递归,不是进行一次目标分解,而是进行多次目标分解,理解起来也会容易很多。

写得太不容易了。。。

发布了10 篇原创文章 · 获赞 4 · 访问量 1万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览