算法通关村第七关——一篇理解递归(青铜)
递归的特征
递归是一种在计算机科学中经常使用的重要技术,它能够解决许多问题。本文将对递归的特征进行深入探讨,并通过适当的图示来进行解释。
首先,我们来了解什么是递归。递归是指一个函数在其定义中调用自身的过程。简而言之,递归就是通过把一个大问题不断地分解为更小的子问题来解决问题的方法。
递归有以下几个特征:
- 基本情况:递归函数必须有一个基本情况,即递归终止条件。这是因为如果没有终止条件,递归就会无限地进行下去,导致堆栈溢出。基本情况通常是最小的、不需要再次递归的情况。
- 问题规模缩小:递归函数必须能够将原问题转化为一个或多个规模更小的子问题。这是通过参数的改变来实现的。每次递归调用都会使问题的规模减小,直到达到基本情况。
- 递归调用:递归函数在其定义中调用自身。这是递归的核心部分,通过递归调用来解决问题。在每次递归调用中,函数的参数会发生变化,这样就能处理不同的子问题。
- 合并结果:递归函数在解决完所有子问题后,需要将各个子问题的结果合并得到最终的解。这是通过递归函数的返回值来实现的。每次递归调用都会返回一个结果,这些结果最终会被合并起来。
例子讲解:
那么我用一个例子来讲解递归的这个特征:
让我们以计算阶乘为例来说明递归的特征。
假设我们要计算一个整数n的阶乘,可以使用递归函数来解决。
-
首先,我们需要定义递归函数。在这个例子中,我们可以定义一个名为factorial的函数,它接受一个整数n作为参数,并返回n的阶乘。
-
其次,我们需要确定基本情况。对于阶乘来说,最基本的情况是当n等于0或1时,阶乘的结果都是1。所以我们可以将基本情况定义为:当n等于0或1时,直接返回1。
-
接下来,我们需要缩小问题的规模。在计算阶乘时,我们可以将问题转化为计算(n-1)的阶乘,然后再将结果乘以n。所以我们可以通过递归调用来缩小问题的规模。
-
最后,我们需要将各个子问题的结果合并得到最终的解。在这个例子中,我们可以将递归调用的结果乘以n,然后返回。
下面是一个简单的Java代码实现:
public class Factorial {
public static int factorial(int n) {
// 基本情况
if (n == 0 || n == 1) {
return 1;
}
// 缩小问题规模,并递归调用
int result = factorial(n - 1);
// 合并结果
return result * n;
}
public static void main(String[] args) {
int n = 5;
int result = factorial(n);
System.out.println(result);
}
}
在这个例子中,我们调用factorial(5),首先判断基本情况,由于n不等于0或1,所以将问题转化为计算factorial(4)的结果。然后继续递归调用,直到达到基本情况。
递归调用的过程如下图所示:
factorial(5) -> factorial(4) -> factorial(3) -> factorial(2) -> factorial(1)
| | | | |
V V V V V
return 120 return 24 return 6 return 2 return 1
最终,我们将各个子问题的结果合并得到最终的解:5 * 4 * 3 * 2 * 1 = 120,所以factorial(5)的结果是120。
通过这个例子,我们可以看到递归函数具有基本情况、问题规模缩小、递归调用和合并结果这几个特征。在实际编程中,可以根据具体问题的特点使用递归来解决。
如何写递归
例子1:计算斐波那契数列
好的,下面将逐步详细讲解递归算法的实现:
- 定义递归函数的参数和返回值:
public int fibonacci(int n) {
//...
}
这里定义了一个递归函数fibonacci
,它接受一个整数参数n,并返回斐波那契数列的第n项。
- 定义递归的结束条件:
if (n == 0 || n == 1) {
return n;
}
在上述代码中,判断n是否为0或1,如果是,则直接返回n。这表示当n为0或1时,递归不再继续执行,而是直接返回结果。
- 缩小问题规模:
int result = fibonacci(n - 1) + fibonacci(n - 2);
在这段代码中,通过调用自身来求解斐波那契数列的前两项的和。通过将问题规模缩小,即通过计算第n-1项和第n-2项的和,来求解第n项。
- 调用递归函数:
int result = fibonacci(n - 1) + fibonacci(n - 2);
在上述代码中,通过调用递归函数fibonacci
,传入参数n-1和n-2,来求解第n项。
- 处理递归结果:
return result;
最后,在递归函数中通过返回变量result
来返回计算得到的第n项的值。
综合起来,完整的代码如下:
public class Fibonacci {
public int fibonacci(int n) {
// 递归结束条件
if (n == 0 || n == 1) {
return n;
}
// 调用递归函数
int result = fibonacci(n - 1) + fibonacci(n - 2);
// 处理递归结果
return result;
}
}
这个递归算法通过不断调用自身,将求解问题的规模从n缩小到n-1和n-2,直到达到递归结束条件为止。
例子2:计算阶乘为例
当然,接下来我们将以经典的例子——计算阶乘为例,详细讲解递归算法的步骤:
- 定义递归函数的参数和返回值:
public int factorial(int n) {
//...
}
这里定义了一个递归函数factorial
,它接受一个整数参数n,并返回n的阶乘。
- 定义递归的结束条件:
if (n == 0 || n == 1) {
return 1;
}
在上述代码中,判断n是否为0或1,如果是,则直接返回1。这表示当n为0或1时,递归不再继续执行,而是直接返回结果1。
- 缩小问题规模:
int result = n * factorial(n - 1);
在这段代码中,通过调用自身来求解n的阶乘。通过将问题规模缩小,即通过计算n * (n-1)!,来求解n!
- 调用递归函数:
int result = n * factorial(n - 1);
在上述代码中,通过调用递归函数factorial
,传入参数n-1,来求解n的阶乘。
- 处理递归结果:
return result;
最后,在递归函数中通过返回变量result
来返回计算得到的n的阶乘的值。
综合起来,完整的代码如下:
public class Factorial {
public int factorial(int n) {
// 递归结束条件
if (n == 0 || n == 1) {
return 1;
}
// 调用递归函数
int result = n * factorial(n - 1);
// 处理递归结果
return result;
}
}
这个递归算法通过不断调用自身,将求解问题的规模从n缩小到n-1,直到达到递归结束条件为止。通过乘以n来计算阶乘,最终得到结果。
例子3:二叉树的遍历
当然,还可以举一个例子来讲解递归算法的实现。这次我们以二叉树的遍历为例,详细讲解步骤:
假设有如下定义的二叉树节点类:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
- 定义递归函数的参数和返回值:
public void inorderTraversal(TreeNode root) {
//...
}
这里定义了一个递归函数inorderTraversal
,它接受一个二叉树的根节点作为参数,没有返回值。
- 定义递归的结束条件:
if (root == null) {
return;
}
在上述代码中,判断根节点是否为空,如果是,则直接返回。这表示当遍历到空节点时,递归不再继续执行,而是直接返回。
- 缩小问题规模:
inorderTraversal(root.left);
System.out.println(root.val);
inorderTraversal(root.right);
在这段代码中,通过调用自身来遍历左子树和右子树。通过将问题规模缩小,即分别遍历左子树和右子树,来遍历整个二叉树。
- 调用递归函数:
inorderTraversal(root.left);
在上述代码中,通过调用递归函数inorderTraversal
,传入参数root.left,来遍历左子树。
- 处理递归结果:
System.out.println(root.val);
在递归函数中,对于每个节点的值进行处理,可以通过打印节点的值,来实现中序遍历。
综合起来,完整的代码如下:
public class BinaryTreeTraversal {
public void inorderTraversal(TreeNode root) {
// 递归结束条件
if (root == null) {
return;
}
// 调用递归函数
inorderTraversal(root.left);
System.out.println(root.val);
inorderTraversal(root.right);
}
}
这个递归算法通过不断调用自身,将二叉树的遍历问题转化为对左子树和右子树的遍历问题,直到达到递归结束条件为止。通过打印节点的值,实现了中序遍历。
怎么看懂递归代码
当你要理解递归代码时,可以按照以下步骤来进行:
-
理解递归函数的功能和作用:首先要明确递归函数的目的是什么,它要解决什么问题。在例子中,我们以计算斐波那契数列的第n项为例。
-
理解递归结束条件:找到递归函数中的结束条件,即满足这个条件时不再继续递归执行,直接返回结果。在例子中,当n为0或1时,递归结束,直接返回n。
-
理解递归函数的调用过程:理解递归函数在每次调用时的具体操作。在例子中,每次调用
fibonacci(n - 1)
和fibonacci(n - 2)
来求解前两项的和。 -
理解递归函数的参数传递:注意递归函数每次调用时传入的参数是如何发生变化的,这是问题规模缩小的关键。在例子中,每次传入的参数是n-1和n-2。
-
理解递归函数的返回值处理:了解递归函数每次调用后返回的结果如何被处理。在例子中,返回的结果通过相加得到最终的结果。
下面结合计算斐波那契数列的第n项的代码来一步步详细讲解:
public class Fibonacci {
public int fibonacci(int n) {
// 递归结束条件
if (n == 0 || n == 1) {
return n;
}
// 调用递归函数
int result = fibonacci(n - 1) + fibonacci(n - 2);
// 处理递归结果
return result;
}
}
假设要计算斐波那契数列的第5项,即调用fibonacci(5)
。
- 首先进入
fibonacci(5)
函数。 - 因为5不满足递归结束条件,继续执行下面的代码。
- 首先调用
fibonacci(4)
,继续向下递归。 fibonacci(4)
不满足递归结束条件,继续执行下面的代码。- 调用
fibonacci(3)
并向下递归。 fibonacci(3)
不满足递归结束条件,继续执行下面的代码。- 调用
fibonacci(2)
并向下递归。 fibonacci(2)
不满足递归结束条件,继续执行下面的代码。- 调用
fibonacci(1)
并向下递归。 fibonacci(1)
满足递归结束条件,直接返回1。
下面满足递归结束条件:
-
回到上一层递归,继续执行
fibonacci(2)
后面的代码。 -
调用
fibonacci(0)
并向下递归。 -
fibonacci(0)
满足递归结束条件,直接返回0。 -
回到上一层递归,继续执行
fibonacci(2)
后面的代码。 -
将
fibonacci(1)
的结果和fibonacci(0)
的结果相加,得到结果1。 -
回到上一层递归,继续执行
fibonacci(3)
后面的代码。 -
将
fibonacci(2)
的结果和fibonacci(1)
的结果相加,得到结果2。 -
回到上一层递归,继续执行
fibonacci(4)
后面的代码。 -
将
fibonacci(3)
的结果和fibonacci(2)
的结果相加,得到结果3。 -
回到上一层递归,继续执行
fibonacci(5)
后面的代码。 -
将
fibonacci(4)
的结果和fibonacci(3)
的结果相加,得到结果5。 -
最终返回结果5。
通过以上步骤,我们可以看到递归函数的调用过程,每次调用都在问题规模上做了缩小,直到满足递归结束条件为止。然后根据递归结果进行处理,最终得到计算斐波那契数列第n项的结果。