递归:由浅入深,深入了解递归

一、递归基础知识

1.1 递归的内涵

1、定义 (什么是递归?)

在数学与计算机科学中,递归(Recursion)是指在函数的定义中使用函数自身的方法。实际上,递归,顾名思义,其包含了两个意思:递 和 归,这正是递归思想的精华所在。

2、递归思想的内涵(递归的精髓是什么?)

正如上面所描述的场景,递归就是有去(递去)有回(归来)。“有去”是指:递归问题必须可以分解为若干个规模较小,与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决;“有回”是指 : 这些问题的演化过程是一个从大到小,由近及远的过程,并且会有一个明确的终点(临界点),一旦到达了这个临界点,就不用再往更小、更远的地方走下去。最后,从这个临界点开始,原路返回到原点,原问题解决。

更直接地说,递归的基本思想就是把规模大的问题转化为规模小的相似的子问题来解决。特别地,在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况,这也正是递归的定义所在。格外重要的是,这个解决问题的函数必须有明确的结束条件,否则就会导致无限递归的情况。

3、用归纳法来理解递归

递归的数学模型其实就是 数学归纳法,这个在高中的数列里面是最常用的了,回忆一下数学归纳法。

1.2 递归的三大要素

第一要素:明确你这个函数想要干什么

对于递归,我觉得很重要的一个事就是,这个函数的功能是什么,他要完成什么样的一件事,而这个,是完全由你自己来定义的。也就是说,我们先不管函数里面的代码什么,而是要先明白,你这个函数是要用来干什么。

第二要素:寻找递归结束条件

所谓递归,就是会在函数内部代码中,调用这个函数本身,所以,我们必须要找出递归的结束条件,不然的话,会一直调用自己,进入无底洞。也就是说,我们需要找出当参数为啥时,递归结束,之后直接把结果返回,请注意,这个时候我们必须能根据这个参数的值,能够直接知道函数的结果是什么。

第三要素:找出函数的等价关系式

第三要素就是,我们要不断缩小参数的范围,缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变。

二、递归入门题解

