递归
一、预备知识
1、栈内存
关于栈内存,我们需要了解栈内存管理的细节:
(1)、栈内存分配的基本单位——栈帧
a、之前讲过,局部变量(方法的形式参数和方法中定义的变量)存储在栈空间中;
b、一个方法,当它被调用执行的时候,方法中的局部变量等,才需要在栈空间上存储,当调用结束,立即释放栈空间上占用的内存;
c、即每一个运行中的方法,都需要占用栈内存中的一片内存空间;
d、于是,每一个运行的方法,都会在栈上分配一片,只属于该运行中的方法的内存空间——栈帧(Stack Free)
(2)、栈中内存的基本单位,它的生命周期(何时分配,合适回收)
a、当一个方法被调用执行的时候,给该运行中的方法,分配栈帧;
b、当一个方法执行完毕的时候,它对应的栈帧被回收(销毁释放)
注意:一个方法可以被多次调用,每一次方法的调用(即每一次方法的运行),都会给它分配一个栈帧。(一个方法可以对应多个栈帧,被多次调用)
public class RecursionDemo{
public static void main(String[] args){
//调用递归方法
function(); //报错:StackOverflowError
}
/*
不带递归出口的递归方法(错误的递归)
*/
public static void function(){
//自己调用自己
function();
}
StackOverflowError产生的原因:
1、function方法,无限的自己调用自己,每次调用执行,都需要给该方法分配栈帧。
2、但是栈内存大小有限。
3、在function方法无限调用自己过程中,每一次方法的执行,都没纸箱完毕(都在调用过程中)
4、最终,导致栈内存会没有可用内存空间,此时如果,我在向栈申请创建栈帧就会出现StackOverflowError错误
Stackoverflow —— 一个很有名、很活跃的程序员问答平台
2、栈的内存空间
栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆区 (heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域 - 程序结束后有系统释放
文字常量区 :常量字符串就是放在这里的。 程序结束后由系统释放
程序代码区:存放函数体的二进制代码
关于栈,我们姑且先了解以上内容
更多关于栈的详细知识可参见以下博文:
https://www.cnblogs.com/dwlsxj/p/Stack.html
https://www.cnblogs.com/George1994/p/6399895.html
二、递归
1、递归方法的定义
方法定义中调用方法本身的现象
2、实现递归需要注意的问题
(1)、递归一定要有出口!!(递归需要有出口条件,在满足某种特殊条件的情况下,停止递归调用)
(2)、次数不能太多,否则就会出现Stack overflow
public class RecursionDemo1{
public static void main(String[] args){
//调用递归方法
function(); //StackOverflowError
}
/*
不带递归出口的递归方法(错误的递归)
*/
public static void function(){
//自己调用自己
function();
}
/*
带递归出口的递归方法
*/
public static void recursion(int n){
//递归出口
if(n <= 0){
//当达到递归出口,不再自己调用自己
System.out.println("n <= 0, n = " + n);
return;
}
System.out.println("n > 0, n = " + n);
//当未达到递归出口条件时
n--;
recursion(n);
}
}
汉诺塔问题:
/*
1.有三根杆子A,B,C。A杆上有 N 个 (N>1) 穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至 C 杆:
a.每次只能移动一个圆盘。
b.大盘不能叠在小盘上面。
N在原始的汉诺塔问题中64,如果要完成64个圆盘的搬运,需要的很长很长时间。
问:最少要移动多少次?如何移?
解决思路以N个圆盘为例:
1. 当我们要解决N个圆盘的搬运问题,对于N个圆盘,我们可能无法一次性得到结果,
于是,我们把N个圆盘的搬运问题, -> 最大一个的圆盘的搬运 & 最大圆盘上面的 n - 1
2. 对于规模为1那个待搬运的最大的圆盘,直接就知道如何搬运(一步搞定)
3. 在2的基础上,只需要,再解决 N - 1个圆盘搬运的问题
总结一下,汉诺塔问题,解决思路(递归算法的核心思想):
分而治之: 把一个复杂的大规模的问题,分解成若干相似的小规模的子问题,当问题规模,
足够小的是时候,我们就可以直接得到小规模问题的解,再把小规模问题的解,
组合起来,——> 大规模问题提的解
*/
public class RecursionDemo2{
public static void main(String[] args){
// 测试我们的hanoi的代码
hanoi('A', 'C', 'B', 4);
// 测试计数方法
long result = count(4);
System.out.println(result);
}
/*
计数对于n个圆盘的搬运,一共需要搬运多少次
count(n) 该方法的返回值,n个盘搬运的总次数
*/
public static long count(int n) {
// 递归出口
if (n == 1) {
return 1;
}
//1,hanoi(start, middle, end, n - 1);
long toMiddle = count(n - 1);
//2. 对于最大的那个盘,需要搬运一次,从start -> end
//3. hanoi(middle, end, start, n - 1);
long toEnd = count(n - 1);
// 2* count(n - 1) + 1
return toMiddle + 1 + toEnd;
}
/**
* 用该方法来模拟解决汉诺塔问题中圆盘的搬运
* 方法参数中,3个char类型的变量,代表3个杆的名称‘A’,‘B’,‘C’
* start——> 起始杆
* end ——> 目标杆
* middle ——> 辅助杆
* n ——> 待搬运的圆盘数量
*/
public static void hanoi(char start, char end, char middle, int n){
//递归出口,当规模足够小时,退出递归
if(n == 1){
System.out.println(start + "——>" + end);
return;
}
//按照相同的方式,将最大的圆盘之上的(n-1)个圆盘搬运到辅助杆middle上
hanoi(start, middle, end, n-1);
//最大的圆盘保留在起始杆start上,且起始杆上只有这个圆盘,对应该圆盘直接搬运到目标杆上,一步到位
System.out.println(start + "——>" + end);
//再将辅助杆上的(n-1)个圆盘,从middle杆——>end杆(以start杆为辅助杆)
hanoi(middle, end, start, n-1);
}
}
电影院问题:
/*
周末晚上你和女朋友去看电影,月黑风高,女朋友悄悄地问你:我们在第几排?电影院太黑,没办法数?怎么办?
a. 无法直接看到,自己在第几排,但是我可以,问前排的同学,他在第几排
b. 当依次向前询问,当问到第一排同学的时候,他可以用触觉来判断,比如,摸了一下发现前面没有椅子
递:分解问题 n -> n - 1 -> ... -> 1(出口条件的情况)
归: 组合小规模问题的解 n <- n - 1 <- ... 2 <- 1
递推公式:
location(n) = location(n - 1) + 1
*/
public class Demo2 {
public static void main(String[] args) {
int location = location(10);
System.out.println(location);
}
/*
n代表,实际所处的排数
*/
public static int location(int n) {
//递归出口
if (n == 1) {
// 第一排的同学直接知道自己在第一排
return 1;
}
return location(n - 1) + 1;
}
}
n的阶乘:
/**
* 3. 求n的阶乘
* 1! = 1
* 100!= 100 * (99 * 98 * ... * 1)
* 100! = 100 * 99!
*
* 如果用f(n) 表示n的阶乘的结果
* f(n) = n * f(n - 1)
*/
public class Demo3 {
public static void main(String[] args) {
int factor = factor(5);
System.out.println(factor);
}
/*
求n的阶乘
*/
public static int factor(int n) {
// 出口条件
if (n == 1) {
return 1;
}
return n * factor(n - 1);
}
}
不死兔神:
/* *
* 4.有一对兔子,从出生后第三个月开始每月生一对兔子,小兔子从第三个月开始每月也生一对兔子,
* 假如是不死神兔,那么第20个月一共生多少对兔子?
*
* 月份 1 2 3 4 5
兔子对数 1 1 2 3 ...
i, i + 1, i + 2 代表任意连续的3个月份
N(i) 表示第i个月出生的兔子数量
N(i - 1) 表示第i - 1个月出生的兔子数量
f(i - 2) 表示第 i - 2个月及之前出生的兔子数量
i i + 1 i + 2
N(i) + N(i) = N(i) * 2
N(i - 1) + N(i - 1) + N(i - 1) = N(i - 1) * 2 + N(i - 1)
f(i - 2) + f(i - 2) + f(i - 2) = f(i - 2) * 2 + f(i - 2)
f(i) 表示第i个月兔子的数量
f(i) = f(i - 1) + f(i - 2)
f(1) = 1
f(2) = 1
兔子数量的数列:
1 2 3 4 5 6 ...
1 1 2 3 5 8 ...
fab(n) = fab(n - 1) + fab(n - 2)
递归的缺陷: 可能存在重复计算问题
*/
public class Demo4 {
public static void main(String[] args) {
int count = countRabbit(20);
System.out.println(count);
//测试利用循环求解斐波那契数列的值
int fab = fab(20);
System.out.println(fab);
}
/*
求解第n个月的兔子数量
*/
public static int countRabbit(int n) {
//出口条件
if (n == 1 || n == 2) {
return 1;
}
return countRabbit(n - 1) + countRabbit(n - 2);
}
/*
当利用递归求解,出现重复计算的时候,我们可以使用循环来替代递归(这种直接的转化主要针对比较简单的情况)
n:代表要求解的第n项的菲波那切数列的项数
*/
public static int fab(int n) {
// 就用来存储已经得到结果的第n项的斐波那契数列的值
int[] tmp = new int[n + 1];
tmp[1] = 1;
tmp[2] = 1;
for (int i = 3; i <= n; i++) {
tmp[i] = tmp[i - 1] + tmp[i - 2];
}
return tmp[n];
}
}