递归(英语:recursion)在电脑科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。
0、前言
递归其实和循环是非常像的,循环都可以改写成递归,递归未必能改写成循环,这是一个充分不必要的条件。
我们初学编程的时候肯定会做过类似的练习:
1+2+3+4+....+100(n)
求和我们要记住的是,想要用递归必须知道 两个条件:
技巧:在递归中常常是将问题切割成两个部分(1和整体的思想),这能够让我们快速找到递归表达式(规律)
一、求和
如果我们使用for
循环来进行求和1+2+3+4+....+100
,那是很简单的:
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum = sum + i;
}
System.out.println("公众号:Java3y:" + sum);
前面我说了,for循环都可以使用递归来进行改写,而使用递归必须要知道两个条件:1、递归出口,2、递归表达式(规律)
首先,我们来找出它的规律:1+2+3+...+n
,这是一个求和的运算,那么我们可以假设X=1+2+3+...+n
,可以将1+2+3+...+(n-1)
看成是一个整体。而这个整体做的事又和我们的初始目的(求和)相同。以我们的高中数学知识我们又可以将上面的式子看成X=sum(n-1)+n
好了,我们找到我们的递归表达式(规律),它就是sum(n-1)+n
,那递归出口呢,这个题目的递归出口就有很多了,我列举一下:
n=1
时,那么就返回1
n=2
时,那么就返回3
(1+2)n=3
时,那么就返回6
(1+2+3)当然了,我肯定是使用一个最简单的递归出口了:if(n=1) return 1
递归表达式和递归出口我们都找到了,下面就代码演示:
递归出口为1:
public static void main(String[] args) {
System.out.println("公众号:Java3y:" + sum(100));
}
/**
*
* @param n 要加到的数字,比如题目的100
* @return
*/
public static int sum(int n) {
if (n == 1) {
return 1;
} else {
return sum(n - 1) + n;
}
}
递归出口为4:
public static void main(String[] args) {
System.out.println("公众号:Java3y:" + sum(100));
}
/**
*
* @param n 要加到的数字,比如题目的100
* @return
*/
public static int sum(int n) {
//如果递归出口为4,(1+2+3+4)
if (n == 4) {
return 10;
} else {
return sum(n - 1) + n;
}
}
结果都是一样的。
二、数组内部的最大值
如果使用的是循环,那么我们通常这样实现:
int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2};
//将数组的第一个假设是最大值
int max = arrays[0];
for (int i = 1; i < arrays.length; i++) {
if (arrays[i] > max) {
max = arrays[i];
}
}
System.out.println("公众号:Java3y:" + max);
那如果我们用递归的话,那怎么用弄呢?首先还是先要找到递归表达式(规律)和递归出口
2
与数组后面的数->{3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2}
进行切割,将数组后面的数看成是一个整体X={3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2}
,那么我们就可以看成是第一个数和一个整体进行比较if(2>X) return 2 else(2
2
进行比较。找出整体的最大值又是和我们的初始目的(找出最大值)是一样的if( 2>findMax() )return 2 else return findMax()
使用到数组的时候,我们通常为数组设定左边界和右边界,这样比较好地进行切割
arrays.length-1
(长度-1在角标中才是代表最后一个元素)那么可以看看我们递归的写法了:
public static void main(String[] args) {
int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 1};
System.out.println("公众号:Java3y:" + findMax(arrays, 0, arrays.length - 1));
}
/**
* 递归,找出数组最大的值
* @param arrays 数组
* @param L 左边界,第一个数
* @param R 右边界,数组的长度
* @return
*/
public static int findMax(int[] arrays, int L, int R) {
//如果该数组只有一个数,那么最大的就是该数组第一个值了
if (L == R) {
return arrays[L];
} else {
int a = arrays[L];
int b = findMax(arrays, L + 1, R);//找出整体的最大值
if (a > b) {
return a;
} else {
return b;
}
}
}
三、冒泡排序递归写法
在冒泡排序章节中给出了C语言的递归实现冒泡排序,那么现在我们已经使用递归的基本思路了,我们使用Java来重写一下看看:
冒泡排序:俩俩交换,在第一趟排序中能够将最大值排到最后面,外层循环控制排序趟数,内层循环控制比较次数
以递归的思想来进行改造:
L==R
public static void main(String[] args) {
int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 1};
bubbleSort(arrays, 0, arrays.length - 1);
System.out.println("公众号:Java3y:" + arrays);
}
public static void bubbleSort(int[] arrays, int L, int R) {
int temp;
//如果只有一个元素了,那什么都不用干
if (L == R) ;
else {
for (int i = L; i < R; i++) {
if (arrays[i] > arrays[i + 1]) {
temp = arrays[i];
arrays[i] = arrays[i + 1];
arrays[i + 1] = temp;
}
}
//第一趟排序后已经将最大值放到数组最后面了
//接下来是排序"整体"的数据了
bubbleSort(arrays, L, R - 1);
}
}
四、斐波那契数列
接触过C语言的同学很可能就知道什么是费波纳切数列了,因为往往做练习题的时候它就会出现,它也是递归的典型应用。
菲波那切数列长这个样子:{1 1 2 3 5 8 13 21 34 55..... n }
数学好的同学可能很容易就找到规律了:前两项之和等于第三项
例如:
1 + 1 = 2
2 + 3 = 5
13 + 21 = 34
如果让我们求出第n项是多少,那么我们就可以很简单写出对应的递归表达式了:Z = (n-2) + (n-1)
递归出口在本题目是需要有两个的,因为它是前两项加起来才得出第三项的值
同样地,那么我们的递归出口可以写成这样:if(n==1) retrun 1 if(n==2) return 2
下面就来看一下完整的代码吧:
public static void main(String[] args) {
int[] arrays = {1, 1, 2, 3, 5, 8, 13, 21};
//bubbleSort(arrays, 0, arrays.length - 1);
int fibonacci = fibonacci(10);
System.out.println("公众号:Java3y:" + fibonacci);
}
public static int fibonacci(int n) {
if (n == 1) {
return 1;
} else if (n == 2) {
return 1;
} else {
return (fibonacci(n - 1) + fibonacci(n - 2));
}
}
五、汉诺塔算法
图片来源百度百科:
玩汉诺塔的规则很简单:
我们下面就来玩一下:
.......................
从前三次玩法中我们就可以发现的规律:
简单来说就分成三步:
那么就可以写代码测试一下了(回看上面玩的过程):
public static void main(String[] args) {
int[] arrays = {1, 1, 2, 3, 5, 8, 13, 21};
//bubbleSort(arrays, 0, arrays.length - 1);
//int fibonacci = fibonacci(10);
hanoi(3, 'A', 'B', 'C');
System.out.println("公众号:Java3y" );
}
/**
* 汉诺塔
* @param n n个盘子
* @param start 起始柱子
* @param transfer 中转柱子
* @param target 目标柱子
*/
public static void hanoi(int n, char start, char transfer, char target) {
//只有一个盘子,直接搬到目标柱子
if (n == 1) {
System.out.println(start + "---->" + target);
} else {
//起始柱子借助目标柱子将盘子都移动到中转柱子中(除了最大的盘子)
hanoi(n - 1, start, target, transfer);
System.out.println(start + "---->" + target);
//中转柱子借助起始柱子将盘子都移动到目标柱子中
hanoi(n - 1, transfer, start, target);
}
}
我们来测试一下看写得对不对:
参考资料:
六、总结
递归的确是一个比较难理解的东西,好几次都把我绕进去了....
要使用递归首先要知道两件事:
在递归中常常用”整体“的思想,在汉诺塔例子中也不例外:将最大盘的盘子看成1,上面的盘子看成一个整体。那么我们在演算的时候就很清晰了:将”整体“搬到B柱子,将最大的盘子搬到C柱子,最后将B柱子的盘子搬到C柱子中
因为我们人脑无法演算那么多的步骤,递归是用计算机来干的,只要我们找到了递归表达式和递归出口就要相信计算机能帮我们搞掂。
在编程语言中,递归的本质是方法自己调用自己,只是参数不一样罢了。
最后,我们来看一下如果是5个盘子,要运多少次才能运完:
计算阶乘
计算阶乘是递归程序设计的一个经典示例。计算某个数的阶乘就是用那个数去乘包括 1 在内的所有比它小的数。例如,factorial(5) 等价于 5*4*3*2*1,而 factorial(3) 等价于 3*2*1。
阶乘的一个有趣特性是,某个数的阶乘等于起始数(starting number)乘以比它小一的数的阶乘。例如,factorial(5) 与 5 * factorial(4) 相同。您很可能会像这样编写阶乘函数:
不过,这个函数的问题是,它会永远运行下去,因为它没有终止的地方。函数会连续不断地调用 factorial。 当计算到零时,没有条件来停止它,所以它会继续调用零和负数的阶乘。因此,我们的函数需要一个条件,告诉它何时停止。
由于小于 1 的数的阶乘没有任何意义,所以我们在计算到数字 1 的时候停止,并返回 1 的阶乘(即 1)。因此,真正的递归函数类似于:
可见,只要初始值大于零,这个函数就能够终止。停止的位置称为 基线条件(base case)。基线条件是递归程序的 最底层位置,在此位置时没有必要再进行操作,可以直接返回一个结果。所有递归程序都必须至少拥有一个基线条件,而且 必须确保它们最终会达到某个基线条件;否则,程序将永远运行下去,直到程序缺少内存或者栈空间。
下面这个求 n! 的例子中,递归出口(确定递归什么时候结束)是fun(1)=1,递归体(确定递归求解时的递归关系)是fun(n)=n*fun(n-1),n>1。
求n! 的递归思路就是把 fun(n)=n! 转化成 fun(n-1) ,再把 fun(n-1) 分解为 f(n-2) 来解决,依此类推直到每个问题都可以直接解决(分解到 fun(1) )。