对于递归问题,我主要是紧紧围绕着以下几点来解决:

  • 明确函数的功能(利用这个函数来求什么)
  • 根据递归的基本思想(把规模大的问题转化为规模小的相似的子问题来解决)来找出等价关系式
  • 寻址递归结束条件(找边界、临界)

    2.1 👻求n的阶乘

    给出一个整数n,求出n的阶乘

    分析:

    • 函数的功能:f(n):求n的阶乘

    • 划分子问题:

      f(n)求n的阶乘 => f(n-1)求(n-1)的阶乘

      n的阶乘不就是n乘以(n-1)的阶乘吗?

      即有 f(n) = n*f(n-1)

    • 递归出口:当n为1时,f(1)=1,则应该返回

      代码:

      public class Main { public static void main(String[] args) { System.out.println(f(5));
          }
          
          public static int f(int n) { if (n == 1) { return 1;
              }
              return n * f(n - 1);
          }
      }
      

      2.2 👽打印

      传入整数i和j打印i到j的数

      分析:

      • 函数的功能:打印整数
      • 划分子问题:

        * 打印i=>j,可先打印i,再打印(i+1)=>j

        * 再打印(i+2),(i+3)=>j

      • 递归出口:当打印到5时即可结束,即i>j

        代码

        public class Main { public static void main(String[] args) { f(1, 5);
            }
            public static void f(int i, int j) { if (i > j) { return;
                }
                System.out.println(i);
                f(i + 1, j);
            }
        }
        

        2.3 🐲数组求和

        传入数组索引0,和数组,对数组进行求和

        分析

        • 函数的功能:求和
        • 划分子问题:

        • 递归出口:当加到数组中最后一个数时则应该返回

          代码

          public class Main { public static void main(String[] args) { System.out.println(f(0, new int[]{1, 2, 3}));
              }
              public static int f(int i, int[] arr) { if (i == arr.length - 1) { return arr[i];
                  }
                  return arr[i] + f(i + 1, arr);
              }
          }
          

          2.4 👾斐波那契数列

          斐波那契数列(Fibonacci sequence),指的是这样一个数列:1、1、2、3、5、8、13、21、34、……

          分析:

          • 函数的功能:F(n):求出斐波那契数列中第n个数

          • 等价关系式:根据斐波那契数列的规律易知

            • F(n) = 1 , n=1,2
            • F(n) = F(n-1)+F(n-2) , n>2

              代码

              public class Main { public static void main(String[] args) { System.out.println(f(5));
                  }
                  public static int f(int n) { if (n == 1 || n == 2) { return 1;
                      }
                      return f(n - 1) + f(n - 2);
                  }
              }
              

              2.5 🤡上楼梯

              有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶、3阶

              请实现一个方法,计算小孩有多少种上楼的方式。

              为了防止溢出,请将结果Mod 1000000007

              分析

              • 函数的功能:F(n)=n阶台阶的上楼方式种树
              • 函数的关系式:

                代码

                public class Main { static final int mod = 1000000007;
                    public static void main(String[] args) { System.out.println(f(20));
                    }
                    public static int f(int n) { if (n < 0)
                            return 0;
                        if (n == 0 || n == 1)
                            return 1;
                        if (n == 2)
                            return 2;
                        //递推关系
                        return f(n - 1) % mod + f(n - 2) % mod + f(n - 3) % mod;
                    }
                }
                

                三、递归进阶题解

                3.1 🐾汉诺塔问题

                汉诺塔问题是一个经典的问题。汉诺塔(Hanoi Tower),又称河内塔,源于印度一个古老传说。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,任何时候,在小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。问应该如何操作?

                分析

                • 函数的功能:F(n):求n个圆盘从A移动到C

                • 划分子问题:当盘子数量为2时进行分析

                  问题类比:

                  递归出口分析:当只有一个盘子时

                  总结:

                  1. 当只有一个盘子时,直接从A移动到C即可
                  2. 当盘子数大于2时,我们总是可以看做是两个盘(最下边的一个盘,上面的所有盘)
                  3. 先将上面的所有盘从A->B,移动的过程中借助C
                  4. 把下面的一个盘从A->C
                  5. 把B塔中所有盘从B->C,移动的过程中借助A

                  代码

                  public class Main { public static void main(String[] args) { hanoiTower(3,'A','B','C');
                      }
                      //汉诺塔移动的方法
                      //使用分治算法
                      private static void hanoiTower(int num, char a, char b, char c) { if (num == 1) { //如果只有一个盘
                              System.out.println("第1个盘从 " + a + "->" + c);
                          } else { //如果n>=2情况,我们总是可以看做是两个盘 
                              //1.最下边一个盘 2.上面的所有盘
                              //1.先把最上面的所有盘 A->B,移动过程借助C
                              hanoiTower(num - 1, a, c, b);
                              //2.把最下面的盘 A->C
                              System.out.println("第" + num + "个盘从 " + a + "->" + c);
                              //3.把B塔的所有盘从 B->C,移动过程借助A
                              hanoiTower(num - 1, b, a, c);
                          }
                      }
                  }
                  

                  3.2 🤖机器人走方格

                  有一个X * Y的网格,一个机器人只能走格点且只能向右或向下走,要从左上角到右下角

                  请设计一个算法,计算机器人有多少种走法

                  给定两个正整数x,y,返回机器人的走法数目。保证x+y小于等于12

                  分析

                  • 函数的功能:F(x,y)=机器人在网格为x,y的图中有多少种走法。
                  • 函数关系式:

                    根据图解分析过程可知,当只有一行或者一列时,只有一种走法=>递归出口

                    F(x,y)=F(x-1)+F(y-1)

                    代码

                    public class Main { public static void main(String[] args) { System.out.println(f(3, 3));
                        }
                        
                        public static int f(int x, int y) { if (x == 1 || y == 1) return 1;
                            return f(x - 1, y) + f(x, y - 1);
                        }
                    }
                    

                    3.3 🤑硬币表示问题

                    假设我们有4种不同面值的硬币{1,5,10,25},用这些硬币组合构成一个给定的数值n。

                    问总共有多少种可能的组合方式?

                    分析

                    • 函数关系式思路图解:

                      相较于之前的题目,这题难度有一定的提升,主要在于,在循环中递归

                      public class Main { public static void main(String[] args) { for (int i = 0; i < 26; i++) { System.out.println(i + "-" + countWays(i));
                              }
                          }
                          public static int countWays(int n) { if (n <= 0) return 0;
                              return countCore(n, new int[]{1, 5, 10, 25}, 3);
                          }
                          /**
                           * 思路:对于n分钱,要先决定最大面值的要去(i=0,1,2...)个,剩下的(n-i*最大面值)交给剩余的几种面值来凑
                           * @param n     要凑多少分钱
                           * @param coins 硬币种类
                           * @param cur   当前最大面值
                           * @return 多少种凑法
                           */
                          public static int countCore(int n, int[] coins, int cur) { if (cur == 0) { // 只剩下1只有1种
                                  return 1;
                              }
                              int res = 0;
                              for (int i = 0; i * coins[cur] <= n; i++) { res += countCore(n - i * coins[cur], coins, cur - 1);
                              }
                              return res;
                          }
                      }
                      

                      3.4 🐏合法括号

                      实现一种算法,打印n对括号的全部有效组合(即左右括号正确配对)

                      实例

                      输入:3

                      输出:((())),(()()),(())(),()(()),()()()

                      分析

                      代码

                      import java.util.HashSet;
                      import java.util.Set;
                      public class Main { public static void main(String[] args) { System.out.println(kuoHao(3));
                          }
                          // 递归
                          public static Set kuoHao(int n) { Set s_n = new HashSet();
                              if (n == 1) { s_n.add("()");
                                  return s_n;
                              }
                              Set s_n_1 = kuoHao(n - 1);
                              for (String s : s_n_1) { s_n.add("()" + s); // 加左
                                  s_n.add(s + "()"); // 加右
                                  s_n.add("(" + s + ")"); // 包
                              }
                              return s_n;
                          }
                      }
                      

                      3.5 🐭子集生成

                      编写一个方法,返回某集合的所有非空子集

                      给定一个int数组A和数组的大小int n,请返回A的所有非空子集

                      保证A的元素个数小于等于20,且元素互异

                      分析

                      代码

                      import java.util.ArrayList;
                      import java.util.HashSet;
                      import java.util.Set;
                      public class Main { public static void main(String[] args) { int[] A = {1, 2, 3};
                              Set> subsets = getSubsets(A, A.length);
                              subsets.remove(new HashSet()); // 去掉空集
                              System.out.println(subsets);
                          }
                          public static Set> getSubsets(int[] A, int n) { return getSubsetsCore(A, n, n - 1);
                          }
                          // 递归
                          private static Set> getSubsetsCore(int[] A, int n, int cur) { Set> newSet = new HashSet<>();
                              if (cur == 0) { // 处理第一个元素
                                  HashSet nil = new HashSet<>(); // 空集
                                  HashSet first = new HashSet<>(); // 包含第一个元素的集合
                                  first.add(A[0]);
                                  newSet.add(nil);
                                  newSet.add(first);
                                  return newSet;
                              }
                              Set> oldSet = getSubsetsCore(A, n, cur - 1);
                              for (Set set : oldSet) { // 对于每个子集,cur这个元素可以加进去,也可以不加进去
                                  newSet.add(set); // 保留原样
                                  // 克隆一下,添加当前元素
                                  Set clone = (Set) ((HashSet) set).clone();
                                  clone.add(A[cur]);
                                  newSet.add(clone);
                              }
                              return newSet;
                          }
                      }
                      

                      二进制解法