通过数学问题全面理解递归(动态规划、分治、尾递归、回溯)

本文通过一系列数学问题深入探讨递归,包括阶乘、斐波那契数列、pow函数、组合公式和泰勒级数。讲解了递归的核心要素、动态规划优化、分治策略、尾递归以及回溯算法的应用,帮助读者理解并掌握递归在解决实际问题中的应用。
摘要由CSDN通过智能技术生成

通过数学问题全面理解递归函数

在最开始接触到 递归(recursion) 的时候,我整个人是懵逼的,几行简单的代码就可以把我绕晕。由于递归会引起 栈溢出(stack overflow) 等问题,我便以此为借口,在很长一段时间里刻意地去回避思考和应用递归函数。但是后来发现,递归在计算机科学中是一个十分重要的概念,很多算法和数据结构的设计都离不开递归。在一些情况下,递归函数是可以作为解决问题的手段,虽然存在着内存使用情况的不确定性,但是递归函数有着无可比拟的简洁度。

学习和理解递归,最好的办法就是动手尝试用递归去解决一些具体的问题。这篇文章将用一系列的数学问题来全面剖析递归。除了递归的基本概念,本文还包括了以下几个知识点:

  • 使用 动态规划 优化递归
  • 使用 循环 代替递归
  • 使用 分治 优化递归
  • 尾递归 的特殊性
  • 递归和 回溯算法

在开始之前,我们先了解一下递归函数的核心功能:

  • 将一个问题分解成有限小的子问题并解决。

接着我们要记住递归的两大要素:

  1. 结束条件(base case):不需要调用自身就能解决的最小子问题。
  2. 递归关系(recurrence relation):通过调用自身使问题更加接近于所定义的结束条件。

1. 阶乘(Factorial)

阶乘相信大家肯定很熟悉了,它的数学表达式是(这里 n n n为非负整数):

n ! = 1 × 2 × 3 × ⋯ × ( n − 1 ) × n n! = 1 \times 2 \times 3 \times \cdots \times (n-1) \times n n!=1×2×3××(n1)×n

定义一个函数fac(n),让它的返回值等于 n ! n! n!。我们当然可以使用循环来轻易地实现这个函数。但是,如果想要用递归来解决这个问题的话,首先我们要找到上面所说的递归两大要素——结束条件递归关系。我们可以用数学表达式将它们写出来:

n ! = { 1 if  n ≤ 1 n ⋅ ( n − 1 ) ! otherwise n! = \begin{cases} 1 & \text{if } n \le 1 \\ n \cdot (n-1)! & \text{otherwise} \end{cases} n!={ 1n(n1)!if n1otherwise

大括号里的第一行就是我们要找的结束条件,也就是 0 ! = 1 0!=1 0!=1 1 ! = 1 1!=1 1!=1。第二行是递归关系,我们将 n ! n! n!这个问题拆解成了一个乘法和 ( n − 1 ) ! (n-1)! (n1)!;而 ( n − 1 ) ! (n-1)! (n1)!可以继续拆解,直到满足结束条件。把这个式子写成代码就是:

unsigned fac(unsigned n) {
   
  if (n <= 1) {
    return 1; } // 结束条件
  return n * fac(n - 1);    // 递归关系
}

1.1. 递归调用(Recursive Calls)

这个函数具体是怎么运作的呢?我们可以举一个例子,假设我们在主函数中调用了fac(3),那么我们可以把接下来的递归调用画成下图。从图中就可以看出如何通过递归关系将fac(3)分解成结束条件的。

fac(3)
fac(3)=6
fac(2)
fac(2)=2
fac(1)
fac(1)=1
main(): fac(3);
fac(3): return 3*fac(2);
fac(2): return 2*fac(1);
fac(1): return 1;

1.2. 递归和栈

除了理解递归调用,我们还必须知道递归是怎么使用计算机内存的。任何函数的调用都会占用内存中 栈(stack) 的一部分,用来储存函数的局部变量和参数(比如该函数中的n),而且在函数返回之前,被占用的栈是不会被释放的。在上面这个例子中,在进程运行到fac(1)时,栈的使用情况如下(注意:栈是向下增长的):


   |     +------------+
   |     |   main()   |
   |     +------------+
  Grow   |   fac(3)   |
Downward +------------+
   |     |   fac(2)   |
   |     +------------+
   |     |   fac(1)   |
   |     +------------+
   V     |    ....    |
   

所以不管是主函数,还是fac(n),都在调用时有着自己的 栈帧(stack frame)。由此可见,如果n是一个非常大的值,递归调用需要使用大量的栈空间,而内存不是无限的,这就会引起 栈溢出,导致进程被操作系统强制中断。

2. 斐波那契数列(Fibonacci Sequence)

通过阶乘了解了递归的基本概念之后,我们可以看一些其他例子。斐波那契数列是最经典的例子之一,几乎所有的教课书都会用它来介绍递归。

斐波那契数列是这样一个数列:0,1,1,2,3,5,8,13,21…,从第二项开始,每一项为前两项的和,求第n项的值。

它的表达式是:

F n = { n if  n ≤ 1 F n − 1 + F n − 2 otherwise F_{n} = \begin{cases} n & \text{if } n \le 1 \\ F_{n-1} + F_{n-2} & \text{otherwise} \end{cases} Fn={ nFn1+Fn2if n1otherwise

式子中直接就包含了结束条件和递归关系,所以我们可以很容易地写出代码:

int fib(int n) {
   
  if (n <= 1) {
    return n; }       // 结束条件
  return fib(n - 1) + fib(n - 2); // 递归关系
}

2.1. 递归调用树

函数中如果出现超过一次的递归调用,很多人就会觉得不好理解。我们可以画出递归调用树来帮助我们理解,这里我们用fib(4)举例:

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值