递归算法:原理、应用与实例分析
1. 递归的基本概念
递归是一种强大的编程技术,可用于解决重复性问题。若一个问题能分解为一系列与整体问题相同的较小子问题,那么就可以使用递归方法来解决。不过,递归并非解决问题的必要手段,任何能用递归解决的问题,也可以通过迭代(使用循环)来解决。实际上,递归算法通常比迭代算法效率低,因为每次方法调用时,JVM 需要执行一些操作,如为参数和局部变量分配内存,存储方法结束后控制返回的程序位置地址,这些操作被称为开销,而循环则不需要这些开销。
但对于某些重复性问题,使用递归比迭代更容易解决。虽然迭代算法可能执行速度更快,但程序员可能能更快地设计出递归算法。
一般来说,递归方法的工作方式如下:
- 如果问题现在可以不使用递归解决,那么方法就直接解决并返回结果。
- 如果问题现在无法解决,那么方法将其简化为一个更小但相似的问题,并调用自身来解决这个更小的问题。
为了应用这种方法,首先要确定至少一个可以不使用递归解决问题的情况,这被称为基本情况(base case);其次,要确定在其他所有情况下使用递归解决问题的方法,这被称为递归情况(recursive case)。在递归情况下,必须始终将问题简化为原始问题的较小版本。通过每次递归调用减少问题规模,最终会达到基本情况,递归也会停止。
2. 递归在数学中的应用:阶乘计算
在数学中,
n!
表示数字
n
的阶乘。非负整数的阶乘可以通过以下规则定义:
- 如果
n = 0
,则
n! = 1
- 如果
n > 0
,则
n! = 1 × 2 × 3 × ... × n
将
n!
替换为
factorial(n)
,规则可重写为:
- 如果
n = 0
,则
factorial(n) = 1
- 如果
n > 0
,则
factorial(n) = 1 × 2 × 3 × ... × n
当设计一个递归算法来计算任意数字的阶乘时,基本情况是
n = 0
,即:
if (n == 0)
factorial(n) = 1;
当
n > 0
时,这是递归情况,可表示为:
if (n > 0)
factorial(n) = n × factorial(n - 1);
以下是用 Java 实现的阶乘递归方法:
private static int factorial(int n)
{
if (n == 0)
return 1; // 基本情况
else
return n * factorial(n - 1);
}
以下是一个演示该方法的程序:
import javax.swing.JOptionPane;
/**
* 这个程序演示了递归阶乘方法。
*/
public class FactorialDemo
{
public static void main(String[] args)
{
String input; // 用于保存用户输入
int number; // 用于保存一个数字
// 从用户那里获取一个数字
input = JOptionPane.showInputDialog("输入一个非负整数:");
number = Integer.parseInt(input);
// 显示该数字的阶乘
JOptionPane.showMessageDialog(null,
number + "! 是 " + factorial(number));
System.exit(0);
}
/**
* 阶乘方法使用递归计算其参数的阶乘,假设参数是非负数字。
* @param n 用于计算的数字
* @return n 的阶乘
*/
private static int factorial(int n)
{
if (n == 0)
return 1; // 基本情况
else
return n * factorial(n - 1);
}
}
在这个程序的示例运行中,当
factorial
方法的参数
n
为 4 时,由于
n
不等于 0,
if
语句的
else
子句会执行
return n * factorial(n - 1);
语句。但在返回值确定之前,必须先确定
factorial(n - 1)
的值。
factorial
方法会递归调用,直到第 5 次调用时,
n
参数将被设置为 0。
这也说明了为什么递归算法必须在每次递归调用时减少问题规模。最终,递归必须停止才能得到解决方案。如果每次递归调用处理的是问题的较小版本,那么递归调用就会朝着基本情况推进。基本情况不需要递归,因此它会停止递归调用链。通常,问题通过在每次递归调用时使一个或多个参数的值变小来简化。在阶乘方法中,参数
n
的值在每次递归调用时逐渐接近 0,当
n
达到 0 时,方法会返回一个值,而不再进行另一次递归调用。
3. 直接递归和间接递归
到目前为止讨论的示例展示了直接调用自身的递归方法,这被称为直接递归。在程序中也可能创建间接递归,即方法 A 调用方法 B,而方法 B 又调用方法 A,甚至可能涉及多个方法。例如,方法 A 调用方法 B,方法 B 调用方法 C,方法 C 再调用方法 A。
4. 递归方法的实例分析
4.1 数组元素范围求和
rangeSum
方法使用递归对数组元素的指定范围进行求和。该方法接受三个参数:包含要求和元素范围的整数数组、范围的起始元素索引和范围的结束元素索引。
以下是
rangeSum
方法的定义:
public static int rangeSum(int[] array, int start, int end)
{
if (start > end)
return 0;
else
return array[start] + rangeSum(array, start + 1, end);
}
该方法的基本情况是当
start
参数大于
end
参数时,方法返回 0。否则,方法返回
array[start]
加上递归调用
rangeSum
方法的返回值。在递归调用中,范围的起始元素索引为
start + 1
,本质上是“返回范围中第一个元素的值加上范围中其余元素的和”。
以下是演示该方法的程序:
/**
* 这个程序演示了递归 rangeSum 方法。
*/
public class RangeSum
{
public static void main(String[] args)
{
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
System.out.print("元素 2 到 5 的和是 " + rangeSum(numbers, 2, 5));
}
/**
* rangeSum 方法计算数组中指定范围元素的和。
* @param start 指定起始元素
* @param end 指定结束元素
* @return 范围的和
*/
public static int rangeSum(int[] array, int start, int end)
{
if (start > end)
return 0;
else
return array[start] + rangeSum(array, start + 1, end);
}
}
程序输出:
元素 2 到 5 的和是 18
4.2 绘制同心圆
Circles
小程序使用递归绘制同心圆。同心圆是大小不同、一个套一个且具有共同中心点的圆。以下是小程序的代码:
import javax.swing.*;
import java.awt.*;
/**
* 这个小程序使用递归方法绘制同心圆。
*/
public class Circles extends JApplet
{
/**
* init 方法
*/
public void init()
{
getContentPane().setBackground(Color.white);
}
/**
* paint 方法
* @param g 小程序的 Graphics 对象
*/
public void paint(Graphics g)
{
// 绘制 10 个同心圆。最外层圆的包围矩形应位于 (5, 5),宽 300 像素,高 300 像素。
drawCircles(g, 10, 5, 300);
}
/**
* drawCircles 方法绘制同心圆。
* @param g Graphics 对象
* @param n 要绘制的圆的数量
* @param topXY 最外层圆的包围矩形的左上角坐标
* @param size 最外层圆的包围矩形的宽度和高度
*/
private void drawCircles(Graphics g, int n, int topXY, int size)
{
if (n > 0)
{
g.drawOval(topXY, topXY, size, size);
drawCircles(g, n - 1, topXY + 15, size - 30);
}
}
}
drawCircles
方法从
paint
方法调用,使用递归绘制同心圆。
n
参数表示要绘制的圆的数量,当
n
为 0 时,方法达到基本情况。否则,它调用
g
对象的
drawOval
方法绘制一个圆。
topXY
参数是包围矩形的左上角坐标,
size
参数是包围矩形的宽度和高度。绘制完一个圆后,
drawCircles
方法会递归调用,参数值会调整为绘制下一个圆。
4.3 斐波那契数列计算
斐波那契数列是一个著名的数学序列,在该序列中,除前两个数字外,每个数字都是前两个数字的和。斐波那契数列可以定义为:
- 如果
n = 0
,则
Fib(n) = 0
- 如果
n = 1
,则
Fib(n) = 1
- 如果
n > 2
,则
Fib(n) = Fib(n - 1) + Fib(n - 2)
以下是计算斐波那契数列中第
n
个数字的递归 Java 方法:
public static int fib(int n)
{
if (n == 0)
return 0;
else if (n == 1)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
该方法实际上有两个基本情况:当
n
等于 0 和当
n
等于 1 时,在这两种情况下,方法都会返回一个值,而不进行递归调用。以下是演示该方法的程序,它显示斐波那契数列的前 10 个数字:
/**
* 这个程序演示了递归 fib 方法。
*/
public class FibNumbers
{
public static void main(String[] args)
{
System.out.println("斐波那契数列的前 10 个数字是:");
for (int i = 0; i < 10; i++)
System.out.print(fib(i) + " ");
System.out.println();
}
/**
* fib 方法计算斐波那契数列中的第 n 个数字。
* @param n 要计算的第 n 个数字
* @return 第 n 个数字
*/
public static int fib(int n)
{
if (n == 0)
return 0;
else if (n == 1)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
}
程序输出:
斐波那契数列的前 10 个数字是:
0 1 1 2 3 5 8 13 21 34
4.4 最大公约数计算
计算两个正整数
x
和
y
的最大公约数(GCD)也可以使用递归方法。
x
和
y
的 GCD 定义如下:
- 如果
y
能整除
x
,则
gcd(x, y) = y
- 否则,
gcd(x, y) = gcd(y, x % y)
以下是计算 GCD 的递归 Java 方法:
public static int gcd(int x, int y)
{
if (x % y == 0)
return y;
else
return gcd(y, x % y);
}
以下是演示该方法的程序:
import java.util.Scanner;
/**
* 这个程序演示了递归 gcd 方法。
*/
public class GCDdemo
{
public static void main(String[] args)
{
int num1, num2; // 用于计算 GCD 的两个数字
// 创建一个 Scanner 对象用于键盘输入
Scanner keyboard = new Scanner(System.in);
// 从用户那里获取第一个数字
System.out.print("输入一个整数: ");
num1 = keyboard.nextInt();
// 从用户那里获取第二个数字
System.out.print("输入另一个整数: ");
num2 = keyboard.nextInt();
// 显示 GCD
System.out.println("这两个数字的最大公约数是 " + gcd(num1, num2));
}
/**
* gcd 方法计算传入的 x 和 y 参数的最大公约数。
* @param x 一个数字
* @param y 另一个数字
* @return x 和 y 的最大公约数
*/
public static int gcd(int x, int y)
{
if (x % y == 0)
return y;
else
return gcd(y, x % y);
}
}
程序输出示例:
输入一个整数: 49
输入另一个整数: 28
这两个数字的最大公约数是 7
递归算法的流程图示例
graph TD;
A[开始] --> B{问题能否直接解决?};
B -- 是 --> C[解决问题并返回结果];
B -- 否 --> D[将问题简化为更小问题];
D --> E[调用自身解决更小问题];
E --> B;
C --> F[结束];
通过以上这些实例可以看出,递归在解决各种不同类型的问题中都有广泛的应用,并且能够以简洁的方式实现复杂的逻辑。但在使用递归时,需要注意控制递归的终止条件,避免出现无限递归的情况。
5. 递归二分查找方法
二分查找算法也可以使用递归实现。递归二分查找算法比迭代版本更优雅、更易于理解,它也是将问题反复分解为更小部分直至解决的一个很好的例子。
递归二分查找算法的步骤如下:
- 如果
array[middle]
等于搜索值,则找到该值。
- 如果
array[middle]
小于搜索值,则在数组的上半部分进行二分查找。
- 如果
array[middle]
大于搜索值,则在数组的下半部分进行二分查找。
以下是实现该算法的 Java 方法:
public static int binarySearch(int[] array, int first, int last, int value)
{
int middle; // 搜索的中点
// 测试值未找到的基本情况
if (first > last)
return -1;
// 计算中间位置
middle = (first + last) / 2;
// 搜索值
if (array[middle] == value)
return middle;
else if (array[middle] < value)
return binarySearch(array, middle + 1, last, value);
else
return binarySearch(array, first, middle - 1, value);
}
该方法的参数说明如下:
| 参数 | 说明 |
| ---- | ---- |
|
array
| 要搜索的数组 |
|
first
| 搜索范围的第一个元素的下标 |
|
last
| 搜索范围的最后一个元素的下标 |
|
value
| 要搜索的值 |
该方法与迭代版本一样,如果找到值,则返回该值的下标;如果未找到,则返回 -1。以下是演示该方法的程序:
import java.util.Scanner;
/**
* 这个程序演示了递归二分查找方法。
*/
public class RecursiveBinarySearch
{
public static void main(String [] args)
{
int searchValue; // 要搜索的值
int result; // 搜索结果
String input; // 输入的一行
char again; // 用于保存单个字符
// 以下数组中的值按升序排序
int numbers[] = {101, 142, 147, 189, 199, 207, 222,
234, 289, 296, 310, 319, 388, 394,
417, 429, 447, 521, 536, 600};
// 创建一个 Scanner 对象用于键盘输入
Scanner keyboard = new Scanner(System.in);
do
{
// 获取要搜索的值
System.out.print("输入一个要搜索的值: ");
searchValue = keyboard.nextInt();
// 搜索该值
result = binarySearch(numbers, 0, (numbers.length - 1), searchValue);
// 显示结果
if (result == -1)
{
System.out.println(searchValue + " 未找到。");
}
else
{
System.out.println(searchValue + " 在元素 " + result + " 处找到。");
}
// 用户是否要再次搜索?
System.out.print("是否要再次搜索? (Y 或 N): ");
// 消耗剩余的换行符
keyboard.nextLine();
// 读取一行输入
input = keyboard.nextLine();
} while (input.charAt(0) == 'y' || input.charAt(0) == 'Y');
}
/**
* 二分查找方法在整数数组上执行二分查找。
* @param array 要搜索的数组
* @param first 搜索范围的第一个元素
* @param last 搜索范围的最后一个元素
* @param value 要搜索的值
* @return 如果找到值,则返回其下标;否则返回 -1
*/
public static int binarySearch(int[] array, int first, int last, int value)
{
int middle; // 搜索的中点
// 测试值未找到的基本情况
if (first > last)
return -1;
// 计算中间位置
middle = (first + last) / 2;
// 搜索值
if (array[middle] == value)
return middle;
else if (array[middle] < value)
return binarySearch(array, middle + 1, last, value);
else
return binarySearch(array, first, middle - 1, value);
}
}
程序输出示例:
输入一个要搜索的值: 289
289 在元素 8 处找到。
是否要再次搜索? (Y 或 N): y
输入一个要搜索的值: 388
388 在元素 12 处找到。
是否要再次搜索? (Y 或 N): y
输入一个要搜索的值: 101
101 在元素 0 处找到。
是否要再次搜索? (Y 或 N): y
输入一个要搜索的值: 999
999 未找到。
是否要再次搜索? (Y 或 N): n
6. 汉诺塔问题
汉诺塔是一个数学游戏,常用于说明递归的强大之处。该游戏使用三个柱子和一组中间有孔的圆盘,圆盘按大小顺序堆叠在其中一个柱子上,最大的圆盘在底部。
游戏规则如下:
- 每次只能移动一个圆盘。
- 圆盘不能放在比它小的圆盘上面。
- 除移动时外,所有圆盘都必须放在柱子上。
游戏的目标是将所有圆盘从第一个柱子移动到第三个柱子,中间的柱子可以作为临时存放处。
对于不同数量的圆盘,解决方案如下:
- 如果只有一个圆盘,解决方案很简单:将圆盘从柱子 1 移动到柱子 3。
- 如果有两个圆盘,需要三步:
1. 将圆盘 1 移动到柱子 2。
2. 将圆盘 2 移动到柱子 3。
3. 将圆盘 1 移动到柱子 3。
随着圆盘数量的增加,移动的复杂度也会增加。移动三个圆盘需要七步。
整体解决方案可以描述为:使用柱子 2 作为临时柱子,将
n
个圆盘从柱子 1 移动到柱子 3。
以下是模拟该游戏解决方案的递归算法:
graph TD;
A[开始] --> B{是否还有圆盘要移动?};
B -- 是 --> C[将 n - 1 个圆盘从 A 移动到 B,使用 C 作为临时柱子];
C --> D[将剩余的圆盘从 A 移动到 C];
D --> E[将 n - 1 个圆盘从 B 移动到 C,使用 A 作为临时柱子];
E --> B;
B -- 否 --> F[结束];
以下是实现该算法的 Java 方法:
private void moveDiscs(int num, int fromPeg, int toPeg, int tempPeg)
{
if (num > 0)
{
moveDiscs(num - 1, fromPeg, tempPeg, toPeg);
System.out.println("将一个圆盘从柱子 " + fromPeg + " 移动到柱子 " + toPeg);
moveDiscs(num - 1, tempPeg, toPeg, fromPeg);
}
}
该方法的参数说明如下:
| 参数 | 说明 |
| ---- | ---- |
|
num
| 要移动的圆盘数量 |
|
fromPeg
| 要从哪个柱子移动圆盘 |
|
toPeg
| 要将圆盘移动到哪个柱子 |
|
tempPeg
| 作为临时柱子使用的柱子 |
如果
num
大于 0,则表示还有圆盘要移动。第一个递归调用
moveDiscs(num - 1, fromPeg, tempPeg, toPeg);
是将除最后一个圆盘外的所有圆盘从
fromPeg
移动到
tempPeg
,使用
toPeg
作为临时柱子。然后显示一条消息,表示将一个圆盘从
fromPeg
移动到
toPeg
。最后,另一个递归调用
moveDiscs(num - 1, tempPeg, toPeg, fromPeg);
是将除最后一个圆盘外的所有圆盘从
tempPeg
移动到
toPeg
,使用
fromPeg
作为临时柱子。
以下是使用该方法的类:
/**
* 这个类显示汉诺塔游戏的解决方案。
*/
public class Hanoi
{
private int numDiscs; // 圆盘数量
/**
* 构造函数。
* @param n 要使用的圆盘数量
*/
public Hanoi(int n)
{
// 分配圆盘数量
numDiscs = n;
// 将所有圆盘从柱子 1 移动到柱子 3,使用柱子 2 作为临时存储位置
moveDiscs(numDiscs, 1, 3, 2);
}
/**
* moveDiscs 方法显示圆盘移动。
* @param num 要移动的圆盘数量
* @param fromPeg 要从哪个柱子移动
* @param toPeg 要移动到哪个柱子
* @param tempPeg 临时柱子
*/
private void moveDiscs(int num, int fromPeg, int toPeg, int tempPeg)
{
if (num > 0)
{
moveDiscs(num - 1, fromPeg, tempPeg, toPeg);
System.out.println("将一个圆盘从柱子 " + fromPeg + " 移动到柱子 " + toPeg);
moveDiscs(num - 1, tempPeg, toPeg, fromPeg);
}
}
}
以下是演示该类的程序,它显示移动三个圆盘的说明:
/**
* 这个类演示了 Hanoi 类,它显示解决汉诺塔游戏所需的步骤。
*/
public class HanoiDemo
{
static public void main(String[] args)
{
Hanoi towersOfHanoi = new Hanoi(3);
}
}
总结
递归是一种强大的编程技术,在解决各种问题中都有广泛的应用。它通过将问题分解为更小的子问题,利用基本情况和递归情况来,逐步解决问题。虽然递归算法通常比迭代算法效率低,但在某些情况下,它能让程序员更快速地设计出解决方案。
在使用递归时,需要注意以下几点:
- 明确基本情况,确保递归能够终止,避免无限递归。
- 每次递归调用时,要将问题规模缩小,使递归朝着基本情况推进。
- 注意递归的开销,对于大规模问题,可能需要考虑使用迭代方法。
通过阶乘计算、数组元素范围求和、同心圆绘制、斐波那契数列计算、最大公约数计算、二分查找和汉诺塔问题等实例,我们可以看到递归在不同场景下的应用。无论是数学计算还是数据搜索,递归都能以简洁的代码实现复杂的逻辑。希望通过本文的介绍,你能对递归有更深入的理解,并在实际编程中灵活运用递归技术。
1161

被折叠的 条评论
为什么被折叠?



