算法思维基础
递归(Recursion)
函数定义中使用函数自身的方法
1、递归问题可以分解为若干个规模较小,与原问题形式相同的子问题。
并且这些子问题可以用完全相同的解决思路来解决
2、递归问题的演化过程是一个对原问题从大到小进行拆解的过程,并且有一个明确的终点(临界点)。
一旦原问题到达临界点,不再往更小的问题拆解。
最后从这个临界点开始,将小问题的答案按照原路返回。原问题得以解决
递归的基本思想就是将大规模问题转化为规模小的相同子问题来解决
大问题和小问题是一样的问题,因此大问题的解决方法和小问题的相同,产生了函数调用自身的情况
解决问题的函数有明确的结束条件,否则导致无限递归。
递归的实现:递归主体 + 终止条件
递归的算法思想
数学归纳法
一个常见的题目是:证明当 n 等于任意一个自然数时某命题成立。
当采用数学归纳法时,证明分为以下 2 个步骤:
- 证明当 n = 1 时命题成立;
- 假设 n = m 时命题成立,那么尝试推导出在 n = m + 1 时命题也成立。
与数学归纳法类似,当采用递归算法解决问题时,也需要围绕这 2 个步骤去做文章:
- 大规模问题分解为几个小规模的同样问题;
- 通过多轮分解后最终的结果,终止条件如何定义
所以当一个问题同时满足以下 2 个条件时,就可以使用递归的方法求解:
- 可以拆解为除了数据规模以外,求解思路完全相同的子问题;
- 存在终止条件。
中序遍历:递归实现树的遍历
对树中的任意结点来说,
1、先中序遍历它的左子树,
2、然后打印这个结点,
3、最后中序遍历它的右子树。
若某个结点没有左子树和右子树,
直接打印结点,完成终止。
由此可见,树的中序遍历完全满足递归的两个条件,可以通过递归实现。
// 中序遍历
public static void inOrderTraverse(Node node) {
if (node == null)
return;
inOrderTraverse(node.left);
System.out.print(node.data + " ");
inOrderTraverse(node.right);
}
递归代码的关键:递推公式 和 终止条件
1、找到将大问题分解成小问题的规律,基于此写出递推公式,
2、然后找出终止条件,找到最简单的问题时,如何写出答案
3、最终将递推公式和终止条件翻译成实际代码
汉诺塔问题
从左到右有 x、y、z 三根柱子,其中 x 柱子上面有从小叠到大的 n 个圆盘。现要求将 x 柱子上的圆盘移到 z 柱子上去。要求是,每次只能移动一个盘子,且大盘子不能被放在小盘子上面。求移动的步骤。
大规模的复杂问题,先把问题化简
原问题:把从小到大的 n 个盘子,从 x 移动到 z
拆解为以下 3 个小问题:
- 把从小到大的 n-1 个盘子,从 x 移动到 y;
- 接着把最大的一个盘子,从 x 移动到 z;
- 再把从小到大的 n-1 个盘子,从 y 移动到 z。
一、首先,判断是否满足递归的第一个条件
第 1 和第 3 个问题就是汉诺塔问题。
完成把大问题缩小为完全一样的小规模问题。
已经定义好递归体,满足递归的第一个条件
二、然后,判断是否满足终止条件
递归体不断缩小范围,汉诺塔问题由原来“移动从小到大的 n 个盘子”,缩小为“移动从小到大的 n - 1 个盘子”,直到缩小为“移动从小到大的 1 个盘子”。
移动最小的那个盘子,规则发现,最小的盘子可以自由移动。因此,递归的终止条件也满足
三、实现
汉诺塔问题可以用递归实现。定义汉诺塔的递归函数为 hanoi()。函数的输入参数:
- 3 根柱子的标记 x、y、z;
- 待移动的盘子数量 n。
hanoi(n, x, y, z),代表了把 n 个盘子由 x 移动到 z。
根据分析,可知递归体包含 3 个步骤:
- 把从小到大的 n - 1 个盘子从 x 移动到 y。调用 hanio(n-1, x, z, y);
- 再把最大的一个盘子从 x 移动到 z。直接完成一次移动的动作就可以了;
- 再把从小到大的 n - 1 个盘子从 y 移动到 z。调用 hanoi(n - 1, y, x, z)。
对于终止条件则需要判断 n 的大小。如果 n 等于 1,那么同样直接移动就可以了。
public static void main(String[] args) {
String x = "x";
String y = "y";
String z = "z";
hanoi(3, x, y, z);
}
public void hanoi(int n, String x, String y, String z) {
if (n < 1) {
System.out.println("汉诺塔的层数不能小于1");
} else if (n == 1) {
System.out.println("移动: " + x + " -> " + z);
return;
} else {
hanoi(n - 1, x, z, y);
System.out.println("移动: " + x + " -> " + z);
hanoi(n - 1, y, x, z);
}
}
在主函数中,执行了 hanoi(3, “x”, “y”, “z”)。发现 3 比 1 要大,则进入递归体。
分别先后执行了 hanoi(2, “x”, “z”, “y”)、“移动: x->z”、hanoi(2, “y”, “x”, “z”)
其中的 hanoi(2, “x”, “z”, “y”),又先后执行了 hanoi(1, “x”, “y”, “z”)、“移动: x->y”、hanoi(1, “z”, “x”, “y”)。
在这里,hanoi(1, “x”, “y”, “z”) 的执行结果是 “移动: x->z”,hanoi(1, “z”, “x”, “y”)的执行结果是"移动: z->y"
另一边,hanoi(2, “y”, “x”, “z”) 则要先后执行 hanoi(1, “y”, “z”, “x”)、“移动: y->z”、hanoi(1, “x”, “y”, “z”)。
在这里,hanoi(1, “y”, “z”, “x”) 的执行结果是"移动: y->x",hanoi(1, “x”, “y”, “z”) 的执行结果是 “移动: x->z”
代码执行的结果
移动: x->z
移动: x->y
移动: z->y
移动: x->z
移动: y->x
移动: y->z
移动: x->z
处理输入异常的代码部分不谈,代码包含了 2 个部分:
- 终止条件,即如何处理小规模的问题,实现的代码量一定是很少的;
- 递归体,即大问题向小问题分解的过程,实现的代码量也不会太多。
总结
递归的核心思想是把规模大的问题转化为规模小的相似的子问题
解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况。解决问题的函数必须有明显的结束条件,这样就不会产生无限递归的情况
练习
斐波那契数列是:0,1,1,2,3,5,8,13,21,34,55,89,144……。你会发现,这个数列中元素的性质是,某个数等于它前面两个数的和;也就是 a[n+2] = a[n+1] + a[n]。至于起始两个元素,则分别为 0 和 1。在这个数列中的数字,就被称为斐波那契数。
现在的问题是,写一个函数,输入 x,输出斐波那契数列中第 x 位的元素。例如,输入 4,输出 2;输入 9,输出 21。要求:需要用递归的方式来实现
package recursion;
import java.util.Scanner;
/**
* @author hym
* @date 2021/11/8
*/
public class Fibonacci {
public static int getNumberByIndex(int index) {
if (index <= 2) {
return index - 1;
}
return getNumberByIndex(index - 1) + getNumberByIndex(index - 2);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int index = scanner.nextInt();
int result = getNumberByIndex(index);
System.out.println(result);
}
}