方法递归
一、递归的形式和特点
1、什么是方法递归?
- 方法直接调用自己或者间接调用自己的形式称为方法递归(recursion)。
- 递归作为一种算法在程序设计语言中广泛应用。
2、递归的形式
- 直接递归:方法自己调用自己。
- 间接递归:方法调用其他方法,其他方法又调回方法自己。
3、方法递归注意事项
-
递归如果没有控制好终止,会出现递归死循环,导致栈内存溢出现象。
package com.app.d2_recursion; /** 目标:学习递归的形式 */ public class RecursionDemo01 { public static void main(String[] args) { test(); } public static void test() { System.out.println("------test被执行了-------"); test(); // 方法递归:直接递归形式 } }
------test被执行了------- ------test被执行了------- ------test被执行了------- ------test被执行了------- ------test被执行了------- ... at com.app.d2_recursion.RecursionDemo01.test(RecursionDemo01.java:13) at com.app.d2_recursion.RecursionDemo01.test(RecursionDemo01.java:13) at com.app.d2_recursion.RecursionDemo01.test(RecursionDemo01.java:13) at com.app.d2_recursion.RecursionDemo01.test(RecursionDemo01.java:13) ... Process finished with exit code 1
package com.app.d2_recursion; /** 目标:学习递归的形式 */ public class RecursionDemo01 { public static void main(String[] args) { // test(); test2(); } public static void test() { System.out.println("------test被执行了-------"); test(); // 方法递归:直接递归形式 } public static void test2() { System.out.println("------test2被执行了-------"); test3(); // 方法递归:间接递归形式 } private static void test3() { System.out.println("------test3被执行了-------"); test2(); } }
------test2被执行了------- ------test3被执行了------- ------test2被执行了------- ------test3被执行了------- ------test2被执行了------- ------test3被执行了------- ... at com.app.d2_recursion.RecursionDemo01.test3(RecursionDemo01.java:24) at com.app.d2_recursion.RecursionDemo01.test2(RecursionDemo01.java:19) at com.app.d2_recursion.RecursionDemo01.test3(RecursionDemo01.java:24) at com.app.d2_recursion.RecursionDemo01.test2(RecursionDemo01.java:19) at com.app.d2_recursion.RecursionDemo01.test3(RecursionDemo01.java:24) at com.app.d2_recursion.RecursionDemo01.test2(RecursionDemo01.java:19) at com.app.d2_recursion.RecursionDemo01.test3(RecursionDemo01.java:24) ... Process finished with exit code 1
总结
1、什么是递归?
- 方法直接或间接自己调用自己的编程技巧称为递归(recursion)
2、什么是递归死循环?
- 递归的方法无限调用自己,无法终止,直到出现栈内存溢出。
二、递归的算法流程和核心要素
1、递归案例导学-计算 1-n 的阶乘
- 需求:
- 计算 1-n 的阶乘的结果,使用递归思想解决,我们从数学思维上理解递归的流程和核心点。
- 分析:
- 1、假如我们认为存在一个公式是:
f(n) = 1*2*3*4*5*6*7...(n-1)*n
; - 2、那么公式等价形式就是:
f(n) = f(n-1)*n
; - 3、如果求的是
1-5
的阶乘的结果,我们应如何应用上述公式计算?
- 1、假如我们认为存在一个公式是:
package com.app.d2_recursion;
/**
目标:理解递归的算法和执行流程
需求:
计算 1-n 的阶乘的结果,使用递归思想解决,我们从数学思维上理解递归的流程和核心点。
*/
public class RecursionDemo02 {
public static void main(String[] args) {
System.out.println(f(10)); // 计算1-10 的阶乘的结果
System.out.println(f(5)); // 计算1-5 的阶乘的结果
}
/**
递归算法方法
*/
public static int f(int n) {
if (n == 1) {
return 1;
}else {
return f(n - 1) * n;
}
}
}
3628800
120
Process finished with exit code 0
2、递归解决问题的思路:
- 把一个复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
3、递归算法三要素大体可以总结为:
-
递归的公式:f(n) = f(n-1)*n;
-
递归的终结点:f(1);
-
递归的方向必须走向终结点:
4、递归求阶乘的执行流程
- 方法的执行都是在栈内存中的
- 1、先是调用f()方法,将5传入方法中,接收到参数后,if开始判断,由于5等于1不成立,因此判断为false,走第二个if分支
- 2、套入公式:n * f(n - 1) ==> 5 * f(5 - 1) ==> 5 * f(4),但是结果还是不能确定,因此会停止
- 3、再次调用 f()方法,以此类推,直至走到终结点 f(1) = 1
-
结果已经确定了,因此方法会依次从栈内存中退出!
总结
1、递归算法三要素大体可以总结为什么?
- *递归的公式:f(n) = f(n - 1)n;
- 递归的终结点:f(1);
- 递归的方向必须走向终结点。
三、递归常见案例
递归案例-计算1-n的和
-
需求:
- 计算1-n的和,使用递归思想解决,我们先从数学思维上理解递归的流程和核心点。
-
分析:
-
1、假如我们认为存在一个公式是:
f(n) = 1+2+3+4+5+6+7 ...(n-1)+n
; -
2、那么公式等价形式就是:
f(n) = f(n-1)+n
; -
3、递归的终结点:
f(1) = 1
; -
4、如果求的是 1-5 的和,应如何应用上述公式计算?
- 先递伸,再归回,简称递归
-
package com.app.d3_recursion_test;
/**
目标:通过递归案例,更深入的掌握和理解递归的算法
需求:
计算1-n的和,使用递归思想解决,我们先从数学思维上理解递归的流程和核心点。
*/
public class Test01 {
public static void main(String[] args) {
int result1 = f(10);
int result2 = f(5);
System.out.println("1-10的和:" + result1);
System.out.println("1-5的和:" + result2);
}
/**
递归算法方法
*/
public static int f(int n) {
if (n == 1) {
return 1; // 递归的终结点(递归的最终走向)
}else {
return f(n - 1) + n; // 递归的公式
}
}
}
1-10的和:55
1-5的和:15
Process finished with exit code 0
四、递归的经典问题
案例-猴子吃桃问题
- 猴子第一天摘下若干桃子,当即吃了一半,觉得不过瘾,于是又多吃了一个;
- 第二天又吃了前天剩余桃子数量的一半,觉得不过瘾,于是又多吃了一个;
- 以后每天都是吃前天剩余桃子数量的一半,觉得不过瘾,又多吃了一个;
- 等到第10天的时候发现桃子只有1个了。
- 需求:
- 请问猴子第一天摘了多少个桃子?
- 分析:
- 1、整体来看,每一天都是做同一事件,典型的规律化问题,考虑递归三要素:
- 递归公式
- 递归终结点
- 递归方向
- 1、整体来看,每一天都是做同一事件,典型的规律化问题,考虑递归三要素:
package com.app.d3_recursion_test;
/**
目标:通过递归经典案例-猴子吃桃,更深入的理解和使用递归算法,并学会灵活转换递归算法公式
公式(合理的):
以后每天的桃子量 = 前天桃子量 - 前天桃子量的一半 - 多吃的一个桃子
f(n+1) = f(n) - f(n)/2 - 1
公式变形后才方便使用:
变形前:f(n+1) = f(n) - f(n)/2 - 1
变形1:2f(n+1) = 2f(n) - f(n) - 2 // 两边都乘以2
变形2:2f(n+1) + 2 = f(n) // 移项
变形3:f(n) = 2f(n+1) + 2 // 换边
求猴子第一天摘了多少个桃子?
f(1) = ?
终结点:第10天只剩1个桃子 ==> f(10) = 1
*/
public class Test02 {
public static void main(String[] args) {
int oneDay = f(1); // 求第一天猴子摘了多少个桃子
System.out.println("猴子第一天摘了 " + oneDay + " 个桃子~~");
System.out.println("第二天剩余的桃子量:" + (f(2)));
System.out.println("第三天剩余的桃子量:" + (f(3)));
System.out.println("第四天剩余的桃子量:" + (f(4)));
System.out.println("第五天剩余的桃子量:" + (f(5)));
System.out.println("第六天剩余的桃子量:" + (f(6)));
System.out.println("第七天剩余的桃子量:" + (f(7)));
System.out.println("第八天剩余的桃子量:" + (f(8)));
System.out.println("第九天剩余的桃子量:" + (f(9)));
System.out.println("第十天剩余的桃子量:" + (f(10)));
}
/**
递归算法的方法
*/
public static int f(int n) {
if (n == 10) {
return 1; // 递归的终结点:如果传入的参数为10,说明到了第10天,桃子只剩下1个
}else {
return 2 * f(n + 1) + 2; // 递归的公式
}
}
}
猴子第一天摘了 1534 个桃子~~
第二天剩余的桃子量:766
第三天剩余的桃子量:382
第四天剩余的桃子量:190
第五天剩余的桃子量:94
第六天剩余的桃子量:46
第七天剩余的桃子量:22
第八天剩余的桃子量:10
第九天剩余的桃子量:4
第十天剩余的桃子量:1
Process finished with exit code 0
五、非规律化递归案例
1、思考
1、在上述的案例中递归算法都是针对存在规律化的递归问题。
2、有很多问题是非规律化的递归问题,比如文件搜索。如何解决呢?
- 非规律化递归问题自己看着办,需要流程化的编程思维。
2、案例-文件搜索
- 需求:
- 文件搜索、从
D盘
中,搜索出Typora.exe
并输出绝对路径。
- 文件搜索、从
- 分析:
- 1、先定位出一级文件对象
- 2、遍历全部一级文件对象,判断是否是文件
- 3、如果是文件,判断是否是自己想要的
- 4、如果是文件夹,需要继续递归进去重复上述过程
package com.app.d3_recursion_test;
import java.io.File;
import java.io.IOException;
/**
目标:通过案例-文件搜索,学会使用流程化的编程思维来解决非规律化的递归问题
需求:
文件搜索:从 D盘 中,搜索出 Typora.exe 并输出绝对路径。
*/
public class Test03 {
public static void main(String[] args) {
// 2、传入要搜索的一级文件对象 和 文件名称
searchFile(new File("D:/"), "Typora.exe");
}
/**
* 1、文件搜索功能
* @param dir 被搜索的目录(文件夹)
* @param fileName 被搜索的文件名称
*/
public static void searchFile(File dir, String fileName) {
// 3、判断该文件对象是否为空,并且是否为目录(文件夹)
if (dir != null && dir.isDirectory()) {
// 可以开始找了!!
// 4、获取当前目录下的一级文件对象
File[] files = dir.listFiles(); // 可能是:null、[]
// 5、判断是否存在一级文件对象
if (files != null && files.length > 0) {
// 存在
// 6、开始遍历一级文件对象
for (File file : files) {
// 7、判断当前遍历的一级文件对象是文件 还是 目录(文件夹)
if (file.isFile()) {
// 是文件
// 8、判断是不是我们要找的文件,是就把该文件的绝对路径输出
if (file.getName().contains(fileName)) {
System.out.println("找到了:" + file.getAbsolutePath());
// 找到后启动该程序:只能启动exe后缀的应用程序
/*try {
Runtime r = Runtime.getRuntime();
r.exec(file.getAbsolutePath());
} catch (IOException e) {
e.printStackTrace();
}*/
}
}else {
// 是目录
// 9、需要继续递归寻找
searchFile(file, fileName);
}
}
}
}else {
System.out.println("sorry!您当前搜索的位置不是目录!");
}
}
}
找到了:D:\常用软件\typroa文本编辑器\Typora\Typora.exe
Process finished with exit code 0
3、案例-啤酒问题
- 需求:
- 啤酒2元1瓶,4个盖子可以换一瓶,2个空瓶可以换一瓶。
- 请问10元钱可以喝多少瓶啤酒,剩余多少空瓶和盖子?
- 答案:15瓶,3盖子,1空瓶子
package com.app.d3_recursion_test;
/**
目标:通过案例-啤酒问题,更深入的使用流程化编程思维来解决非规律化递归问题
需求:
啤酒2元1瓶,4个盖子可以换一瓶,2个空瓶可以换一瓶。
请问10元钱可以喝多少瓶啤酒,剩余多少空瓶和盖子?
答案:15瓶,3盖子,1空瓶子
*/
public class Test04 {
// 定义静态变量分别用于存储买的酒的数量、每次剩余的瓶子、盖子的个数
public static int totalNumber; // 总数量
public static int lastBottleNumber; // 记录每次剩余的瓶子个数
public static int lastCoverNumber; // 记录每次剩余的盖子个数
public static void main(String[] args) {
// 1、拿钱买酒
buy(10);
System.out.println("10元钱可以喝 " + totalNumber + " 瓶啤酒~");
System.out.println("剩余盖子 " + lastCoverNumber + " 个~");
System.out.println("剩余空瓶 " + lastBottleNumber + " 个~");
}
/**
* 2、买啤酒功能
* @param money // 买啤酒的钱
*/
public static void buy(int money) {
// 3、计算10元钱可以买多少瓶啤酒
int buyNumber = money / 2; // 5瓶
totalNumber += buyNumber;
// 4、把空酒瓶 和 盖子换算成钱
// 统计本轮总的盖子数 和 瓶子数
int coverNumber = lastCoverNumber + buyNumber;
int bottleNumber = lastBottleNumber + buyNumber;
// 5、统计可以换算的钱
int allMoney = 0;
// 6、当盖子数大于等于4的时候,可以换一瓶啤酒
if (coverNumber >= 4) {
allMoney += (coverNumber / 4) * 2; // 假如盖子数为5,5除以4 == 1,1乘以2 == 2元,可以买一瓶酒
}
lastCoverNumber = coverNumber % 4; // 假如盖子数为5,5取余4 == 1,这里的1就是剩余的盖子数,留作下一轮累加盖子数
// 7、当空酒瓶大于等于2的时候,可以换一瓶啤酒
if (bottleNumber >= 2) {
allMoney += (bottleNumber / 2) * 2; // 假如空酒瓶数为5,5除以2 == 2,2乘以2 == 4元,可以买两瓶酒
}
lastBottleNumber = bottleNumber % 2; // 假如空酒瓶数为5,5取余2 == 1,这里的1就是剩余的空酒瓶数,留作下一轮累加空酒瓶数
// 8、判断换算后的钱是否大于等于2元钱
if (allMoney >= 2) {
// 够2元钱了,可以买一瓶啤酒了~
buy(allMoney);
}
}
}
10元钱可以喝 15 瓶啤酒~
剩余盖子 3 个~
剩余空瓶 1 个~
Process finished with exit code 0