文章目录
前言
⭐⭐许多人对递归不成体系,没有方法论,**每次写递归算法 ,都是靠玄学来写代码**,代码能不能编过都靠运气。本篇将介绍写递归的一些基本思想,一些同学可能会感觉很简单,其实不然,我们要通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归。
一、什么是递归?
⭐⭐简而言之递归就是在函数运行期间调用自己的过程
例如 :
int f(int a){
return f(a-1);
}
⭐⭐函数f在运行期间调用了自己,这就叫递归。
递归树:
⭐⭐不过这个函数运行起来就永远也不会结束了,因为它没有出口。
所以我们给他加一个出口
int f(int a){
if(a==0){
//当小于0时不再调用自己
return 0;
}
return f(a-1);
}
给a赋初值3,看一下它的递归树:
所以最后,f(3)=0;
⭐⭐当然递归实际应用中不会这么简单,它在调用自身的同时,还要干很多其它事情。
二、如何用递归解决实际问题
方法总结
⭐⭐我们无法给递归找一个相当严密的模板,直接套用。但可以总结一些思维方法,帮助自己找准切入点,成功用递归解决问题。
这里所说的“思维”,就是:
- 切蛋糕思维
- 找递推公式
- 找等价问题
⭐⭐而当用代码具体实现时可以按照以下步骤:
- 找重复:这一步确定我们如何进行递归
- 找变化:变化量一定会作为参数之一
- 找出口:找跳出条件
下面依次举例。
1.使用递归打印i~j
这里我们就可以“切蛋糕”。假如打印i~j这个问题是一个长方形
从最左侧切一刀,把原问题划分为:打印i+打印(i+1)~j
⭐⭐而打印(i-1)到j 和打印i到j其实是同一个问题,只是参数不同而已,而且他的规模比原来更小
打印(i-1)到j还可以继续切成打印i-1+打印i-2到j,再继续切直到最后打印j。
也就是说我们把打印i到j这个问题分解成了打印i和打印(i+1)俩个问题,而打印i可以说是一个直接动作,打印(i+1)到j是一个等价于原问题,但是规模更小的一个子问题。
⭐⭐即:原问题=直接动作+子问题
子问题等价于原问题且规模更小
只需要照着这个模式,就可以写递归函数了
代码:
描述很复杂,代码实现很简单,不太明白的还可以通过调试看一下程序的实际运行效果
public static void f1(int i, int j) {
//出口
if (i > j) {
return;
}
//直接动作
System.out.print(i + " ");
//子问题
f2(i + 1, j);
}
递归树:
2.求n的阶乘
n!=n✖(n-1)✖(n-2)✖(n-3)✖(n-4)…一直乘到1
这里我们用找递推公式:
n!=n×(n-1)!
然后按照第一题的步骤:
1.找重复:求n的阶乘可以转化为求n乘以(n-1)的阶乘,其中n是一个直接量,求(n-1)的阶乘是一个等价于原问题,但规模更小的子问题。
即:原问题=直接量+子问题(广义的加)
2.找变化:n在越来越小**(n一定会作为参数之一)**
3.找出口:当n等于1时,求1的阶乘不需要再继续划分为直接量加子问题,因为1!=1
代码:
public static int f1(int n) {
//出口
if (n == 1) {
return 1;
}
//直接动作n * f1(n - 1)
//子问题f1(n-1)
return n * f1(n - 1);
}
递归树:
3.用递归实现用辗转相除法求最大公约数
首先了解一下辗转相除法:
辗转相除法是我国古代数学家发明的一种求俩个数的最多公约数的方法,具体是指,求m和n(m>n)的最大公约数相当于求n和m%n的最大公约数,其中%是取余运算,如果m%n等于0那么n就是要求的最大公约数,如果不等于0,那就再算下去
例如求18和4的最大公约数:
因为18%4=2,所以求18和4的最大公约数转化为求4和2的最大公约数
因为4%2=0,所以4和2的最大公约数就是2,所以18和4的最大公约数就是2
显然辗转相除法是通过等价转换把问题规模逐步缩小,从而实现求最大公约数
再按照步骤:
1.找重复:求m和n的最大公约数转化为求n和m%n的最大公约数
即:原问题=子问题(子问题的规模小于原问题)
2.找变化:m和n俩个数都在变化,一定作为参数
3.找出口:当m%n=0时,说明最大公约数已经找到了,就不需要再等价转换了
代码:
描述很复杂,代码实现很简单,不太明白的还可以通过调试看一下程序的实际运行效果
public static int f6(int m,int n){
//出口
if(m%n==0){
return n;
}
//子问题
return f6(n,m%n);
}
递归树:
以求54和12的最大公约数为例
三、递归基础例题
有些例题本身用递归实现并没有多大意义,原来的循环就挺好的,这里要求用递归主要是为了学习使用递
归,掌握递归。
1、用递归求数组中元素的和
1.找重复:求m和n的最大公约数转化为求n和m%n的最大公约数
即:原问题=子问题(子问题的规模小于原问题)
2.找变化:m和n俩个数都在变化,一定作为参数
3.找出口:当m%n=0时,说明最大公约数已经找到了,就不需要再等价转换了
代码:
public static int f3(int[] a, int i) {
//出口
if (i == a.length - 1) {
return a[a.length - 1];
}
//直接动作:a[i] + f3(a, i + 1)
//子问题:f3(a, i + 1)
return a[i] + f3(a, i + 1);
}
递归树:以a={1,2,3,4}举例
2、用递归实现字符串反转
1.找重复: 反转abcd相当于反转abc+d
反转abc相当于反转ab+c
反转ab相当于反转a+b
反转a不需要再继续“切”
即:原问题=直接动作+子问题
2.找变化:单独的那个字母每次都是不一样的,这个字母的下标end作为参数,下标每次减一
3.找出口:当下标end等于0时,不再递归
代码:
public static String reverse(String s,int end){
//出口
if(end==0){
return ""+s.charAt(0);
}
//直接动作s.charAt(end)+reverse(s,end-1)(字母加字符串)
//子问题reverse(s,end-1)
return s.charAt(end)+reverse(s,end-1);
}
递归树:(以反转abcd为例)
3、斐波那契数列
基本形式:Fn=1,1,2,3,5,8,13,21…
规律就是除了F1,F2=1,之后的任意一项都等于前俩项的和
即:Fn=Fn-1+Fn-2
这个问题是所有初学都会碰到的,但很少有人能真正理解
1.找重复:求Fn相当于求Fn-1和Fn-2,然后把它们加起来,求Fn-1,Fn-2是等价于原问题的子问题
即:原问题=子问题+子问题+直接动作(子问题的规模小于原问题)
2.找变化:下标n一直在变化
3.找出口:当n=1或n=2时不需要再划分子问题,可以直接得出它们等于1
代码:
public static int f5(int n){
//出口
if(n==1||n==2){
return 1;
}
//直接动作:求和
//子问题:f5(n-1)、f5(n-2)
return f5(n-1)+f5(n-2);
}
递归树:(以求f(5)为例)
4、用递归实现插入排序
不了解插入排序的可以先去了解一下插入排序再来看这个问题,或者直接跳过,之后再来做。
1.找重复:排列n个元素,相当于先排列前n-1个元素再把第n个元素插入到前n-1个元素中,而排列n-1个元素,相当于先排列前n-2个元素再把第n-1个元素插入到前n-2个元素中
即:原问题=直接动作+子问题(这次的“直接动作”要比前面的几个题复杂些)
2.找变化:待插入元素的下标在递减
3.找出口:当n=1时,只需要排列一个元素,不需要再往下拆分
代码:
//i:待插入元素的下表
//len:已经排列好的部分数组的长度
//j:已经排列好的元素的下标
public static void f7(int[] a,int i){
//出口
if(i==0){
return;
}
//子问题
f7(a,i-1);
//动作:把i位置上的元素插入到已排好的序列中
int len=i-1;
for(int j=len;j>=0;j--){
if(a[i]<a[j]){
int temp=a[i];
a[i]=a[j];
a[j]=temp;
}
i--;
}
}
递归树:(以a={4,3,2,1}为例)
测试前边所有的例题(7个),包含主方法,可以直接运行
public class testdemo {
public static void main(String[] args) {
System.out.println("测试求阶乘");
System.out.println(f1(5));
System.out.println("测试打印i到j");
f2(3, 10);
System.out.println();
System.out.println("测试求数组中元素的和");
int[] a = {1, 2, 3, 4, 5};
System.out.println(f3(a, 0));
System.out.println("测试反转字符串");
String s="abc";
System.out.println(reverse(s,s.length()-1));
System.out.println("测试求最大公约数");
System.out.println(f6(18,12));
System.out.println("测试插入排序");
int b[]={45,12,89,56,23,45,789,1};
f7(b,b.length-1);
for(int i=0;i<b.length;i++){
System.out.print(b[i]+" ");
}
}
//用递归求阶乘
public static int f1(int n) {
if (n == 1) {
return 1;
}
return n * f1(n - 1);
}
//用递归打印i到j(i<j)
public static void f2(int i, int j) {
if (i > j) {
return;
}
System.out.print(i + " ");
f2(i + 1, j);
}
//用递归求数组中元素的和
public static int f3(int[] a, int i) {
if (i == a.length - 1) {
return a[a.length - 1];
}
return a[i] + f3(a, i + 1);
}
//字符串反转
public static String reverse(String s,int end){
if(end==0){
return ""+s.charAt(0);
}
return s.charAt(end)+reverse(s,end-1);
}
//斐波那契数列
public static int f5(int n){
if(n==1||n==2){
return 1;
}
return f5(n-1)+f5(n-2);
}
//辗转相除法+递归求最大公约数
public static int f6(int m,int n){
if(m%n==0){
return n;
}
return f6(n,m%n);
}
//用递归实现插入排序
public static void f7(int[] a,int i){
//i:待插入元素的下表
//len:已经排列好的部分数组的长度
//j:已经排列好的元素的下标
if(i==0){
return;
}
f7(a,i-1);
int len=i-1;
for(int j=len;j>=0;j--){
if(a[i]<a[j]){
int temp=a[i];
a[i]=a[j];
a[j]=temp;
}
i--;
}
}
}
四、递归经典例题
1、汉诺塔
由来:
⭐⭐法国数学家爱德华·卢卡斯曾编写过一个印度的古老传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片:一次只移动一片,不管在哪根针上,小片必须在大片上面。僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、庙宇和众生也都将同归于尽…
(文字内容摘自:https://blog.csdn.net/qq_42942961/article/details/104305602)
⭐⭐图片摘自:原文链接
这是一个汉诺塔小游戏,可以先去玩一下,有助于理解:汉诺塔小游戏
⭐⭐然后我们依次看一下,当圆盘个数分别,为1,2,3时,具体怎么把圆盘全部从A移动到B
当n=1时:移动1 方向 A—>C; 移动一次
当n=2时:移动1 方向 A—>B;
移动2 方向 A—>C;
移动1 方向 B—>C; 移动三次
当n=3时:移动1 方向 A—>C;
移动2 方向 A—>B;
移动1 方向 C—>B;
移动3 方向 A—>C;
移动1 方向 B—>A;
移动2 方向 B—>C;
移动1 方向 A—>C; 移动七次
可以总结
(1)找重复:
把n个圆盘从A移动到C相当于以下三步
1>把n-1个圆盘由A移到B;(等价于原问题的子问题)
2>把第n个圆盘由A移到C;(直接动作)
3>把n-1个圆盘由B移到C;(等价于原问题的子问题)
即:原问题=子问题+直接动作+子问题
把n-1个圆盘由A移到B和原问题是同一个问题,只是A,B,C的角色在变换
(2)找变化:圆盘的个数n在变化
(3)找出口:当n=1时不需要再划分子问题可以直接移动圆盘
圆盘从上到下编号1~n
(1)文字打印圆盘移动过程
public class 汉诺塔 {
//包含主方法,可以直接运行
public static void main(String[] args) {
f(3,"A","B","C");
}
static void f(int n,String from,String to,String help){
//出口
if(n==1){
System.out.println("Move"+n+"from"+from+"to"+to);
return;
}
//子问题1
f(n-1,from,help,to);
//直接动作
System.out.println("Move"+n+"from"+from+"to"+to);
//子问题2
f(n-1,help,to,from);
}
}
(2)用容器ArryList真实模拟汉诺塔
import java.util.ArrayList;
public class 汉诺塔 {
//包含主方法,可以直接运行
public static void main(String[] args) {
int n=2;
ArrayList<Integer> A=new ArrayList<Integer>();
ArrayList<Integer> B=new ArrayList<Integer>();
ArrayList<Integer> C=new ArrayList<Integer>();
//在A中放置1到n
for(int i=n;i>=1;i--){
A.add(i);
}
System.out.println("初始状态");
System.out.println("A:"+A);
System.out.println("B:"+B);
System.out.println("C:"+C);
f(A,B,C,n);
System.out.println("最终结果");
System.out.println("A:"+A);
System.out.println("B:"+B);
System.out.println("C:"+C);
}
public static void f(ArrayList<Integer> from,ArrayList<Integer> to,ArrayList<Integer> help,int m){
if(m==1){
int temp=from.get(from.size()-1);
from.remove(from.size()-1);
to.add(temp);
System.out.println("--------------------");
System.out.println(from);
System.out.println(help);
System.out.println(to);
return;
}
f(from,help,to,m-1);
int temp=from.get(from.size()-1);
from.remove(from.size()-1);
to.add(temp);
System.out.println("--------------------");
System.out.println(from);
System.out.println(help);
System.out.println(to);
f(help,to,from,m-1);
}
}
递归树:
2、用递归实现二分查找
⭐⭐不了解二分法的先看一下这一片文章:二分查找的基本思想及其代码实现
根据二分法的特性可以总结:
(1)找重复:二分查找每次把查找范围缩小一半,在小范围内继续二分查找,方法与原来相同
即:原问题=子问题(子问题规模大约是原问题的一半)
(2)找变化:查找范围一直在变化,也就是说,待查询范围的边界元素的下标l,r在变化
中点mid也随着l和r的变化而变化,但mid实际上是个中间变量,不需要作为参数
(3)找出口:当mid位置所在的元素与目标值target相等时,结束
//包含主方法,可以直接运行
public class 二分查找_递归 {
public static void main(String[] args) {
int[] a={1,2,3,9,12,15,23,41,45,56,59,72,78,79,89,90,100};
int target=3;
int l=0;
int r=a.length-1;
System.out.println(f(a,target,l,r));
}
public static int f(int[] a,int target,int l,int r){
//出口
if(l>r){
return -1;
}
int mid=l+(r-l)/2;
if(target==a[mid]){
return mid;
}else if (target>a[mid]){
//子问题
return f(a,target,mid+1,r);
}else{
//子问题
return f(a,target,l,mid-1);
}
}
}
递归树:
以在{1,2,3,9,12,15,23,41,45,56,59,72,78,79,89,90,100}中寻找3为例
⭐⭐二分查找的递归树看起来很像斐波那契数列的递归树,但其实不一样,二分查找只会走其中的一条路径,而斐波那契数列是把所有的路径都走下去,可见二分查找的效率之高。
五、总结
⭐⭐递归并不可怕,努力练习,深入理解,总能找到属于自己的方法来写递归。递归也是循环的一种,我们在之后的练习中可以通过大量的把循环改造成递归来提升我们对递归的应用。
如果有不懂的地方或者文章有错误之处,欢迎评论区留言!