提及递归,我们或许有很多个理由来拒绝它:
- 设计不合理容易陷入死循环,耗尽内存;
- 循环的函数调用导致资源开销大;
- 因存在对同类小规模问题的单独处理而存在重复计算问题等等。
但不可否认的,递归可以使得代码逻辑更加简洁、直观,而且针对某些复杂场景比如树的遍历操作,递归策略显得更加适用!
对于大多数猿类来说,递归一直是一个神奇的存在:有时候它很简单,简单到求解sum(n)(1, 2, 3, …, n这n个正整数求和);有时候它又很复杂,复杂到无从下手,代码增删改不停循环,最终放弃!
本文结合个人的一些经验,以上述正整数递归求和问题为例来聊聊关于递归的一些感悟!
简单来说,递归函数就是一个函数重复进行自我调用,直至触发某个条件,此时函数将返回确定的结果。
- 明确问题是什么
针对一个递归问题,我们最容易犯的错误一上来就将注意力放在如何实现递归逻辑的代码,而忽略了这个递归函数要解决什么样的问题!也就是说,从一开始我们的注意力就被函数的实现细节拖累,陷入细节泥潭!
针对上述情况,个人经验是首先从宏观全局把握你要设计的函数是用来解决什么问题的?无论是递归函数还是非递归函数,其都是为了解决某一个特定问题而存在,其都需要特定的函数声明。故,第一要素即是 – 确定一个合理的函数声明。
针对实例程序,我们可以明确其递归函数的声明:int sum(int n) {} - 明确结束条件
前面提及过,递归是函数的一个自我调用过程,为了避免陷入循环调用的无底洞,我们需要给函数设定一个结束条件,当该条件被触发时,函数调用将立即结束,此时,我们可以获取到一个明确的执行结果。
针对示例程序,我们可以清晰看出来:
n=0,sum(n) = 0;
n=1,sum(n) = 1; - 明确等价关系
针对存在前后项依赖的情况,我们需要寻找一个等价关系(可以理解为一个递推式),针对示例程序来说类似于:
sum(n) = n + sum(n-1)。
一旦等价关系确定,你会发现问题的规模可以借助于其不断减小:
sum(n) = n + sum(n-1)
sum(n-1) = (n-1) + sum(n-2)
…
sum(3) = 3 + sum(2)
sum(2) = 2 + sum(1)
sum(1) = 1; // 递归结束条件
既然递归的三个关键问题都已经清晰,那么这个递归函数也就很容易写出来,我们加以完善形成最终的代码块:
/* 计算1, 2, 3, ..., n 这n个正整数的和 */
int sum(int n) {
if (1 <= n) {
return n;
}
return (n + sum(n-1));
}
相信看完本文的同学会有一种恍然大悟的感觉,但是文中示例仅仅是极为简单的一个程序,实际应用中的递归问题往往复杂很多。但是,无论何种递归问题,只要能够解决上述三个关键问题,那么最终的解决方案必将很快呈现于你我面前。
【若干递归问题】