递归算法入门指南:从原理到实战,轻松掌握编程核心思想
什么是递归?
递归是一种函数直接或间接调用自身的过程,对应的函数称为递归函数。递归算法通过逐步解决问题,并在每一步中调用自身来推进。当我们达到解决方案时,算法就会停止。
由于被调用的函数可能会进一步调用自身,这个过程可能会永远持续下去。因此,提供一个基本情况来终止这个递归过程至关重要。
实现递归的步骤
步骤1 - 定义基本情况
识别最简单(或基础)的情况,这种情况的解决方案是已知的或微不足道的。这是递归的停止条件,防止函数无限调用自身。
步骤2 - 定义递归情况
将问题定义为更小的子问题。将问题分解为自身的更小版本,并递归调用函数来解决每个子问题。
步骤3 - 确保递归终止
确保递归函数最终达到基本情况,不会进入无限循环。
步骤4 - 组合解决方案
组合子问题的解决方案来解决原始问题。
示例1:自然数求和(n=3)
输入:n = 3
输出:6
解释:前3个自然数的和是1+2+3 = 6。
输入:n = 7
输出:28
解释:前7个自然数的和是1+2+3+4+5+6+7 = 28。
基本情况:当 n == 1 时,返回1。对于 n = 3,递归在经历 3 → 2 → 1 后达到此情况。
递归情况:每次调用将 n 加到 sum(n-1) 上,所以 sum(3) = 3 + sum(2),sum(2) = 2 + sum(1)。
#include <iostream>
using namespace std;
int sum(int n) {
// 基本情况
if (n == 1)
return 1;
return n + sum(n - 1);
}
int main() {
int n = 5;
cout << sum(n);
return 0;
}
输出:15
递归解决方案的执行流程
调用按 sum(3) → sum(2) → sum(1) 的顺序堆叠,然后才开始加法运算。结果以相反的顺序相加:sum(1) = 1,sum(2) = 3,sum(3) = 6。
为什么需要递归?
递归有助于逻辑构建。递归思维通过将复杂问题分解为更小的子问题来帮助解决复杂问题。
递归解决方案是动态规划和分治算法的基础。某些问题使用递归可以很容易地解决,如汉诺塔(TOH)、树的中序/前序/后序遍历、图的深度优先搜索等。
递归中的基本情况是什么?
递归程序在基本情况处停止。一个递归中可以有多个基本情况。在上面的程序中,基本情况是当 n = 1 时。
如何使用递归解决特定问题?
思路是将问题表示为一个或多个更小的问题,并添加一个或多个停止递归的基本情况。
示例2:计算阶乘
数字 n(其中 n >= 0)的阶乘是从 1 到 n 的所有正整数的乘积。要递归计算阶乘,我们使用 (n-1) 的阶乘来计算 n 的阶乘。递归函数的基本情况是当 n = 0 时,此时我们返回 1。
def fact(n):
# 基本情况
if n == 0:
return 1
return n * fact(n - 1)
print("5的阶乘:", fact(5))
输出:5的阶乘:120

