07 递归

举个生活中的例子:

周末,你带着你女朋友去买奶茶,可是这家店太火爆了,排了老长的队伍。突然,奶茶店给出了一个通知,说,只剩最后100杯了,卖完就没了。

女朋友问你,现在我们所在位置是第几呀?队伍太长了,你不想一个个数一次。于是你问前面的人他是第几个,这样只需要在他的基础上加1,就知道自己是第几了。

但是前面的人也不知道他自己是第几,所以他也问前面的。就这样一个问一个,直到问道了第一个,说我是第一个。然后再这样一个个把数字往后传,直到你前面的人告诉你他是第几,这样你就知道答案了。

这就是一个非常标准的递归求解问题的分析过程,去的过程叫“递“,回来的过程叫”归“。

基本上所有的问题都能用递推公式来表示,刚才的例子就可以这样表示:

f(n) = f(n-1) + 1

f(1) = 1

f(n)表示你所在位置的数字,f(n-1)表示你前面位置的数字。f(1) =1表示第一个位置的数字是1.

有了递推公式,我们就能把他改写成代码:

int f(int n) {
  if (n == 1) return 1;
  return f(n-1) + 1;
}

一、递归需要满足的三个条件

1、一个问题的解可以分为多个问题的解

可以把一个问题分为一个个数据规模更小的子问题,比如前面的例子。

2、子问题的求解思路完全样

比如前面的例子,除了第一个,每个人求自己位置的办法都一样。

3、存在递归终止条件

这是最重要的一个条件。把问题分为子问题,再把子问题一层层分解下去,必须得有终止条件,不能无限制。

比如刚才f(1) =1 就是终止条件。

二、如何编写递归代码

最关键的就是找出递推公式终止条件。

比如一个常见的算法题,爬楼梯:

假如有n阶台阶,你每次只能跨一个台阶或者2个台阶,那么走这n个台阶有多少种走法?  

比如一个台阶只有一种走法。2个台阶可以是1 1和2,两种走法。3阶台阶可以是1 1 1,1 2,2 1这三种走法。。。。

分析:

当你走到第n阶台阶的时候,最后一步你只会有2种情况走上来。要么是跨了一个台阶上到第n个台阶,要么是跨了2个台阶上来。

当你是跨了跨一个台阶上来的时候,只需要知道n-1个台阶的时候的走法数。跨了2个台阶,只需要知道n-2个台阶的走法数。

可以表示为f(n) = f(n-1) + f(n-2)

也就是第n个台阶的走法= n-1个台阶位置的走法 + n-2个台阶位置的走法。

终止条件:

f(1) = 1, f(2) = 2

所以合起来就是:

f(n) = f(n-1) + f(n-2)
f(1) = 1
f(2) = 2

代码表示:

int f(int n) {
  if (n == 1) return 1;
  if (n == 2) return 2;
  return f(n-1) + f(n-2);
}

总结:

人的大脑适合平铺直叙的思维方式,不适合递归这种思维,所以不要用大脑去一层层嵌套下去,我们只需要总结递推公式就好。只有找到终止条件,以及问题与子问题之前的公式就能写出递归代码了。

三、递归要警惕栈溢出

前面栈章节说过,程序调用会使用栈来存储临时变量,每调用一个函数,就会将临时变量封装为栈帧放压入内存栈(java中叫做java虚拟机栈),等函数执行完返回,才会出栈。这种空间一般都不大,很容易造成溢出。

如何避免:

1、一定要有终止条件    2、增加变量,每递归一次就+1。然后限制递归深度。

四、递归要警惕重复计算

之前的代码中,如上图,我们要计算f(6)就得先计算f(5)和f(4)。计算f(5)的时候还得再计算一次f(4)。有很多重复运算。

为了避免重复运算,我们可以把计算后的结果保存下来。照着这个思路,我们改写下代码:


public int f(int n) {
  if (n == 1) return 1;
  if (n == 2) return 2;
  
  // hasSolvedList可以理解成一个Map,key是n,value是f(n)
  if (hasSolvedList.containsKey(n)) {
    return hasSolvedList.get(n);
  }
  
  int ret = f(n-1) + f(n-2);
  hasSolvedList.put(n, ret);
  return ret;
}

 在考虑递归的时间复杂度,我们要考虑递归调用,所以前面排队买奶茶的例子中,时间复杂度是O(n)

五、如何将递归代码改为非递归

第一个排队:


int f(int n) {
  int ret = 1;
  for (int i = 2; i <= n; ++i) {
    ret = ret + 1;
  }
  return ret;
}

第二个爬楼梯:


int f(int n) {
  if (n == 1) return 1;
  if (n == 2) return 2;
  
  int ret = 0;
  int pre = 2;
  int prepre = 1;
  for (int i = 3; i <= n; ++i) {
    ret = pre + prepre;
    prepre = pre;
    pre = ret;
  }
  return ret;
}

是不是所有的递归都能改为非递归那?

笼统的讲: 是的,因为递归本身就是借助栈来实现的,如果自己实现栈,任何代码看上去都会是非递归的。不过这样徒然增加复杂度,没有改变递归的本质。

六、总结

递归只要满足三个条件,就能写出来。

递归代码难写,难理解。编写递归代码不要把自己绕进去,正确的姿势是写出递推公式,终止条件,然后再翻译成递归代码。

递归代码要注意:堆栈溢出、重复计算等

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

七号公园的忧伤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值