参考:https://blog.csdn.net/feizaosyuacm/article/details/54919389
https://blog.csdn.net/sinat_38052999/article/details/73303111
1.定义
在数学与计算机科学中,递归(Recursion)是指在函数的定义中使用函数自身的方法。实际上,递归,顾名思义,其包含了两个意思:递 和 归,这正是递归思想的精华所在。
2、递归思想的内涵(递归的精髓是什么?)
递归是有去(递去)有回(归来),如下图所示。“有去”是指:递归问题必须可以分解为若干个规模较小,与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决;“有回”是指 : 这些问题的演化过程是一个从大到小,由近及远的过程,并且会有一个明确的终点(临界点),一旦到达了这个临界点,就不用再往更小、更远的地方走下去。最后,从这个临界点开始,原路返回到原点,原问题解决。
更直接地说,递归的基本思想就是把规模大的问题转化为规模小的相似的子问题来解决。特别地,在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况,这也正是递归的定义所在。格外重要的是,这个解决问题的函数必须有明确的结束条件,否则就会导致无限递归的情况。
3、用归纳法来理解递归
数学都不差的我们,第一反应就是递归在数学上的模型是什么,毕竟我们对于问题进行数学建模比起代码建模拿手多了。观察递归,我们会发现,递归的数学模型其实就是 数学归纳法,这个在高中的数列里面是最常用的了,下面回忆一下数学归纳法。
数学归纳法适用于将解决的原问题转化为解决它的子问题,而它的子问题又变成子问题的子问题,而且我们发现这些问题其实都是一个模型,也就是说存在相同的逻辑归纳处理项。当然有一个是例外的,也就是归纳结束的那一个处理方法不适用于我们的归纳处理项,当然也不能适用,否则我们就无穷归纳了。总的来说,归纳法主要包含以下三个关键要素:
步进表达式:问题蜕变成子问题的表达式
结束条件:什么时候可以不再使用步进表达式
直接求解表达式:在结束条件下能够直接计算返回值的表达式
事实上,这也正是某些数学中的数列问题在利用编程的方式去解决时可以使用递归的原因,比如著名的斐波那契数列问题。
4、递归的三要素
在我们了解了递归的基本思想及其数学模型之后,我们如何才能写出一个漂亮的递归程序呢?笔者认为主要是把握好如下三个方面:
1、明确递归终止条件;
2、给出递归终止时的处理办法;
3、提取重复的逻辑,缩小问题规模。
1). 明确递归终止条件
我们知道,递归就是有去有回,既然这样,那么必然应该有一个明确的临界点,程序一旦到达了这个临界点,就不用继续往下递去而是开始实实在在的归来。换句话说,该临界点就是一种简单情境,可以防止无限递归。
2). 给出递归终止时的处理办法
我们刚刚说到,在递归的临界点存在一种简单情境,在这种简单情境下,我们应该直接给出问题的解决方案。一般地,在这种情境下,问题的解决方案是直观的、容易的。(在代码上直观体现为,直接return某个结果)
3). 提取重复的逻辑,缩小问题规模*
我们在阐述递归思想内涵时谈到,递归问题必须可以分解为若干个规模较小、与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决。从程序实现的角度而言,我们需要抽象出一个干净利落的重复的逻辑,以便使用相同的方式解决子问题。
4.递归与循环的区别于联系
相同点:
(1)都是通过控制一个变量的边界(或者多个),来改变多个变量为了得到所需要的值,而反复而执行的;
(2)都是按照预先设计好的推断实现某一个值求取;(请注意,在这里循环要更注重过程,而递归偏结果一点)
不同点:
(1)递归通常是逆向思维居多,“递”和“归”不一定容易发现(比较难以理解);而循环从开始条件到结束条件,包括中间循环变量,都需要表达出来(比较简洁明了)。
简单的来说就是:用循环能实现的,递归一般可以实现,但是能用递归实现的,循环不一定能。因为有些题目①只注重循环的结束条件和循环过程,而往往这个结束条件不易表达(也就是说用循环并不好写);②只注重循环的次数而不注重循环的开始条件和结束条件(这个循环更加无从下手了)。
5.递归的应用场景
在我们实际学习工作中,递归算法一般用于解决三类问题:
(1). 问题的定义是按递归定义的(Fibonacci函数,阶乘,…);
(2). 问题的解法是递归的(有些问题只能使用递归方法来解决,例如,汉诺塔问题,…);
//上面两类比较简单,算是单向递归;较难的递归问题,一般都不是单向递归,而是需要使用【回溯】的方法,递归的方法不太容易想到.
(3). 数据结构是递归的(链表、树等的操作,包括树的遍历,树的深度,…)。
6.递归经典问题
- 第一类问题:问题的定义是按递归定义的
(1). 阶乘
public class Factorial {
public static long f(int n){
if(n == 1) // 递归终止条件
return 1; // 简单情景
return n*f(n-1); // 相同重复逻辑,缩小问题的规模
}
(2). 斐波纳契数列
* Description: 斐波纳契数列,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、……
* 在数学上,斐波纳契数列以如下被以递归的方法定义:F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*)。
* 两种递归解法:经典解法和优化解法
public class FibonacciSequence {
public static int fibonacci(int n) {
if (n == 1 || n == 2) { // 递归终止条件
return 1; // 简单情景
}
return fibonacci(n - 1) + fibonacci(n - 2); // 相同重复逻辑,缩小问题的规模
}
(3). 杨辉三角的取值
* x 指定行
* y 指定列
* Title: 杨辉三角形又称Pascal三角形,它的第i+1行是(a+b)i的展开式的系数。
* 它的一个重要性质是:三角形中的每个数字等于它两肩上的数字相加。
*
* 例如,下面给出了杨辉三角形的前4行:
* 1
* 1 1
* 1 2 1
* 1 3 3 1
* 递归获取杨辉三角指定行、列(从0开始)的值
注意:与是否创建杨辉三角无关
int getValue(int x, int y) {
if(y <= x && y >= 0){ //这里是保证输入x,y是正确的值
if(y == 0 || x == y){ // 递归终止条件,因为第0列和第(x,y)都是固定值 1
return 1;
}else{
// 递归调用,缩小问题的规模
return getValue(x-1, y-1) + getValue(x-1, y);
}
}
return -1;
}
}
- 第二类问题:问题解法按递归算法实现
(1). 汉诺塔问题
* Title: 汉诺塔问题
* Description:古代有一个梵塔,塔内有三个座A、B、C,A座上有64个盘子,盘子大小不等,大的在下,小的在上。
* 有一个和尚想把这64个盘子从A座移到C座,但每次只能允许移动一个盘子,并且在移动过程中,3个座上的盘子始终保持大盘在下,
* 小盘在上。在移动过程中可以利用B座。要求输入层数,运算后输出每步是如何移动的。
*
public class HanoiTower {
/*
*在程序中,我们把最上面的盘子称为第一个盘子,把最下面的盘子称为第N个盘子
*
*盘子的个数
*盘子的初始地址
*转移盘子时用于中转
*盘子的目的地址
*/
public static void moveDish(int level, char from, char inter, char to) {
if (level == 1) { // 递归终止条件
printf("从 %c 移动盘子 %d号到 %c",from,level,to)
} else {
/* 递归调用:将level-1个盘子从from移到inter(不是一次性移动,每次只能移动一个盘子,
其中to用于周转)*/
moveDish(level - 1, from, to, inter); // 递归调用,缩小问题的规模
// 将第level个盘子从A座移到C座
printf("从 %c 移动盘子 %d号到 %c",from,level,to)
// 递归调用:将level-1个盘子从inter移到to,from 用于周转
moveDish(level - 1, inter, from, to); // 递归调用,缩小问题的规模
}
}
//这之后的问题,不是简单的递归问题,与其他思想结合,暂时不懂,多是直接复制,并没有自己的理解,先放着
- 第三类问题:数据的结构是按递归定义的
(1). 二叉树深度
/**
* Title: 递归求解二叉树的深度
* Description:
* @author rico
* @created 2017年5月8日 下午6:34:50
*/
public class BinaryTreeDepth {
/**
* @description 返回二叉数的深度
* @author rico
* @param t
* @return
*/
public static int getTreeDepth(Tree t) {
// 树为空
if (t == null) // 递归终止条件
return 0;
int left = getTreeDepth(t.left); // 递归求左子树深度,缩小问题的规模
int right = getTreeDepth(t.left); // 递归求右子树深度,缩小问题的规模
return left > right ? left + 1 : right + 1;
}
}
(2). 二叉树深度
/**
* @description 前序遍历(递归)
* @author rico
* @created 2017年5月22日 下午3:06:11
* @param root
* @return
*/
public String preOrder(Node<E> root) {
StringBuilder sb = new StringBuilder(); // 存到递归调用栈
if (root == null) { // 递归终止条件
return ""; // ji
}else { // 递归终止条件
sb.append(root.data + " "); // 前序遍历当前结点
sb.append(preOrder(root.left)); // 前序遍历左子树
sb.append(preOrder(root.right)); // 前序遍历右子树
return sb.toString();
}
}