1、什么是递归
递归简单来说就是在运行过程中不断调用自己,直到碰到终止条件,返回结果的过程。
递归可以看作两个过程,分别是递和归。递就是原问题把要计算的结果传给子问题;归则是子问题求出结果后,把结果层层返回原问题的过程。
下面设一个需要经过三次递归的问题,为大家详细看一下递归的过程:
2、递归的三要素
递归的基本思想是某个函数直接或者间接地调用自身,这样原问题的求解就转换为了许多性质相同但是规模更小的子问题。求解时只需要关注如何把原问题划分成符合条件的子问题,而不需要过分关注这个子问题是如何被解决的。
递归有三大要素
第一要素:明确你这个函数想要干什么
对于递归,我觉得很重要的一个事就是,这个函数的功能是什么,他要完成什么样的一件事,而这个,是完全由你自己来定义的。也就是说,我们先不管函数里面的代码什么,而是要先明白,你这个函数是要用来干什么。
例如,我定义了一个函数
// 算 n 的阶乘(假设n不为0)
int f(int n){
}
这个函数的功能是算 n 的阶乘。好了,我们已经定义了一个函数,并且定义了它的功能是什么,接下来我们看第二要素。
第二要素:寻找递归结束条件
所谓递归,就是会在函数内部代码中,调用这个函数本身,所以,我们必须要找出递归的结束条件,不然的话,会一直调用自己,进入无底洞。也就是说,我们需要找出当参数为啥时,递归结束,之后直接把结果返回,请注意,这个时候我们必须能根据这个参数的值,能够直接知道函数的结果是什么。
例如,上面那个例子,当 n = 1 时,那你应该能够直接知道 f(n) 是啥吧?此时,f(1) = 1。完善我们函数内部的代码,把第二要素加进代码里面,如下
// 算 n 的阶乘(假设n不为0)
int f(int n){
if(n == 1){
return 1;
}
}
有人可能会说,当 n = 2 时,那我们可以直接知道 f(n) 等于多少啊,那我可以把 n = 2 作为递归的结束条件吗?
当然可以,只要你觉得参数是什么时,你能够直接知道函数的结果,那么你就可以把这个参数作为结束的条件,所以下面这段代码也是可以的。
// 算 n 的阶乘(假设n>=2)
int f(int n){
if(n == 2){
return 2;
}
}
注意我代码里面写的注释,假设 n >= 2,因为如果 n = 1时,会被漏掉,当 n <= 2时,f(n) = n,所以为了更加严谨,我们可以写成这样:
// 算 n 的阶乘(假设n不为0)
int f(int n){
if(n <= 2){
return n;
}
}
第三要素:找出函数的等价关系式
第三要素就是,我们要不断缩小参数的范围,缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变。
例如,f(n) 这个范围比较大,我们可以让 f(n) = n * f(n-1)。这样,范围就由 n 变成了 n-1 了,范围变小了,并且为了原函数f(n) 不变,我们需要让 f(n-1) 乘以 n。
说白了,就是要找到原函数的一个等价关系式,f(n) 的等价关系式为 n * f(n-1),即
f(n) = n * f(n-1)。
举个栗子,还是从阶乘来出发
假设我们用递归来算阶乘 f(n)
f = n =>
n === 1 ? 1
: n * f(n-1)
f 里面用到了 f,怎么理解呢?
很简单,把式子展开即可:
所谓的递归:
3、递归经典例题
N的阶乘
首先按照思路分析是否可以使用递归算法:
N!可以拆分为(N-1)!*N
(N-1)!与N!只有数字规模不同,求解思路相同
当N=1时,结果为1,递归终止
满足条件,可以递归:
public static int Factorial(int num)
{
if(num==1)
{
return num;
}
return num*Factorial(num-1);
}
而最后的return,便是第四步,缩小参数num的值,让递归进入下一层。
顺序打印:当输入一个整数的时,按照顺序依次打印每一位的值
令num不断除10,最后只剩下最高位时并打印。
而在输出时%10,是因为当最高位打印返回后,继续打印的数不一定是个位数,%10只保留个位。
public static void PrintNumber(int num)
{
if (num>10)
{
PrintNumber(num/10);
}
System.out.print(num%10+",");
}
斐波那契数列:有一个数列:1、1、2、3、5、8、13、21、34....
斐波那契数列的规律是N=(N-1)+(N-2),要保证返回值不为0,所以有两个终止条件。
public static int Fibonacci(int num)
{
if (num==1||num==2)
{
return 1;
}
return Fibonacci(num-1)+Fibonacci(num-2);
}
变式
已知 f(n)f(1)=1;
n=1f(2)=1;
n=2f(3)=1;
n=3f(n)=2f(n-1)+3f(n-2) +f(n-3) n>=4
public static int f(int n) {
if (n == 1 || n == 2 || n == 3) {
return 1;
}
return 2 * f(n - 1) + 3 * f(n - 2) + f(n - 3);
}
青蛙跳台阶问题:一只青蛙一次可以跳1级或2级台阶,求该青蛙跳N级的台阶总共有多少种跳法。
当N=1,1种跳法。
当N=2,2种跳法。
当N=3,青蛙第一次跳的时候,它只有两种选择:跳1级,剩两级台阶,即N=2,有2种跳法;跳2级,剩一级台阶,即N=1,1种跳法。 所以,N3的跳法=N2+N1。
所以,N4=N3+N2
public static int FrogJumpSteps(int num)
{
if (num==1)
{
return 1;
}
if (num==2)
{
return 2;
}
return FrogJumpSteps(num-1)+FrogJumpSteps(num-2);
}
类似的一步可跳1级,或跳3级
private static long climbWays(int n) {
if (n == 0) return 1;
if (n == 1) return 1;
if (n == 2) return 1;
return climbWays(n - 1) + climbWays(n - 3);
}
汉诺塔:大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
先让我们来理清思路
1.首先,题目的意思就是一个柱子A上有64的按大小顺序放置的圆片,要全部移动到另一根柱子C上,每次只能移动一个,可以借助柱子B,且小的不能在大的下面。求最少的移动次数。
2.然后,寻找其中的数学规律(数学归纳法)。
先让所有圆片从上到下按1到N排序。
当N=1时,只需从A->C,移动1次。
当N=2时,先让1号从A->B,再让2号从A->C,最后让1号从B->C,移动3次。
当N=3时,可以理解为先让1、2号从A->B,再让3号从A->C,最后让1、2号从B->C。因为由N=2可以得知,移动两片需要3次,而A->B,B->C共进行了两次,所以共有6次,再加上3号A->C的过程,要移动7次。
同理,当N=4时,共要移动2×7(N=3的移动次数)+1=15次。
所以,当N=N时,要移动2×(N-1)的移动次数+1次 (其实也可以看做2的N次方-1)
public static int TowerOfHanoi(int num)
{
if (num==1)
{
return 1;
}
return TowerOfHanoi(num-1)*2+1;
}