记得最开始接触到递归,是在大一学c语言时候接触到的,教材上讲的最简单的递归就是求阶乘,f(n) = n * f(n-1),很简单的一句话,但是当时我还是花了很多时间研究代码的执行过程,懂了以后,碰见后面的汉诺塔则完全崩溃了,代码的执行过程完全理不清,因此,最初看递归代码或者写递归代码,一定是从宏观入手,所以一定要知道宏观的思路:
我是老总,负责创建策略(递归方程),并且使用该策略完成第一步任务,然后将剩余任务给我手下,他使用我创造的策略完成第二步任务(与第一步任务的不同仅在于传递的实参不同),然后我手下再将他的剩余任务给了他的手下,他手下使用我创造的策略完成第三步任务(依然是实参不同而已)...... 以此一直往下分配,直到最后一步被完成(设置的递归出口)。
上面一段话很容易理解,简单而言,递归就是 递出去,归回来。
要练习递归,可以从改变for循环开始,因为只要是循环问题一般都可以转化为递归实现,例如输出9-0(至于输出0-9则会在下面的回溯中讲到):
for(int i=9; i>=0; --i)
System.out.println(i);
先想策略:编写输出9-0,我只负责输出当前的数,其余的数由手下完成,过程如下:
1、框架:负责打印n-0
f(int n){
}
2、我负责的部分是输出当前的n:
f(int n){
System.out.println(n);
}
3、手下负责输出(n-1)-0:
f(int n){
System.out.println(n);
f(n-1);
}
4、递归出口,当输完0后不再继续往下分配:
f(int n){
if(n<0) return;
System.out.println(n);
f(n-1);
}
完成了,这是最简单的for循环改变递归,。
递归代码的形参很重要,如果感觉当前的形参数目不能解决问题,可以增加相应的参数,例如下面求数组的和:
private static int f(int[] a) {
}
如果单纯使用数组做形参是没办法计算的,因为不知道当前计算到了数组的哪个位置,所以应该再加一个参数代表数组的当前位置:
private static int f(int[] a, int k) {
return a[k] + f1(a, k + 1);
}
上面的策略中,我负责将当前位置的值与手下计算的数组后面值的和相加,至于递归出口就很简单了,如下:
private static int f(int[] a, int k) {
if (k == a.length)
return 0;
return a[k] + f1(a, k + 1);
}
所以,为了练习递归,应该把你见到的for循环,都尽可能的改为递归,这样才能提高对递归的理解。
对递归有了一定的感觉,就可以对他的执行过程研究研究了,其实他就是利用栈来实现的,首先将当前代码的运行环境(变量值,参数值,等)压栈,然后又调用自身,又压栈,最后到了递归出口,然后依次弹出栈,并将结果返回,因此,递归注重的往往是结果,而循环则注重过程,循环条件也必须明确,所以,有时候递归能解决的问题,循环不一定能解决。
如果想在脑海里构建递归模型,可以使用下面的模型,最内层执行完后,就退出,然后回到上一层,上一层的所有变量值还是原来的值,这点需要注意。
f(,n,)
{
f(,n-1,)
{
f(,n-2,)
{
.
.
.
f(,0,)
{
}
}
}
}
经常与递归一块出现的一个词是回溯,也很简单,就是在上面的模型里面每一个f执行完后添加几行代码,这几行代码就是回溯的代码,如下黄色部分代码(不用管代码意思)所处的位置就是回溯的代码,它一定是每一个f执行完都要执行的代码:
f(,n,)
{
f(,n-1,)
{
f(,n-2,)
{
f(,0,)
{
.
.
.
}
System.out.println();
}
System.out.println();
}
System.out.println();
}
上面第一个例子是输出9-0,下面输出0-9则要用到回溯:
public static void f(int n) {
if(n<0) return;
f(n-1);
System.out.println(n); //回溯
}
上面的策略是先递归调用f,在递到出口时,开始归,但每次归的时候会执行回溯代码,因此会输出0-9;
递归是需要练习的,经过大量的练习,就会掌握它,它是很强大的,而且用的多了就会感觉它的代码很简洁,尤其学到二叉树,处处是递归,因为二叉树本身就是一个递归概念,所以一定要掌握递归,因为它并不难。
下面有几个例子供学习,从f2到f9难度递增:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
// boolean t = f2("abc", "abcd");
// System.out.println(t);
//
// int n = f3(5, 2);
// System.out.println(n);
//
// int a[] = { 2, 1, 3 };
// f4(a, 0);
//
// int n = f5("aabcd", "aac");
// System.out.println(n);
//
// String s = f6("abcd");
// System.out.println(s);
//
// int n = f7(4, 3);
// System.out.println(n);
//
// int n = f8(4, 1);
// System.out.println(n);
//
int a[] = new int[6];
f9(3, a, 0);
}
// 将n分解为所有可能的加和,如3=2+1, 3=1+1+1
private static void f9(int n, int[] a, int k) {
if (n == 0) {
for (int i = 0; i < k; ++i) {
System.out.print(a[i] + " ");
}
System.out.println();
}
for (int i = n; i > 0; i--) {
if (k > 0 && i > a[k - 1]) {
continue;
}
a[k] = i;
f9(n - i, a, k + 1);
}
}
// 计算m个A,n个B共有几种排列顺序
private static int f8(int m, int n) {
if (m == 0 || n == 0) {
return 1;
}
return f8(m - 1, n) + f8(m, n - 1);
}
// 计算杨辉三角形第m行n列的数
private static int f7(int m, int n) {
if (n == 0) {
return 1;
}
if (m == n) {
return 1;
}
return f7(m - 1, n - 1) + f7(m - 1, n);
}
// 返回s串的倒串
private static String f6(String s) {
if (s.length() == 0) {
return "";
}
return f6(s.substring(1)) + s.charAt(0);
}
// 最大公共子序列
private static int f5(String s1, String s2) {
if (s1.length() == 0 || s2.length() == 0) {
return 0;
}
if (s1.charAt(0) == s2.charAt(0)) {
return f5(s1.substring(1), s2.substring(1)) + 1;
}
return Math.max(f5(s1.substring(1), s2), f5(s1, s2.substring(1)));
}
// 对数组a进行全排列
private static void f4(int[] a, int k) {
if (k == a.length - 1) {
System.out.println(Arrays.toString(a));
}
for (int i = k; i < a.length; ++i) {
int t = a[k];
a[k] = a[i];
a[i] = t;
f4(a, k + 1);
t = a[k];
a[k] = a[i];
a[i] = t;
}
}
// n个球取m个球,不放回的取法
private static int f3(int n, int m) {
if (n < m) {
return 0;
}
if (n == m) {
return 1;
}
if (m == 0) {
return 1;
}
return f3(n - 1, m - 1) + f3(n - 1, m);
}
// 比较两个串是否相等
private static boolean f2(String s1, String s2) {
if (s1.length() != s2.length()) {
return false;
}
if (s1.length() == 0) {
return true;
}
if (s1.charAt(0) != s2.charAt(0)) {
return false;
}
return f2(s1.substring(1), s2.substring(1));
}
}