递归中何时会发生栈溢出错误?
如果未达到或未定义基本情况,则可能出现栈溢出问题。让我们举个例子来理解这一点。
int fact(int n) {
// 错误的基本情况(可能导致栈溢出)
if (n == 100)
return 1;
else
return n * fact(n - 1);
}
在这个例子中,如果调用 fact(10),函数将递归调用 fact(9),然后 fact(8),fact(7),依此类推。然而,基本情况检查 n == 100。由于在这些递归调用期间 n 永远不会达到 100,因此永远不会触发基本情况。结果,递归无限继续。
这种连续的递归会消耗函数调用栈上的内存。如果由于这些无休止的函数调用而耗尽了系统的内存,就会发生栈溢出错误。
为了防止这种情况,必须定义一个适当的基本情况,例如 if (n == 0),以确保递归终止并且函数不会耗尽内存。
理解递归的调用栈和状态变化,是掌握这个核心思想的关键。如果觉得抽象概念难以想象,可以试试用可视化的方式辅助学习。
最近发现一个叫图码的工具特别实用,它提供了超过60种数据结构和算法的交互式动画。
你可以输入自己的递归数据,甚至上传完整的代码,亲眼看到每一步的执行过程和内存状态,这对理清思路帮助巨大。
尤其是准备408考研或应对数据结构期末考试,它的知识点梳理和代码可视化功能简直是复习利器。
强烈建议你去亲手体验一下这种算法可视化的魅力,相信会有全新的理解。
图码-数据结构与算法交互式可视化平台
访问网站:https://totuma.cn
直接递归和间接递归有什么区别?
如果函数在执行期间直接调用自身,则称为直接递归。换句话说,函数在其自身体内对自己进行递归调用。
间接递归函数是调用另一个函数,而该函数又直接或通过其他函数调用原始函数。这创建了一个涉及多个函数的递归调用链,与直接递归不同,直接递归中函数调用自身。
// 直接递归示例
void directRecFun() {
// 一些代码...
directRecFun();
// 一些代码...
}
// 间接递归示例
void indirectRecFun1() {
// 一些代码...
indirectRecFun2();
// 一些代码...
}
void indirectRecFun2() {
// 一些代码...
indirectRecFun1();
// 一些代码...
}
尾递归和非尾递归有什么区别?
当递归调用是函数执行的最后一件事时,递归函数是尾递归的。
递归中如何为不同的函数调用分配内存?
递归使用更多内存来存储内部函数调用栈中每个递归调用的数据。每当我们调用一个函数时,它的记录就会被添加到栈中,并在调用完成之前一直保留在那里。内部系统使用栈是因为函数调用遵循 LIFO(后进先出)结构,最后调用的函数最先完成。
当从 main() 调用任何函数时,内存会在栈上分配给它。递归函数调用自身,被调用函数的内存分配在调用函数的内存之上,并且为每个函数调用创建局部变量的不同副本。当达到基本情况时,函数将其值返回给调用它的函数,内存被释放,过程继续。
让我们通过一个简单的函数来了解递归的工作原理。
class GFG {
static void printFun(int test) {
if (test < 1)
return;
else {
System.out.printf("%d ", test);
printFun(test - 1); // 语句2
System.out.printf("%d ", test);
return;
}
}
public static void main(String[] args) {
int test = 3;
printFun(test);
}
}
输出:3 2 1 1 2 3
初始调用:当从 main() 调用 printFun(3) 时,为 printFun(3) 分配内存。局部变量 test 初始化为 3,语句 1 到 4 被压入栈。
第一次递归调用:printFun(3) 调用 printFun(2)。为 printFun(2) 分配内存,局部变量 test 初始化为 2,语句 1 到 4 被压入栈。
第二次递归调用:printFun(2) 调用 printFun(1)。为 printFun(1) 分配内存,局部变量 test 初始化为 1,语句 1 到 4 被压入栈。
第三次递归调用:printFun(1) 调用 printFun(0)。为 printFun(0) 分配内存,局部变量 test 初始化为 0,语句 1 到 4 被压入栈。
基本情况:当调用 printFun(0) 时,它遇到基本情况(if 语句)并将控制权返回给 printFun(1)。
从递归返回:从 printFun(0) 返回后,执行 printFun(1) 的剩余语句,并将控制权返回给 printFun(2)。类似地,从 printFun(2) 返回后,控制权返回给 printFun(3)。
输出:因此,输出将按以下顺序打印值:从 3 到 1(在进行递归调用时),然后从 1 回到 3(在递归调用展开时)。
内存栈随着每个函数调用而增长,并随着递归展开而缩小,遵循 LIFO 结构。
递归编程相对于迭代编程有哪些优势?
递归提供了一种干净简单的编写代码的方式。有些问题本质上是递归的,如树遍历、汉诺塔等。对于此类问题,最好编写递归代码。我们也可以借助栈数据结构迭代地编写此类代码。
递归编程相对于迭代编程有哪些缺点?
并非每个递归程序都可以迭代编写,反之亦然。递归程序通常需要更多的空间,并且需要更多时间来维护递归调用栈。
递归会使代码更难理解和调试,因为它需要考虑多个级别的函数调用。
示例3:使用递归计算斐波那契数列
编写一个程序和递推关系来找到 n >= 0 的斐波那契数列。
数学方程:
如果 n == 0, n == 1,则 fib(n) = n
否则 fib(n) = fib(n-1) + fib(n-2)
递推关系:T(n) = T(n-1) + T(n-2) + O(1)
def fib(n):
# 停止条件
if n == 0:
return 0
# 停止条件
if n == 1 or n == 2:
return 1
# 递归函数
else:
return fib(n - 1) + fib(n - 2)
n = 5
print("5个数的斐波那契数列是:", end=" ")
for i in range(n):
print(fib(i), end=" ")
输出:5个数的斐波那契数列是:0 1 1 2 3

递归的常见应用
树和图遍历
用于系统地探索树和图等数据结构中的节点/顶点。
排序算法
像快速排序和归并排序这样的算法将数据划分为子数组,递归地对它们进行排序,然后合并它们。
分治算法
像二分查找这样的算法使用递归将问题分解为更小的子问题。
分形生成
递归通过重复应用递归公式来帮助生成分形图案,如曼德博集合。
回溯算法
用于需要一系列决策的问题,其中递归探索所有可能的路径并在需要时回溯。
记忆化
涉及缓存递归函数调用的结果,以避免重新计算昂贵的子问题。
这些只是递归在计算机科学和编程中众多应用中的几个例子。递归是一种多功能且强大的工具,可用于解决许多不同类型的问题。
递归总结
- 递归中有两种类型的案例:递归案例和基本情况
- 基本情况用于在案例为真时终止递归函数
- 每个递归调用都会在栈内存中创建该方法的新副本
- 无限递归可能导致栈内存耗尽
- 递归算法示例:归并排序、快速排序、汉诺塔、斐波那契数列、阶乘问题等
递归是编程中的核心概念,掌握它对于解决复杂问题和理解高级算法至关重要。通过练习和实际应用,你将能够更好地理解和运用递归思想。
920

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



