递归的定义
引子
递归和循环的涵义:
递归:
你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开它。若干次之后,你打开面前的门后,发现只有一间屋子,没有门了。然后,你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这你把钥匙打开了几扇门。
循环:
你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门(若前面两扇门都一样,那么这扇门和前两扇门也一样;如果第二扇门比第一扇门小,那么这扇门也比第二扇门小,你继续打开这扇门,一直这样继续下去直到打开所有的门。但是,入口处的人始终等不到你回去告诉他答案。
递归的定义:
递归:
递归是运行过程中,自己调用自己的一种方法,并且一步步向前推进。在数学与计算机科学中,递归(Recursion)是指在函数的定义中使用函数自身的方法。实际上,递归,顾名思义,其包含了两个意思:递 和 归,这正是递归思想的精华所在。
递归的思想(内涵)
正如上面所描述的场景,递归就是有去(递去)有回(归来),如下图所示。“有去”是指:递归问题必须可以分解为若干个规模较小,与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决,就像上面例子中的钥匙可以打开后面所有门上的锁一样;“有回”是指 : 这些问题的演化过程是一个从大到小,由近及远的过程,并且会有一个明确的终点(临界点),一旦到达了这个临界点,就不用再往更小、更远的地方走下去。最后,从这个临界点开始,原路返回到原点,原问题解决。
中心思想:
演化过程是一个从大到小,由近及远的过程,并且会有一个明确的终点(临界点),一旦到达了这个临界点,就不用再往更小、更远的地方走下去。
更直接地说,递归的基本思想就是把规模大的问题转化为规模小的相似的子问题来解决。特别地,在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况,这也正是递归的定义所在。格外重要的是,这个解决问题的函数必须有明确的结束条件,否则就会导致无限递归的情况。
递归练习
基础的递归练习:
(1)数值阶乘的求解
a. 十三以内的数值阶乘的求解:
解决此题的主要思想:
递归实现十三以内的任意阶乘数,此题我们可以实现给定的数值n以前的并且包括n的所有阶乘;
package arithmetic.recursion;
import java.util.Scanner;
public class Training1_求阶乘 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner input = new Scanner(System.in);
//特别的注意,控制台输入的整型不能超过13并且不包含13,因为整型的数值范围有限
//而int类型在32位机和64位机最大范围为-2147483648~2147483647,超出范围,建议使用更大的数据类型。
int n = input.nextInt();
//求给定整数n的阶乘,方法回调
int sum = multSum(n);
System.out.println("给定数值的阶乘:" + sum);
//求阶乘之和
int result = Sum(n);
System.out.println("整数1 ~ n的阶乘之和:" + result);
}
//递归实现求给定数值的阶乘
public static int multSum(int n) {
if(n == 1)return 1;
return n * multSum(n - 1);
}
//求阶乘之和,12以内的包含12,结果是正确的
public static int Sum(int n) {
int result = 1;
int sum = 0;
for(int i = 1; i <= n; i++) {
result *= i;
sum += result;
}
return sum;
}
}
b. 求解比12还大的数值阶乘:
package arithmetic.recursion;
import java.util.Scanner;
public class Training1_求阶乘 {
public static void main(String[] args) {
//求解比12还大的数值阶乘并且不能超过21不包含21
Scanner inputs = new Scanner(System.in);
long sums = multiSum(inputs.nextInt());
System.out.println("求解比12大的阶乘数值:" + sums);
}
//递归求解比12还大的数值阶乘并且不能超过21不包含21
public static long multiSum(int n) {
if(n == 1)return 1;
return n * multiSum(n - 1);
}
}
解决的思路:
使用比int还大的变量数值类型便可以求得比13大的数值
(2)打印i ~ j 以内的数
解决思想:本题由控制台是随意输入连个十进制整数,并且第一个数必须小于第二个数,在递归调用的时候注意临界的条件(m > n)。
代码区域:
package arithmetic.recursion;
import java.util.Scanner;
public class Training2_输出i到j以内的数值 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner input = new Scanner(System.in);
int m = input.nextInt();
int n = input.nextInt();
repetition(m,n);
}
public static void repetition(int m,int n) {
if(m > n)return;//临界的条件,很重要,并且不可以忽视
System.out.print(m + " ");//先打印它自身,再逐步的向前推进
repetition(m + 1,n);
}
}
(3)数组求和
a. 主方法中给出数组:
package arithmetic.recursion;
public class Training3_数组求和 {
public static void main(String[] args) {
/*注意第8行和第10行的数组表达方式是等价的*/
// TODO Auto-generated method stub
int[] array = {1,4,6,2,9,10,2,3,5};
int sum = arraySum(array,0);
int result = arraySum(new int[] {1,2,3,4,5},0);
System.out.println(result);
System.out.println(sum);
}
//数组求和,注意没有参数的话就添加参数,注意参数的范围
public static int arraySum(int[] array,int begin) {
//注意临界的条件(也就是数组下标为最后一个)
if(begin == array.length - 1) {
return array[begin];
}
return array[begin] + arraySum(array,begin + 1);
}
}
b. 随机生成指定的数组元素
package arithmetic.recursion;
import java.util.Scanner;
public class Training3_数组求和 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int[] array = new int[n];
for(int i = 0; i < n; i++) {
//随机生成一百以内的随机数
array[i] = (int)(Math.random()*100);
}
print(array);
System.out.println();
int sum = arraySum(array,0);
System.out.println(sum);
}
//打印数组元素
public static int[] print(int[] array) {
for(int i = 0; i < array.length; i++) {
if(i != array.length - 1) {
System.out.print(array[i] + " ");
}
else
System.out.print(array[i]);
}
return array;
}
//递归实现求解数组之和
public static int arraySum(int[] array,int begin) {
if(begin == array.length - 1)
return array[begin];
return array[begin] + arraySum(array,begin + 1);
}
}
(4)翻转字符串
package arithmetic.recursion;
public class Training4_翻转字符串 {
public static void main(String[] args) {
// TODO Auto-generated method stub
String s = "abcdef";
System.out.println(reverse(s,s.length()-1));//需要判断出字符串的长度
}
//翻转字符串需要向递归函数中家参数
public static String reverse(String s,int end) {
if(end == 0)//判断条件,判断临界条件也就是第一个字母
return "" + s.charAt(0);//注意参数的返回值类型
return s.charAt(end) + reverse(s,end-1);//将终点指针向左移动
}
}
解决的思路:
这里的字符串我们要使用递归实现,则需要我们向递归函数中添加参数,这里的参数表示的是数组的第一位元素或者是数组的最后一位元素,这里采用的参数表示的是数组的最后一位元素;
还需要特别注意的是递归函数中给定的参数,它与给定的字符串的临界条件,注意到这一点非常的重要。
(5)最大公约数
package arithmetic.recursion;
import java.util.Scanner;
public class Training5_最大公约数 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner input = new Scanner(System.in);
int m = input.nextInt();
int n = input.nextInt();
int result = gcd(m,n);
System.out.println(result);
}
//求取最大公约数
public static int gcd(int m,int n) {
if(n == 0)
return m;
return gcd(n,m % n);
}
}
(6)插入排序改为递归实现
简单的插入排序:
package arithmetic;
import java.util.Scanner;
public class Main{
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
Comparable[] arr = new Comparable[n];
for(int i = 0; i < n; i++) {
//随机生成1000以内的随机数n个
arr[i] = (int)(Math.random()*1000);
}
print(arr);
insertSort(arr);
System.out.println();
print(arr);
}
//打印数组元素
@SuppressWarnings("rawtypes")
public static Comparable[] print(Comparable[] array) {
for(int i = 0; i < array.length; i++) {
if(i != array.length - 1) {
System.out.print(array[i] + " ");
}
else
System.out.print(array[i]);
}
return array;
}
//普通的插入排序
@SuppressWarnings({ "rawtypes", "unchecked" })
public static Comparable[] insertSort(Comparable[] array) {
for(int i = 0; i < array.length; i++) {
Comparable temp = array[i];
for(int j = i; j > 0 && (array[j].compareTo(array[j - 1]) < 0); j--) {
temp = array[j];
array[j] = array[j -1];
array[j - 1] = temp;
}
}
return array;
}
}
递归实现插入排序
解题思想:
本人喜欢使用静态方法,使用方法的好处是:方便主函数调用,极大的方便用户查看以及编译时很容易观察结果。
这里我使用了随机函数随机生成了指定范围内的数组元素,并且将生成的随机数打印到控制台,经过插入排序进行排序,然后再使用打印函数将排序好的数组打印到控制台。
看起来很难,其实就是纸老虎,你理解之前先将整篇代码分解,根据提示一点一点的找到需要的条件。
package arithmetic;
import java.util.Scanner;
public class Training6_插入排序改为递归实现{
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int[] array = new int[n];
for(int i = 0; i < n; i++) {
//随机生成1000以内的随机数n个
array[i] = (int)(Math.random()*1000);
}
//打印数组函数的调用
print(array);
//将输出结果换行
System.out.println();
//排序,递归插入排序函数的调用
insertSort(array,array.length - 1);
//将排序后的数组打印到控制台
print(array);
}
//打印数组元素
public static int[] print(int[] array) {
for(int i = 0; i < array.length; i++) {
if(i != array.length - 1) {
System.out.print(array[i] + " ");
}
else
System.out.print(array[i]);
}
return array;
}
//存取的函数,供递归插入时调用(排序函数)
public static void insert(int[] array,int k) {
int x = array[k];
int index = k - 1;//记录k-1之前的下标
while(index >= 0 && x < array[index]) {//使用while循环判断并且和前k-1项的元素进行比较并且交换
array[index + 1] = array[index];
index--;
}
array[index + 1] = x;
}
//递归实现插入排序
public static void insertSort(int[] array,int k) {
if(k > 0)
insertSort(array,k-1);//注意这里是递归函数的第array.length - 1项排序
//先将第k-1前面的项进行排序
insert(array,k);//调用排序函数
}
}
(7)斐波那契数列
典型的就是斐波那契数列:
第一项:1
第二项:1
第三项:2
第四项:3
第五项:5
第六项:8
… \ldots … … \ldots … … \ldots … … \ldots … … \ldots …
第n项:f(n-1) + f(n-2)
package experience.basic;
import java.util.Scanner;
public class Analyses1_斐波那契数列的应用 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner input = new Scanner(System.in);
int n = input.nextInt();
//打印每一项斐波那契数列
print(n);
System.out.println("\n");
//将第n项的斐波那契数值赋值给变量m
int m = function(n);
//打印前m项斐波那契数列到控制台
print(m);
}
//编写一个函数,实现前n项的每一位数值
public static int function(int n){
if(n == 1 || n == 2){//控制第一项和第二项的值
return 1;
}
//向上递推
return function(n - 1) + function(n - 2);
}
//打印斐波那契数列的每一项
public static void print(int n){
for(int i = 1; i <= n; i++){//从下标1开始到下标n
if(i != n){
System.out.print(function(i) + " ");
}
else{
System.out.print(function(i));
}
}
System.out.println();
}
}
解决的思想:
将斐波那契数列转换为数学问题,并且将套用数学公式层层递推,向上推进,当用户所给出的数值n为1或者为2时,输出1,根据第一项和第二项逐渐向上推进,直到推进到第n项。
(8)斐波那契数列的前100项能被3整除的个数(斐波那契知识拓展)
package java123;
public class Analus_斐波那契求解100项能被三整除的数的个数 {
//方法一:使用long型数组来存储斐波那契数值
public static void main(String[] args) {
// TODO Auto-generated method stub
long[] array = arrayFei(100);
//判断前100项有多少个能被3整除
int result = 0;
for(int i = 0; i < array.length; i++){
long a = array[i];
if(a % 3 == 0)
result++;
System.out.println(a);
}
System.out.println(result);
}
public static long[] arrayFei(int n){
long[] array = new long[n];
array[0] = 1;
array[1] = 1;
for(int i = 2; i <array.length; i++)
array[i] = array[i - 2] + array[i - 1];
return array;
}
}
异常处理:
这里使用了long型会发生溢出的现象,看下图:
所以使用BigInteger来记录:
代码如下:
package BlueBridgeSimulation.MockUp;
import java.math.BigInteger;
public class Analyses4_斐波那契数列 {
private static int count;
public static void main(String[] args) {
/**
* 我使用BigInteger来做这一题
*
*/
BigInteger one = BigInteger.ONE;//定义常量1
BigInteger three = BigInteger.valueOf(3);//定义常量3
BigInteger[] array = new BigInteger[100];
array[0] = one;
array[1] = one;
//我们从2开始循环
for(int i = 2; i < 100; i++){
//将前两项相加,就等于array[i] = array[i - 2] + array[i - 1]
/**
* 常量:(本身就定义的常量,就和自然数一样,已经存在,直接拿来用就好
* one=BigInteger.ONE 1
* ten=BigInteger.TEN 10
* zero=BigInteger.ZERO 0
*
*
* add() 表示算术运算的加号 比如 1 + 10 <==> one.add(ten)
* multiply() 表示算术运算的乘号1 * 10 <==>one.multiply(ten)
* divide() 表示算术运算的除号 1 / 10 <==> one.divide(ten)
* mod a.mod(b) <==> a % b a.mod(b).intValue() == 0 <==> a % b == 0
* subtract() 表示算术运算的减号10 - 1 <==> ten.subtract(one)
*/
array[i] = array[i - 2].add(array[i - 1]);
if(array[i].mod(three).intValue() == 0)
{
count++;
System.out.println(array[i] + " ");
}
}
System.out.println("能被3整除的前100项的个数为 :" + count);
}
}
特别注意一下这里的 1 0 10, BigInteger使用了特别的包装方法,将它们置为常数。
常量:(本身就定义的常量,就和自然数一样,已经存在,直接拿来用就好
one=BigInteger.ONE 1
ten=BigInteger.TEN 10
zero=BigInteger.ZERO 0
add() 表示算术运算的加号 比如 1 + 10 <==> one.add(ten)
multiply() 表示算术运算的乘号1 * 10 <==> one.multiply(ten)
divide() 表示算术运算的除号 1 / 10 <==> one.divide(ten)
mod a.mod(b) <==>a % b a.mod(b).intValue() == 0 <==> a % b == 0
subtract() 表示算术运算的减号10 - 1 <==> ten.subtract(one)
递归总结
程序编写递归的四条基本准则:
(1)基准情形:不需要递归就能解出数值;
(2)不断推进:适于需要递归求解的情形,每一次求解值都需要递归调用,朝基准情形靠拢;
(3)设计法则:也就是所有的递归调用都能运行;
(4)合成效益法则:求解同一问题应用时,不要做重复性的动作。
构成递归所具备的条件:
(1)子问题须与原始问题为同样的事,且更为简单;
(2)不能无限制地调用本身,须有个出口,化简为非递归状况处理。
递归的三要素:
(1)重复;
(2)变化;
(3)边界。