目录
递归算法概述
递归算法的基本定义
递归(Recursion)在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法,其核心思想是分治策略。 递归式方法可以被用于解决很多的计算机科学问题,因此它是计算机科学中十分重要的一个概念。绝大多数编程语言支持函数的自调用,在这些语言中函数可以通过调用自身来进行递归。
递归算法的目标和功能
在日常开发中,我们使用循环语句远远大于递归,但这不能说明递归就没有用武之地,实际上递归算法的解决问题的步骤更符合人类解决问题的思路,这是递归算法的优点,同时也是它的缺点。递归算法是比较好用,但是理解起来可能不太好理解,所以在递归算法和循环算法对比中,流行一句话:人理解循环,神理解递归。当然这只是一个段子,不过也从侧面反映出递归算法不容易理解的事实。这个我自己也深有体会,就拿排序算法里面的快排和归并排序来说吧,这两种算法采用的都是分治思想来处理排序问题,所以递归在这里就出现了,如果你不理解递归算法,就去学习这两种排序算法,可能理解起来就非常费事,尽管你知道这两种排序的算法原理和它的时间及空间复杂度,但就是不知道它是如何使用递归完成的,所以学习和理解递归算法是非常有必要的。
实际上递归算法的使用场景,远不止上面说的排序算法,在链表,树,图及其他只要符合分治思想的问题中,其实都可以采用递归来处理。
递归算法的适用场景
排序
快速排序、归并排序等常用排序算法中,往往采用的是递归的方式。以归并排序为例,采用分治的思想将一个大问题分解成若干个小问题,然后将这些小问题分别解决。最终,将这些小问题的解决方式汇总考虑整个大问题的解决方案。
树的遍历
在二叉树的遍历问题中,递归往往是最直接的策略。因为二叉树的内在结构很适合递归算法。
动态规划
动态规划是通过将问题分解成若干子问题,通过记录和利用子问题的结果来得到最优解的一种算法。递归则是处理子问题的理想方案。
递归算法原理
递归算法的数学基础
很多时候,大家都在思考递归在数学上面应该如何表示了,毕竟对于数学的简单理解比起我们直接写代码起来还是要简单很多的。观察递归,我们会很容易发现递归的数学模型类似于数学归纳法,这个在高中的数列里面就已经开始应用了。数学归纳法常见的描述如下:
最简单和常见的数学归纳法是证明当 n 等于任意一个自然数时某命题成立。证明分下面两步:
证明当 n= 1 时命题成立。
假设 n=m 时命题成立,那么可以 推导出在 n=m+1 时命题也成立。(m 代表任意自然数)
数学归纳法适用于将需要解决的原问题转换为解决他的子问题,而其中的子问题又可以变成子问题的子问题,而且这些问题都是同一个模型,可以用相同的处理逻辑归纳处理。当然有一个是例外的,就是归纳结束的那一个处理方法不能适用于其他的归纳处理项。递归同样的是将大的问题分解成小问题处理,然后会有一个递归的终止条件,满足终止条件之后开始回归。
数学里面有一个阶乘,我们在编程求解阶乘的时候就会用到递归的思想,在后续的内部中会具体讲到。
递归算法的逻辑流程
我们以阶乘作为:
cpp
int Factorial(int n)
{
if (n == 0)
return 1;
return n * Factorial(n - 1);
}
常常听到 "递归的过程就是出入栈的过程",这句话怎么理解?我们以上述代码为例,取
,则过程如下:
- 第 ~4 步,都是入栈过程,Factorial(3)调用了Factorial(2),Factorial(2)又接着调用Factorial(),直到Factorial(0);
- 第 5 步,因 0 是递归结束条件,故不再入栈,此时栈高度为 4,即为我们平时所说的递归深度;
- 第 6~9 步,Factorial(0)做完,出栈,而Factorial(0)做完意味着Factorial(1)也做完,同样进行出栈,重复下去,直到所有的都出栈完毕,递归结束。
递归算法的三要素
在明确递归的定义及数学模型之后,我们需要掌握递归的三要素:
- 递归终止条件
- 递归终止时候的处理方法
- 递归中重复的逻辑提取,缩小问题规模
递归算法实现
递归算法的伪代码
Function RecursiveFunction(parameters)
If base case then
return base case result
Else
Calculate result using recursive call
return result
EndFunction
使用递归算法实现斐波那契数列
斐波那契数列是一个经典的数列,其数列符合黄金分割比的规律,数列越大,其前一项与后一项的比值,越接近黄金比例。
用文字来说,就是费波那契数列由0和1开始,之后的费波那契系数就是由之前的两数相加而得出。首几个费波那契系数是:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233.....
下面我们用代码实现一个打印前十个斐波那契数的程序
Python代码实现
python
def fibonacci_recursive(n):
if n <= 1:
return n
else:
return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
# 打印前10个斐波那契数
for i in range(10):
print(fibonacci_recursive(i))
Java代码实现
java
public class FibonacciRecursive {
public static int fibonacciRecursive(int n) {
if (n <= 1) {
return n;
} else {
return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(fibonacciRecursive(i));
}
}
}
C++代码实现
cpp
#include <iostream>
using namespace std;
int fibonacciRecursive(int n) {
if (n <= 1) {
return n;
} else {
return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
}
}
int main() {
for (int i = 0; i < 10; i++) {
cout << fibonacciRecursive(i) << endl;
}
return 0;
}
递归算法的优缺点分析
优点
- 代码简洁性:递归算法通常能够用更少的代码行数解决问题,特别是对于那些自然具有递归结构的问题(如树的遍历)。
- 直观性:递归算法能够直观地表达问题解决策略,特别是当问题的定义本身就包含递归性质时(如斐波那契数列)。
- 问题分解:递归是一种将复杂问题分解为更小、更易于管理的子问题的方法。
- 减少重复代码:递归可以避免重复编写处理相似情况的代码,因为相同的递归逻辑可以应用于不同的子问题。
- 易于理解:对于熟悉递归的人来说,递归算法通常比迭代解决方案更容易理解和解释。
- 自然表达:递归算法能够自然地表达一些算法,如分治算法、动态规划中的自顶向下方法等。
缺点
- 性能问题:递归算法可能会导致性能问题,尤其是在没有适当优化(如记忆化递归)的情况下,因为它可能会重复计算相同的子问题多次。
- 栈溢出:深度递归可能会导致栈溢出错误,尤其是当递归深度非常大时,因为每次递归调用都会占用栈空间。
- 效率低下:在没有优化的情况下,递归算法可能比迭代算法效率低,因为它涉及更多的函数调用和返回操作。
- 理解难度:对于初学者来说,递归可能难以理解和调试,特别是当递归深度较大或递归逻辑复杂时。
- 内存消耗:递归调用需要在内存栈中存储大量状态信息,这可能导致较高的内存消耗。
- 限制条件:递归算法要求必须有一个明确的终止条件(基本情况),否则可能导致无限递归。
- 调试难度:调试递归算法可能比较困难,因为需要跟踪多个递归调用和它们的状态。
- 适用性限制:并非所有问题都适合用递归解决,有些问题用迭代方法可能更简单或更高效。
递归算法在不同领域应用
- 数学计算:例如计算阶乘或斐波那契数列,递归提供了一种直接且简洁的解决方案。阶乘可以定义为 n! = n * (n-1)!,其中基本情况是 1! = 1 。
- 数据结构遍历:在树形结构如二叉树的遍历中,递归算法可以优雅地实现深度优先搜索(DFS),通过递归函数访问树的每个节点 。
- 排序算法:某些排序算法如快速排序和归并排序,其分治策略本质上是递归的。快速排序通过递归地划分数组,然后对子数组进行排序 。
- 图算法:在图的遍历中,递归可以用来实现深度优先搜索(DFS),解决诸如路径查找、环检测等问题 。
- 动态规划:虽然动态规划通常与迭代相关联,但递归也可以用来描述动态规划问题的递归关系,尤其是在记忆化搜索中 。
- 排列组合问题:递归算法可以自然地用于解决排列、组合和子集生成等问题,这些问题通常具有重叠子问题的特性 。
- 汉诺塔问题:递归是解决汉诺塔问题的典型方法,通过递归地移动圆盘到辅助柱,然后再递归地将圆盘从辅助柱移动到目标柱 。
- 文件系统操作:在文件系统的遍历中,递归算法可以用来访问目录及其所有子目录中的文件,例如统计文件夹大小或查找特定类型的文件 。
- 解析XML和HTML文档:递归算法可用于解析嵌套的XML或HTML文档,通过递归函数处理每个节点及其子节点 。
- 算法竞赛和面试:递归算法是算法竞赛和编程面试中的常见题目,用以测试应聘者的逻辑思维和编程能力 。