递归效率为什么差?

本文通过斐波那契数列计算示例,分析了递归算法的效率问题,指出了递归调用导致的空间和时间开销,并对比了迭代算法。

部分问题的不适用递归的解决方法将会陆续在算法栏和程序优化栏不定时贴出。

一下为转载内容,谈递归效率低得问题,还是比较明确的,值得参考!http://www.nowamagic.net/librarys/veda/detail/2321  这个博客对递归的描述还是很好的,请参考。

递归在解决某些问题的时候使得我们思考的方式得以简化,代码也更加精炼,容易阅读。那么既然递归有这么多的优点,我们是不是什么问题都要用递归来解决呢?难道递归就没有缺点吗?今天我们就来讨论一下递归的不足之处。谈到递归就不得不面对它的效率问题。

为什么递归是低效的

还是拿斐波那契(Fibonacci)数列来做例子。在很多教科书或文章中涉及到递归或计算复杂性的地方都会将计算斐波那契数列的程序作为经典示例。如果现在让你以最快的速度用C#写出一个计算斐波那契数列第n个数的函数(不考虑参数小于1或结果溢出等异常情况),我不知你的程序是否会和下列代码类似:

1 public static ulong Fib(ulong n)
2 {
3     return (n == 1 || n == 2) ? 1 : Fib(n - 1) + Fib(n - 2);
4 }

这段代码应该算是短小精悍(执行代码只有一行),直观清晰,而且非常符合许多程序员的代码美学,许多人在面试时写出这样的代码可能心里还会暗爽。但是如果用这段代码试试计算Fib(1000)我想就再也爽不起来了,它的运行时间也许会让你抓狂。

看来好看的代码未必中用,如果程序在效率不能接受那美观神马的就都是浮云了。如果简单分析一下程序的执行流,就会发现问题在哪,以计算Fibonacci(5)为例:

从上图可以看出,在计算Fib(5)的过程中,Fib(1)计算了两次、Fib(2)计算了3次,Fib(3)计算了两次,本来只需要5次计算就可以完成的任务却计算了9次。这个问题随着规模的增加会愈发凸显,以至于Fib(1000)已经无法再可接受的时间内算出。

我们当时使用的是简单的用定义来求 fib(n),也就是使用公式 fib(n) = fib(n-1) + fib(n-2)。这样的想法是很容易想到的,可是仔细分析一下我们发现,当调用fib(n-1)的时候,还要调用fib(n-2),也就是说fib(n-2)调用了两次,同样的道理,调用f(n-2)时f(n-3)也调用了两次,而这些冗余的调用是完全没有必要的。可以计算这个算法的复杂度是指数级的。

改进的斐波那契递归算法

那么计算斐波那契数列是否有更好的递归算法呢? 当然有。让我们来观察一下斐波那契数列的前几项:

1 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 …

注意到没有,如果我们去掉前面一项,得到的数列依然满足f(n) = f(n-1) – f(n-2), (n>2),而我们得到的数列是以1,2开头的。很容易发现这个数列的第n-1项就是原数列的第n项。怎么样,知道我们该怎么设计算法了吧?我们可以写这样的一个函数,它接受三个参数,前两个是数列的开头两项,第三个是我们想求的以前两个参数开头的数列的第几项。

1 int fib_i(int a, int b, int n);

在函数内部我们先检查n的值,如果n为3则我们只需返回a+b即可,这是简单情境。如果n>3,那么我们就调用f(b, a+b, n-1),这样我们就缩小了问题的规模(从求第n项变成求第n-1项)。好了,最终代码如下:

1 int fib_i(int a, int b , int n)
2 {
3     if(n == 3)
4         return a+b;
5     else
6         return fib_i(b, a+b, n-1);
7 }

这样得到的算法复杂度是O(n)的。已经是线性的了。它的效率已经可以与迭代算法的效率相比了,但由于还是要反复的进行函数调用,还是不够经济。

递归与迭代的效率比较

我们知道,递归调用实际上是函数自己在调用自己,而函数的调用开销是很大的,系统要为每次函数调用分配存储空间,并将调用点压栈予以记录。而在函数调用结束后,还要释放空间,弹栈恢复断点。所以说,函数调用不仅浪费空间,还浪费时间。

这样,我们发现,同一个问题,如果递归解决方案的复杂度不明显优于其它解决方案的话,那么使用递归是不划算的。因为它的很多时间浪费在对函数调用的处理上。在C++中引入了内联函数的概念,其实就是为了避免简单函数内部语句的执行时间小于函数调用的时间而造成效率降低的情况出现。在这里也是一个道理,如果过多的时间用于了函数调用的处理,那么效率显然高不起来。

举例来说,对于求阶乘的函数来说,其迭代算法的时间复杂度为O(n):

01 int fact(n)
02 {
03     int i;
04     int r = 1;
05     for(i = 1; i < = n; i++)
06     {
07         r *= i;
08     }
09     return r;
10 }

