递归

本文深入解析递归的概念,通过实例讲解递归的分解、求解思路与终止条件,探讨递归代码的实现技巧,包括避免重复计算和栈溢出的方法,并介绍如何将递归转换为非递归。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

递归

1. 如何理解递归

例:领导分配给你一个任务量为n的任务,你做了一部分,然后把剩下的n-1分配给别人,别人又做了一部分,把剩下的n-2分配给了另一个人,直到最后一个人,做了一部分,然后把任务完成返回给他上一个人。

这个任务下发的过程叫,任务返回的过程叫

基本上,所有的递归问题都可以用递推公式来表示,上面这个例子,递推公式表示为:f(n)=f(n-1)+1 其中,f(1)=1

上述递归问题的代码表示如下:

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

2. 递归需要满足的条件

1)一个问题可以分解为几个字问题的解

2)这个问题与分解之后的字问题,除了数据规模不同,求解思路完全一样

3)存在递归终止条件

3. 递归代码如何实现

写递归代码最关键的是写出递推公式,找到终止条件。

青蛙跳台阶问题:

假设有n个台阶,每次青蛙可以跳1个台阶或者2个台阶,请问走完这n个台阶有多少种走法?

例如:有7个台阶,可以2,2,2,1上去,也可以1,2,1,1,2这样上去,总之走法很多,如何编程实现呢?

分析:根据第一步走法把所有走法分为2类,第一类:第一步走了1个台阶,第二类:第一步走了2个台阶。所以n个台阶的走法 =(先走1个台阶后,n-1个台阶的走法)+(先走2个台阶后,n-2个台阶的走法)

用递推公式表示就是:

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

递推的终止条件:

当有一个台阶时,不需要再继续递归,只有一中走法。

当有两个台阶时,可以每次走1个台阶,走两次,也可以一次两个。

当有三个台阶时,就分解为有一个台阶和两个台阶的问题了。

所以有:


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

人脑几乎没办法把整个递归的过程一步一步想清楚,所以很多时候,我们只需要把它抽象成一个递推公式,不用想一层一层的调用关系,这些交给计算机来做就好了。

4. 要注意栈溢出!

如果递归求解的数据规模很大,调用层次很深,就会有栈溢出的发生。

我的一次栈溢出的经历:在练习用递归模拟strlen函数功能时,如下代码:


int my_strlen(char *str)
{
    if(*str == '\0')
        return 0;
    return 1 + my_strlen(str++);
}

这里传进去的始终是str,所以没有终止的一直压栈,导致栈溢出。

解决栈溢出的方法:

1)限制递归深度

这种做法并不能完全解决问题,因为最大允许的递归深度和当前线程剩余的栈空间大小有关,事先无法计算,如果实时计算,代码过于复杂,影响可读性。所以对于最大深度比较小的情况,如10、50,就可以用这种方法,否则这种方法并不是很实用。

2)在堆区模拟系统的工作栈

如果需要可以采用这种方法。

5. 警惕重复计算

以青蛙跳台阶的问题为例:

图片引用自极客时间

从图中可以看到:想要计算f(5),需要先计算f(4)和f(3),而f(4)还需要计算f(3),因此,f(3)被重复计算了多次,这就是重复计算问题!

还有个例子:递归方法求第n个斐波那契数,也会有同一n重复计算多次的现象。

当计算第50个斐波那契数时,f(3)被计算了上亿次,这个计算花了10分钟左右!

重复计算的解决方法:

为了避免这种重复计算带来的负面开销,可以通过引入散列表来保存已经求过的f(k)。当递归调用到f(k)时,先看下是否求解过了,如果是直接从散列表中取这个值,就不需要再次计算了。

6. 递归代码如何改写为非递归代码

递归有利有弊,递归的问题不仅仅就上面说的重复计算和栈溢出,递归的空间复杂度有时也比较高。

如果用递归方法求第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;
}

这种思路其实是将递归改成了“手动”递归,本质没有变,增加了实现难度。


完,文中不足还请多指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值