一、什么是递归
在初学编程时,递归是一个比较难理解、难接受的问题。递归的执行方式和人的思考问题的方式不太一样,相对于普通的选择结构、循环结构,递归似乎给人一种把一个未知数变成了更多的未知数的感觉。但实际上,一旦理解并接受了递归解决问题的思路,普通的递归问题就会变得很容易。
为了文章的完整性,首先说一下什么是递归。
递归(recursion)在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。绝大多数编程语言支持函数的自调用,在这些语言中函数可以通过调用自身来进行递归。计算理论可以证明递归的作用可以完全取代循环,因此有很多在函数编程语言(如Scheme)中用递归来取代循环的例子。——wikipedia
一个方法(函数)自己调用自己就构成了递归:
public void recursion(){ recursion(); //自己调用自己,就叫递归 }
以上是一个简单的JAVA递归,但是由于没有设定中止条件,这个方法会无限调用自身,导致栈溢出。
二、递归的用途
递归主要用来解决以下三类问题:
1、数据的定义是按递归定义的。如Fibonacci函数。
2、问题解法按递归算法实现。如Hanoi问题。
3、数据的结构形式是按递归定义的。如二叉树、广义表等。——wikipedia
斐波那契数列是理解递归问题的经典例子,它的每一项都等于前两项之和,特别的,第1项为1,为方便定义,加上一个第“0”项是0。用公式表示就是:
F0 = 0
F1 = 1
Fn = F(n-1) + F(n-2) (n≥2)
当要求第n项的时候,有两种思路:
第一种,从第2项开始求和,把第2、3、4、……、(n-2)、(n-1)项依次求出来,然后把第n-1 和 n-2项加起来得到第n项。
这种思路的特点是:从已知项出发,逐步向未知的方向推导,最终得出所求。这是我们解决问题的正常思路。
第二种,直接从第n项开始。我们所求的就是第n项,而第n项又有公式表示,我们把它一点一点展开,最终必然会展开到第1项和第0项。这就是递归的思路,根据这个思路,要求第n项,只需要求第n-1 和 n-2项,然后第n-1 和 n-2项又可以分解为第n-2 和 n-3项,第n-3和 n-4项,最终展开到第1项和第0项。
这种思路的特点是:从未知项出发,逐步向已知项展开。这就是递归的思路。
public class Fibo { public static void main(String[] args) { System.out.println(fibo1(0)); pintFibo1(20); pintFiboN(21); } // 思路一:从第0项、第1项开始加,一直加到第n项 public static int fibo1(int n) { // f0 f1 f2分别表示:第3k+0;3k+1,3k+2项,k为自然数 // n被3除,余0就是f0,余1就是f1,余2就是f2 int f0 = 0; int f1 = 1; int f2 = f0 + f1; int f = 0;// 表示第n项数列的值。 if (n <= 1) { return n; } for (int i = 2; i <= n;) { f2 = f0 + f1; if (i++ == n) { f = f2; break; } f0 = f1 + f2; if (i++ == n) { f = f0; break; } f1 = f0 + f2; if (i++ == n) { f = f1; break; } } return f; } // 打印出利用第2项开始求和的方法求出的斐波那契数列 public static void pintFibo1(int n) { System.out.println("从第2项开始求前两项的和,直到第n项:"); for (int i = 1; i <= n; i++) { System.out.print(fibo1(i) + " "); } System.out.println(); } // 思路二:从第N项开始展开。利用递归的方法求第n项斐波那契数。 public static int fiboN(int n) { if (n >= 2) { return fiboN(n - 1) + fiboN(n - 2);// 把第n项,用第(n - 1)、(n - 2)项表示 } else if (n == 0 || n == 1) { return n; // 一直展开到第n=1,n=0 } else { return n; } } // 打印出利用递归的方法求出的斐波那契数列 public static void pintFiboN(int n) { System.out.println("用递归求第n项: "); for (int i = 1; i <= n; i++) { System.out.print(fiboN(i) + " "); } System.out.println(); } }
显然,使用递归的方式求斐波那契数列,比一步一步求和简单得多。
三、常见递归问题的简单理解
1.数列题
已知数列的某种规律,求第n项的值
这种题目一般是给出数列的通项公式,以及某具体项的具体值。
例题:
已知一个数列:
f(20) = 6765,
f(21) = 10946,
f(n) = f(n-1)+f(n-2),其中n是自然数。
求f(0),f(1)的值。
分析:在斐波那契数列中,我们已知f(0),f(1),求f(n),即“小项(0项,1项)”已知,“大项(n项)”未知,是从大项向小项递归。本题中,大项(20,21)已知,求小项(0,1),我们需要从小项向大项递归。但实际上它们的本质都是一样的,都是从未知向已知递归。
解题步骤:
①求出通向公式,return这个公式
本题中,大项(20,21)已知,求小项(0,1),而通向公式是f(n) = f(n-1)+f(n-2),只能已知小项求大项,因此需要变换一下:
移项:f(n-2) = f(n)-f(n-1)
参数同时+2:f(n) = f(n+2)-f(n+1)
这样我们就可以从大项向小项递归了。
代码如下
if (n < 20) { return f(n + 2) - f(n + 1); // 抄转换后的公式 }
②return已知项
if (n == 20) { // 抄已知条件 return 6765; } if (n == 21) { // 抄已知条件 return 10946; }
到这里就基本写完了,完整代码如下:
public static int f(int n) { if (n < 20) return f(n + 2) - f(n + 1); // 抄转换后的公式 else if (n == 20) // 抄已知条件 return 6765; else if (n == 21) // 抄已知条件 return 10946; else return 0; // 这个无实际意义,为了保证有返回值,实际上不会执行到 }
求得f(0)=0,f(1)=1。实际上由题目的通项公式f(n) = f(n-1)+f(n-2)可知,本质上就是斐波那契数列。
总结数列递归题的解法:
Ⅰ.根据通向公式,表示出f(n) 的公式。切记由未知向已知递归,确保最终能展开到已知项。
Ⅱ.写出分支语句,根据n的取值,返回 f(n) 的公式,已知项。直接抄即可。
实际上用递归方法解决数列问题,主要就是“抄”:
public static int fibo(int n) { if (n >= 2) return fibo(n - 1) + fibo(n - 2); if (n == 0 || n == 1) return n; } //求斐波那契数列的代码,都是抄的题目已知条件
2.汉诺塔
汉诺塔问题的关键是:第n个盘子的移动,可以在前n-1个盘子移动的基础上进行,即把前n-1个盘子看作一个整体。
然后前n-1个盘子,再看作前n-2个盘子的整体和第n-1个盘子
最终递归到两个盘子和一个盘子的情况。
用递归解决问题的关键,就是找出第n步和前n-1步的关系。
四、递归正确性的验证与数学归纳法
待补充