而其递归函数的时间复杂度也是O(n):

1 int fact_r(n)
2 {
3     if(n == 0)
4         return 1;
5     else
6         return n * f(n);
7 }

但是递归算法要进行n次函数调用,而迭代算法则只需要进行n次迭代而已。其效率上的差异是很显著的。

小结

由以上分析我们可以看到,递归在处理问题时要反复调用函数,这增大了它的空间和时间开销,所以在使用迭代可以很容易解决的问题中,使用递归虽然可以简化思维过程,但效率上并不合算。效率和开销问题是递归最大的缺点。

虽然有这样的缺点,但是递归的力量仍然是巨大而不可忽视的,因为有些问题使用迭代算法是很难甚至无法解决的(比如汉诺塔问题)。这时递归的作用就显示出来了。


<think>嗯,用户问的是递归算法必须包括什么。首先,我得回忆一下递归的基本概念。递归嘛,就是自己调用自己,对吧?不过具体来说,要构成一个有效的递归算法,肯定有几个必要的组成部分。 首先,递归的终止条件。没有终止条件的话,递归就会无限进行下去,导致堆栈溢出,程序崩溃。所以这是最关键的部分。比如计算阶乘的时候,当n=0或1时返回1,这就是终止条件。 然后是递归调用,也就是算法需要调用自身来解决更小的问题。比如在阶乘的例子中,n! = n * (n-1)!,这里的(n-1)!就是递归调用,把问题规模缩小了。 接下来是问题分解,递归必须能够将大问题分解成更小的子问题,这些子问题的结构和原问题相同,但规模更小。这样才能通过递归调用来逐步解决。 不过用户可能想问的是更具体的组成部分,比如代码结构上的元素。可能需要区分一下递归算法的必要组成部分和通常包含的部分。比如,除了终止条件和递归调用,可能还需要处理当前层逻辑,然后合并子问题的结果。但严格来说,合并结果可能在某些递归中不需要,比如尾递归。 另外,可能要考虑递归效率和堆栈的问题,但用户问的是必须包括什么,而不是优化方面。所以应该集中在基本要素上:终止条件和递归调用。 有没有可能用户混淆了递归和迭代?或者他们可能想知道如何正确设计递归函数?这时候需要明确回答必须的两个部分,并举例说明,确保用户理解每个部分的作用。 比如,斐波那契数列的递归实现,如果没有终止条件n <= 1时返回n,就会无限递归。而递归调用则是fib(n-1) + fib(n-2)。所以这两个部分是必须的。 总结一下,递归算法必须包括两点:终止条件(基础情况)和递归调用(向基础情况推进)。可能需要进一步解释这两个部分的重要性,以及如何正确应用它们来避免常见错误,比如缺少终止条件或递归调用没有缩小问题规模。</think>--- ### **递归算法的核心组成** 一个递归算法必须包含以下两个关键部分: --- #### **1. 终止条件(Base Case)** - **作用**:防止无限递归,确保算法最终终止。 - **要求**:必须至少有一个明确的条件判断,直接返回结果而非继续递归。 - **示例**: ```python def factorial(n): if n == 0: # 终止条件:n=0时直接返回1 return 1 else: return n * factorial(n-1) # 递归调用 ``` --- #### **2. 递归调用(Recursive Case)** - **作用**:将原问题分解为更小的同类子问题。 - **要求**:每次递归调用必须向终止条件逼近(例如参数逐渐减小)。 - **示例**: ```python def fibonacci(n): if n <= 1: # 终止条件:n=0或1时返回n return n else: return fibonacci(n-1) + fibonacci(n-2) # 分解为两个子问题 ``` --- ### **常见错误分析** | 错误类型 | 后果 | 修正方法 | |----------------|-----------------------|-------------------------------| | 缺少终止条件 | 无限递归 → 栈溢出崩溃 | 明确定义边界条件(如n=0、n=1)| | 递归未缩小问题 | 无法收敛到终止条件 | 确保参数逐渐减小(如n-1) | --- ### **递归算法设计要点** 1. **问题可分解性**:原问题必须能分解为结构相同的子问题(如树遍历、分治算法)。 2. **效率权衡**:避免重复计算(如斐波那契递归需用记忆化优化)。 3. **栈深度限制**:过深递归可能导致栈溢出,需考虑迭代替代方案。 --- ### **经典递归应用场景** 1. **数学计算**:阶乘、斐波那契数列。 2. **数据结构遍历**:二叉树的前序/中序/后序遍历。 3. **分治算法**:归并排序、快速排序。 4. **回溯问题**:八皇后、迷宫路径搜索。 --- ### **总结** **递归三要素**: 1. 明确的终止条件(Base Case) ✅ 2. 自我调用的递归过程(Recursive Case) ✅ 3. 每次递归缩小问题规模(Progress Toward Base Case) ✅ **答案**:一个递归算法必须包含**终止条件**和**递归调用**,二者缺一不可。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值