概念:
以编程的角度来看,递归指的是方法定义中调用方法本身的现象。
解决问题的思路:
①把一个复杂的问题层层转化为一个与原问题相似的规模较小的问题来解决;
②递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算过程;
递归解决问题要找出两个内容:
①递归出口:否则会出现内存溢出;
②递归规则:与原问题相似的规模较小的问题;
做题分析
考虑项 | 实现 |
---|---|
递归出口 | 分析递归结束的条件 |
递归规则 | 分析递归工作顺利进行的条件 |
回溯阶段 | 递归具体执行的操作 |
// 228
public class DiGuiDemo {
public static void main(String[] args){
int[] arr = new int[20];
arr[0] = 1;
arr[1] = 1;
for(int i = 2;i < 20;i++){
arr[i] = arr[i-1] + arr[i-2];
}
System.out.println(arr[19]);
// 使用方法f解决不死神兔的问题,
System.out.println(f(20));
/*递归解决问题,首先要定义一个方法:
* 定义一个方法f(n),表示第n个月的兔子对数
* 那么第n-1个月的兔子对数表示为f(n-1)
* 同理 第n-2个月的兔子对数表示为f(n-2)*/
}
/* public static int f(int n){
return f(n-1) + f(n-2);
}*/
// 使用这种方式定义会出现StackOverflowError异常,这个表示当堆栈溢出发生时抛出一个应用程序递归太深。
// 上述由于一直采用递归,致使程序没有出口,造成堆栈溢出抛出应用程序递归太深异常。
public static int f(int n){
if(n == 1 || n == 2){
return 1;
}else{
return f(n-1) + f(n-2);
}
}
}
// 35
public class DiguiDemo {
public static void main(String[] args) {
int sum = function1(5);
System.out.println(sum);
// 输出结果是120
}
public static int function1(int n){
if(n == 1){
return 1;
}else{
return n*function1(n-1);
}
}
}
需求:给定一个路径,请通过递归完成遍历该目录下的所有内容,并把所有文件的绝对路径输出在控制台。
// 35
public class MuluDemo {
public static void main(String[] args){
// 根据指定路径创建一个File对象
File srcFile = new File("E:\\222");
// 递归开始
getAllFilePath(srcFile);
}
public static void getAllFilePath(File srcFile){
// 通过listFiles方法获取File对象表示的目录中的文件和目录的File对象数组
File[] filesArray = srcFile.listFiles();
// 递归规则:如果是目录则继续遍历 递归出口:如果是文件,则直接输出抽象路径的字符串,通过getAbsolutePath方法转换抽象路径为路径名字符串
if(filesArray != null){
// 对file对象数组 进行增强for循环遍历
for(File file:filesArray){
if(file.isFile()){
// 判断抽象路径名表示的File是否为文件
System.out.println(file.getAbsolutePath());
}else{
// 是目录 递归遍历
getAllFilePath(file);
}
}
}
}
}
区别 | 递归 recursion |
迭代 iteration |
---|---|---|
概念 | 运行的过程中调用自己,调用自身的编程思想,即一个函数调用本身 重复调用函数自身实现循环 |
又称为辗转法,利用已知的变量值,根据递推公式不断演进得到变量新值的编程思想 函数内某段代码实现循环 与之相对的直接法(一次解法),即一次性解决问题 |
产生条件 | 子问题须与原始问题为同样的事,且更为简单 不能无限制地调用本身,须有个出口,化简为非递归状态处理 |
– |
案例 | 斐波那契数列、阶乘、汉诺塔问题、全排列 | 斐波那契数列、汉诺塔问题、背包问题 |
结构 | 树结构,从递归到达底部就开始回归,过程相当于树的深度优先遍历 | 环结构,从初始状态开始,每次迭代都遍历这个环,并更新状态,多次迭代直到结束状态 |
递归与普通循环的区别:
递归—有去有回(因为存在终止条件);循环—有去无回;
迭代与普通循环的区别:
迭代—循环代码中参与运算的变量同时是保存结果的变量,当前保存的结果作为下一次循环计算的初始值;循环—当前保存的结果不一定作为下一次循环计算的开始;
需求
计算n的阶乘。
示例
输入:5 输出:
输入:6 输出:
输入:0 输出:0
public int method(int n){
// 异常值判断
if( n == 0)
return 0;
// 递归结束条件
if(n == 1)
return 1;
// 逻辑处理
// 无
// 递归调用
return n*method(n-1);
}
public int mainMethod(int n){
// 异常值判断
if(n==0)
return 0;
// 输出结果值
int result = 1;
// 循环
while(n > 0){
result *= n;
n -= 1;
}
return result;
}
需求
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
取模就是取余的意思。
示例
输入:2 输出:2
输入:7 输出:21
输入:0 输出:1
一般求解 多少种可能性的题目 一般都有 递归性质,即f(n) 和 f(n-2) … f(1) 之间是有联系的;
假设跳上n级台阶有f(n)种跳法,在所有跳法中,青蛙的最后一步有两种情况:跳上1阶、2阶台阶
当为1时,剩n-1个台阶,共有f(n-1)种跳法;
当为2时,剩n-2个台阶,共有f(n-2)种跳法;
综合f(n)为以上两种方式之和,即f(n) = f(n - 1) + f(n - 2);
转化为斐波那契数列求解;
f(0) = 1 f(1) = 1 f(2) = 2 f(3) = 3
public int numWays(int n) {
// 递归
if(n == 0 || n == 1)
return 1;
// 逻辑处理
// 无
// 递归调用
return numWays(n-1)%1000000007 + numWays(n-2)%1000000007;
// 因为有两种情况,跳1阶 或 跳2阶 。
}
问题
超时。以为是没有取模的情况,结果并不是这个原因导致超时;
public int numWays(int n ){
// 临界条件
if( n == 0 || n == 1)
return 1;
// 开始条件
int a = 1, b = 1, sum;
// 循环
for(int i = 0 ; i < n ; i ++){
sum = (a + b) % 1000000007 ;
a = b;
b = sum;
}
return a;
}
分析:
状态定义——设dp为一维数组,其中dp[i]的值代表 斐波那契数列的第i个数字
转移方程——dp[i+1]=dp[i]+dp[i−1] ,即对应数列定义f(n+1)=f(n)+f(n−1)
初始状态—— dp[0] = 1,dp[1]=1 ,即初始化前两个数字
返回值—— dp[n] ,即斐波那契数列的第 n 个数字
代码:
public int numWays(int n) {
// 动态规划
// 临界值
if(n == 1 || n == 0 )
return 1;
// 为了匹配for循环对array数组的赋值操作,这里对n = 2进行另外处理
if(n == 2)
return 2;
// 定义存放跳x台阶的方式数
int[] array = new int[n+1];
// 定义n+1的原因是:因为array[x] 表示跳x台阶的方式数,而计算array数组的公式是array[x+1] = array[x] + array[x-1]的形式
// 最开始的两个特殊array
array[0] = 1;
array[1] = 1;
// for循环给array赋值
for(int i = 1 ; i < n; i++){
array[i+1] = (array[i] + array[i-1]) % 1000000007;
}
return array[n];
}
需求
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
示例
输入:n = 2 输出:1
输入:n = 5 输出:5
public int fib(int n) {
// 临界条件
if(n == 0)
return 0;
// 递归结束条件
if(n == 1)
return 1;
// 递归调用
return fib(n-1) % 1000000007 + fib(n-2) % 1000000007 ;
}
public int fib(int n) {
// 动态规划
int m = 1000000007;
// 特殊值 f(0) = 1,f(1) = 1
if( n < 2)
return n;
// 动态规划
int a = 1,b=1;
int sum = 0;
for(int i = 2 ; i < n ; i++){
sum = (a + b) % m;
a = b ;
b = sum;
}
return b;
}
代码
class Solution {
// 递归实现动态规划
// 这里1e9+7前不加int会报错
static final int MOD = (int)1e9+7;
// 定义动态规划所有值所存放的数组
// 定义101的原因是:因为题目给定的n范围是0-100,那么有第100个数表示f(100),那么就需要101个存储单元,这样下标才能有100
static int[] array = new int[101];
public int fib(int n) {
// 递归的结束条件
if(n < 2)
return n;
// 去掉这个会报:超出时间限制的错误。
// 如果该位置不为0,说明已经计算过了,可以直接输出不用在重复计算。这就使得递归不超时了。
if(array[n] != 0)
return array[n];
// 递归调用
array[n] = fib(n-1) + fib(n-2);
// 逻辑处理
array[n] = array[n] %MOD;
return array[n];
}
}
介绍
使用矩阵快速幂的方法可以降低时间复杂度;
计算一个数的多次幂时,可以先判断其幂次的奇偶性。
class Solution {
// 定义结果要整除的数
static final int Mod = 1000000007;
public int fib(int n) {
// 对于特殊值的快速返回
if(n < 2)
return n;
// 定义矩阵快速幂的矩阵
int[][] q = {{1,1},{1,0}};
// 这个矩阵的形式是根据递推公式得来的
// 矩阵快速幂的结果矩阵
int[][] res = pow(q,n-1);
// q表示矩阵,n-1表示幂次
// 返回值
return res[0][0];
}
// 矩阵快速幂求解方法
public int[][] pow(int[][] a,int n){
// 定义res矩阵
int[][] res = {{1,0},{0,1}};
// 循环判断 n的值
while(n > 0){
// 判断n的末尾是否是1 即幂次数是奇数的情况 需要给(n & 1)加括号
if((n & 1) == 1){
res = multiply(res,a);
}
// 幂数除以2 使用位运算会比除法运算耗费时间更短。
n >>= 1;
a = multiply(a,a);
}
return res;
}
// 两个矩阵相乘
public int[][] multiply(int[][] a,int[][] b){
// 定义最终输出矩阵
int[][] c = new int[2][2];
// 两矩阵的乘法
for(int i = 0 ; i < 2 ; i++){
for(int j = 0 ; j < 2 ; j++){
c[i][j] = (int) (( (long)a[i][0] * b[0][j] + (long)a[i][1] * b[1][j]) % Mod);
}
}
return c;
}
}
图示:
分析:
由于可以利用的范围只有[0,100],那么对其进行打表预处理,然后直接返回。
打表的数组长度是110固定,按理说 101个就可以了,也就是标号可以到100,尝试下(验证成功);
代码:
class Solution {
// 设置静态数组和取模运算的常量,还有静态数组的长度N
static int MOD = 1000000007;
// N的取值,因为n的取值是100 也就需要计算f(100),即下标是100,那么数组长度就是101
static int N = 101;
static int[] table = new int[N];
// 静态代码块,类加载会直接加载的
static{
table[0] = 0;
table[1] = 1;
for(int i = 2; i < N;i++){
table[i] = (table[i-1] + table[i-2]) % MOD;
}
}
public int fib(int n) {
// 特殊值 输出
if(n < 2)
return n;
return table[n];
}
}
描述
在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
(1) 每次只能移动一个盘子;
(2) 盘子只能从柱子顶端滑出移到下一根柱子;
(3) 盘子只能叠在比它大的盘子上。
请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。
你需要原地修改栈。
示例
输入:A = [2, 1, 0], B = [], C = [] 输出:C = [2, 1, 0]
输入:A = [1, 0], B = [], C = [] 输出:C = [1, 0]
xhj理解:
X = [c,b,a] 表示X柱子从下到上的盘子大小依次为 c,b,a,且盘子大小依次递减。
分析:
递归三要素:
1 功能:
实现将a柱上的n-1个盘子移动到b柱上;
a柱上的最后一个盘子移动到c柱上;
b柱上的n-1个元素移动到c柱上;
2 递归结束条件:
当a柱上只有一个元素的的时候 或者 当a柱上没有元素的时候。
3 寻找函数关系等式
f(n,A,B,C) = f(n-1,A,C,B) + f(1,A,B,C) + f(n-1,B,A,C);
可以发现四个式子中,1,2,4是一样的内容,3内容稍微有点不一样,则3是单独的,而其余的三个是相同的方法调用。
代码:
class Solution {
public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
// 最开始的递归调用
moveplant(A.size(),A,B,C);
}
// 递归调用函数
public void moveplant(int n , List<Integer> a,List<Integer> b,List<Integer> c ){
// 递归结束条件
if( n == 1){
// 将a柱的最后一个元素移动到c柱上
c.add(a.remove(a.size()-1));
// 返回
return ;
}
// 寻找函数关系等式
// 将a柱上n-1个元素移动到b柱上
moveplant(n-1,a,c,b);
// 将a柱上的最后一个元素移动到c柱上
c.add(a.remove(a.size() - 1));
// 将b柱上的n-1个元素移动到c柱上
moveplant(n-1,b,a,c);
}
}
class Solution {
public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
// 最开始的递归调用
moveplant(A.size(),A,B,C);
}
// 递归调用函数
public void moveplant(int n , List<Integer> a,List<Integer> b,List<Integer> c ){
// 递归结束条件
if( n == 0){
// 返回
return ;
}
// 寻找函数关系等式
// 将a柱上n-1个元素移动到b柱上
moveplant(n-1,a,c,b);
// 将a柱上的最后一个元素移动到c柱上
c.add(a.remove(a.size() - 1));
// 将b柱上的n-1个元素移动到c柱上
moveplant(n-1,b,a,c);
}
}
描述
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
输入:nums = [0,1] 输出:[[0,1],[1,0]]
输入:nums = [1] 输出:[[1]]
准备
回溯法——一种通过探索所有可能的候选解来找出所有的解的算法。如果候选解被确认不是一个解(或至少不是最后一个解),回溯算法会通过在上一步进行一些变化抛弃该解,即回溯并且再次尝试。
该方法分析
使用数组标记填过的数很直观,该方法不是使用数组标记的;
而是将题目给定的n个数的数组nums划分成左右两个部分,左边的表示已经填过的数字,右边表示待填的数,在回溯的时候只要动态维护这个数组就可以了。
代码:
class Solution {
public List<List<Integer>> permute(int[] nums) {
// 创建用于存放全排列的集合
List<List<Integer>> list = new ArrayList<List<Integer>>();
// 存放单个结果的List
List<Integer> output = new ArrayList<Integer>();
// List接口有构造方法
// 将nums的内容存储到output中,使用增强for循环
for(int num : nums)
output.add(num);
// 定义nums数组的长度
int n = nums.length;
// 递归调用
backtrack(n,output,list,0);
// 返回结果
return list;
}
public List<List<Integer>> backtrack(int n,List<Integer> output,List<List<Integer>> list, int first){
// 迭代结束条件
if(first == n){
// 不加new ArrayList(output) 结果输出全是123
list.add(new ArrayList<Integer>(output));
}
for(int i = first; i < n; i++){
// 交换first 和 i 索引位置的元素
Collections.swap(output,first,i);
// 递归调用
backtrack(n,output,list,first+1);
// 再次交换 first 和 i 索引位置的元素
Collections.swap(output,first,i);
}
return list;
}
}
class Solution {
// 定义静态列表集合存储数组全排列集合元素
List<List<Integer>> lists = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
// 定义nums数组长度
int n = nums.length;
// 定义存放数组全排列元素的集合
List<Integer> list = new ArrayList<Integer>(n);
// 定义存放数组元素是否使用过的状态数组
boolean[] used = new boolean[n];
// 定义变量表示要填第几个数
int count = 0;
// 递归开始
backtrace(list,used,count,nums);
// 返回全排列组成的集合
return lists;
}
public List<Integer> backtrace(List<Integer> list,boolean[] used,int count,int[] nums){
// 递归结束条件
if( count == used.length){
lists.add(new ArrayList<>(list));
}
for(int i = 0 ; i < used.length ; i++){
// nums i 位置的元素没有被使用过
if(!used[i]){
// 元素添加到list中
list.add(nums[i]);
// 将其标志位 置为 1
used[i] = true;
// 递归调用
backtrace(list,used,count+1,nums);
// 将list和used 中对应位置元素 复原
list.remove(list.size() - 1);
// used对应位置复位
used[i] = false;
}
}
return list;
}
}