数据结构与算法

数据结构与算法知识详解
帅旋
关注
充电
IT宅站长,技术博主,共享单车手,全网id:arthinking。

递归算法的艺术:递归问题通用解决方法

发布于 2024-03-11 | 更新于 2024-03-11

1、引言

递归是算法设计中的一种基本技术,简单来说,就是通过函数调用自身以解决问题

通过递归,我们可以将复杂问题分解为更小、更易管理的子问题,直到达到一个简单的条件可以直接解决。

直接说原理太枯燥了,我们结合实际来说明,本文将通过斐波那契数列这一经典问题,探索递归算法的奥秘。

2、递归原理

递归函数通过调用自己来重复执行相同的操作。每次函数调用时,问题的规模都应该减小,直到达到一个基线条件,此时函数可以直接返回结果而不再进行进一步的递归调用。

理解递归的关键在于理解每次函数调用都是独立的,拥有自己的执行上下文。

3、递归算法问题解析:斐波那契数列

题目:斐波那契数[1]

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 01 开始,后面的每一项数字都是前面两项数字的和。也就是:

1
2
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n)

这道题也是动态规划的经典题目,不过本文我们先用来说明递归算法。

3.1、解题思路

理解问题

我们需要计算斐波那契数列的第 n 项。这个问题可以自然地分解为计算前两项的和,这与递归算法的思想完美契合。

拆分问题

要计算 F(n),我们先计算 F(n-1) 和 F(n-2),然后将这两个结果相加。这一过程不断重复,直到我们达到基线条件。

找到递归终止条件

递归的基线条件是 F(0) = 0 和 F(1) = 1,即当 n 为 0 或 1 时,可以直接返回结果。

递归步骤

对于 n > 1 的情况,我们通过返回 F(n-1) + F(n-2) 来进行递归调用。

算法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Fibonacci {

public static int fibonacci(int n) {
if (n <= 1) {
return n;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}

public static void main(String[] args) {
// 测试用例
System.out.println("Fibonacci(0): " + fibonacci(0)); // 应输出 0
System.out.println("Fibonacci(1): " + fibonacci(1)); // 应输出 1
System.out.println("Fibonacci(5): " + fibonacci(5)); // 应输出 5
System.out.println("Fibonacci(10): " + fibonacci(10)); // 应输出 55
}
}

3.2、递归优化技巧

尽管递归解法简洁明了,但它也存在效率问题,特别是在计算较大的 n 时。每次递归调用都可能产生大量的重复计算。通过引入缓存机制,剪枝逻辑或改用迭代方法(迭代方法后面章节继续展开说)可以显著提高效率。

3.2.1、引入缓存机制

在Java中优化斐波那契数列解法的一种常见方法是引入缓存机制,也叫“记忆化”(memoization)。

记忆化是一种优化技术,通过存储之前计算过的结果(例如,在一个数组或映射中)来避免重复计算,从而提高算法效率。

以下是一个使用记忆化来优化斐波那契数列计算的Java实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class FibonacciMemoization {

// 声明一个数组用于存储斐波那契数列的结果,以n为索引
private static long[] memo;

public static long fibonacci(int n) {
// 初始化memo数组,全部填充为-1,表示这些值尚未计算
if (memo == null) {
memo = new long[n + 1];
for (int i = 0; i <= n; i++) {
memo[i] = -1;
}
}

// 如果该值已经被计算过,则直接返回结果
if (memo[n] != -1) {
return memo[n];
}

// 基线条件
if (n <= 1) {
memo[n] = n;
} else {
// 计算斐波那契数,并将结果存储在memo数组中
memo[n] = fibonacci(n - 1) + fibonacci(n - 2);
}
return memo[n];
}

public static void main(String[] args) {
// 测试用例
int n = 50; // 大于之前的例子,用来展示优化的效果
System.out.println("Fibonacci(" + n + "): " + fibonacci(n));
// 由于递归深度,这里使用更大的n值来演示优化效果
}
}

使用记忆化后,即使是计算较大的斐波那契数(例如fibonacci(50)),这种方法也能迅速给出结果,大大提高了效率。这种技术可以广泛应用于其他需要重复计算同一结果的递归算法中。

3.2.3、剪枝法

剪枝逻辑可以帮助提高效率,特别是在处理大量数据或复杂问题时。剪枝逻辑指的是在递归过程中提前终止那些不会导致最优或所需解的路径。

总结

递归提供了一种强大而优雅的方式来解决那些可以分解为相似子问题的复杂问题。

递归题目解决思路

对于递归算法,我们一般的解题思路如下:

  • 1、识别可递归性质的问题:确认问题是否可以通过分解成更小的相同问题来解决;检查问题是否有自然的分而治之(divide and conquer)特性,即是否可以将问题分解成一系列更小的子问题;
  • 2、确定递归函数的参数:确定哪些参数是必要的,以便递归函数在每次调用时都能准确地工作,参数可能包括待处理的数据,递归层数,以及其他用于控制递归过程的变量;
  • 3、定义递归终止条件:如果没有递归终止条件,递归就没法停止了;
  • 4、编写递归步骤:开始编码;
  • 5、考虑优化:识别是否有重复计算,考虑使用记忆化来存储已解决子问题的结果;考虑是否可以将递归算法转换为迭代算法来优化空间复杂度;
  • 6、测试和验证:编写简单的例子验证递归逻辑,重点验证极限条件(递归终止条件),小规模的问题,以及边界情况。

递归代码书写模板

以下是递归代码的书写模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class Solution {

// 解决问题的方法
public void solveProblem(参数列表) {
// 递归调用的入口方法
recursiveFunction(初始参数, 初始状态);
}

// 递归函数
private void recursiveFunction(参数列表, 状态) {
// 递归终止条件
if (满足终止条件) {
// 处理结果
return; // 或者返回某个值
}

// 剪枝逻辑
if (满足剪枝条件) {
// 直接返回,不再继续递归
return; // 或者根据需要调整逻辑
}

// 处理当前层逻辑
processCurrentLevel(参数, 状态);

// 递归调用
// 注意,这里通常会有一些循环逻辑,用于遍历当前层的所有可能性
for (所有可能的下一步) {
// 修改参数或状态
修改参数或状态以准备下一次递归调用;

// 进行下一次递归调用
recursiveFunction(修改后的参数, 修改后的状态);

// 如果需要,恢复参数或状态,以便进行下一次循环
恢复参数或状态;
}

// 如果需要,整合结果
}

// 这个方法用于处理当前层逻辑,具体实现根据问题而定
private void processCurrentLevel(参数列表, 状态) {
// 实现细节
}
}

递归算法书写要点:

  • 先写递归终止条件;
  • 考虑是否可以剪枝;
  • 处理当前层;
  • 下探到下一层;
  • 如果有必要,恢复参数获证状态状态。

References


  1. 斐波那契数 ↩︎

本文作者: 帅旋

本文链接: https://www.itzhai.com/columns/dsa/recursion.html

版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。

×
IT宅

关注公众号及时获取网站内容更新。