更多C语言系列详解见qby2005_-CSDN博客,不定期更新C语言项目实战和例题。
1.递归是啥?
首先,我们要明确递归的概念,这样,才能深入地使用递归。递归递归,从字面上来说,就是先递进,后回归,也就是说,递归的结果不会在第一时间返回。从本质上来说,递归是一种调用本身函数的计算方式。通过这种方式,可以使较为复杂的问题变为一个规模较小,更容易解决的问题,直到分解到子问题不需要再分解也可以轻松解决的程度。
举个非常形象的例子:
从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?"从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?'从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?……'"
这就是递归的终极理解。那么,递归到底是怎么运作的呢?
2.递归的运作方式
在这里,先给大家粘上一张图:
这里以求一个数的阶乘作为例子:
可以看到,上图的流程是先用递进的方式一步一步将问题简化,求5的阶乘变为求4的阶乘又变为3的阶乘......最后化简到一的阶乘,一的阶乘就是一,因此就不需要继续往下算了,但是我们也可以看到,由于递归调用的函数是本身,因此结果并没有第一时间返回,而是通过回溯将结果一步一步往后退,从而计算出最终结果。
3.递归的规则
使用递归需要避免出现死循环,为了确保递归正确工作,递归程序应该包含2个属性:
- 基本情况(bottom cases),基本情况用于保证程序调用及时返回,不在继续递归,保证了程序可终止。
- 递推关系(recurrentce relation),可将所有其他情况拆分到基本案例。
这是递归两个最重要的原则 ,下文我们将在经典例题中体现。
4.递归与其他方式的区别
优雅性
相比其他解法(比如迭代法),使用递归法,你会发现只需少量程序就可描述出解题过程,大大减少了程序的代码量,而且很好理解。递归的能力在于用有限的语句来定义对象的无限集合。
反向性
由于递归调用程序需要维护调用栈,而栈(我们在上文提过)具有后进先出的特征,因此递归程序适合满足取反类需求。我们在第五部分有一些编程实践,比如字符串取反,链表取反等相关有趣的算法问题。
递推关系
递归程序可以较明显的发现递推关系,反过来也可以这么说,具有递推关系的问题基本都可以通过递归求解(当然也许有性能更佳的解法,但递归绝对是一种选择)。递推关系常见问题有杨辉三角、阶乘计算(见本文第五小节)。下一节重点讨论一下递推关系。
那什么时候可以使用递归这样一种优雅的方式呢?
5.使用递归的情景
具有以下特征的问题可考虑递归求解:
- 当问题和子问题具有递推关系,比如计算阶乘(后文讨论)。
- 具有递归性质的数据结构。
- 反向性问题,比如取反。
总结下来,最根本的还是要抓住问题本身是否可以通过层层拆解到最小粒度来得解。
6.递归的经典例题
1.实现n的阶乘
实现n的阶乘也就是从1乘到n,怎么实现这个功能呢?让我们分析一下:
5!=1*2*3*4*5 ==> 5!=5*4!
4!=1*2*3*4 ==> 4! =4*3!
3! = 1*2*3 ==> 3! =3*2!
2! = 1*2 ==> 2! =2*1!
1! = 1
现在,我们就把这种问题理解清楚了,1!是最好算的,就是1,分到1后不可再分,而拆分后都是同样的计算方式,我们这时就可以用递归进行计算。
int Fact(int n)
{
if(n<=0)
return 1;
else
return n*Fact(n-1);
}
这里,我们就写出了递归的方式,值得注意的是:这里的条件,当不再分时,必须让函数返回1,这样乘的时候就是乘一了。
2.顺序打印整数的每一位
例如,当我输入1234,要求输出1 2 3 4,怎么写出代码呢?
我们先分两种情况,第一是当n是个位数时,那么输出的就是n本身
第二时当n是两位数及以上时,输出的不是n本身了,那应该怎么算 呢?
举个例子,当我们输入1234时,我们可以发现,当我们对1234进行模10操作时,会得到4,之后,我们在除10,就会得到123,以此类推,我们就会得出各位数,但是这里的顺序是反的,怎么使顺序正过来呢?
我们已经知道,1234%10为4,那么我们为何不直接将个位数打印出来,注意,这里很多人不理解为啥要怎么做,因为递归是先递后归,因此最先输出的是最内层的函数,并不是最外层,最后,这个函数就可以轻松写出了:
void Print(int n)
{
if(n>9)
{
Print(n/10);
}
printf("%d ", n%10);
}
这样就完成了。
3.斐波那契数列
斐波那契数列是递归中跳不出的一个应用方式。斐波那契数列数列又称兔子问题,C语言中的跳楼梯问题和兔子问题都可以归类为斐波那契数列问题,因此,在这里不再赘述。
什么是斐波那契数列?1,1,2,3,5,8,13,21,34.......
这样的数列就是斐波那契数列,不难看出,斐波那契数列第一项和第二项都是固定的,从第三项开始,每一项都是前两项的和,对于这种找规律的问题,大部分都可以用递归来解决。
#include<stdio.h>
int fib(int m)
{
if(m>=3)
{
return fib(m-1)+fib(m-2);
}
else{
return 1;
}
}
int main()
{
int n;
scanf("%d",&n);
printf("%d",fib(n));
return 0;
}
这就是斐波那契数列的具体代码了。
7.总结
现在,我们更加相信递归是一种强大的技术,它使我们能够以一种优雅而有效的方式解决许多问题。同时,它也不是解决任务问题的灵丹妙药。由于时间或空间的限制,并不是所有的问题都可以用递归来解决。递归本身可能会带来一些不希望看到的副作用,如栈溢出。
有时,在解决实际问题时乍一看,我们并不清楚是否可以应用递归算法来解决问题。然而,由于递归的递推性质与我们所熟悉的数学非常接近,用数学公式来推导某些关系总是有帮助的,也就是说写出递推关系和基本情况是使用递归算法的前置条件。