递归基本法则简论(以斐波那契数列为例)

2023-10-13 02:50

本文主要是介绍递归基本法则简论(以斐波那契数列为例),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

导言

本文假定阅读者已具备“递归思想”的基本知识,并具有用递归程序解决简单问题的能力和经验。在此基础上,本文将以斐波那契数列问题的求解作为例子,简单阐述递归的四项基本法则,给出此程序时间复杂度的数学证明,并最终优化斐波那契数列的算法。

回顾递归思想

我们熟悉的数学函数通常是由一个确定的公式描述的。与之不同的是,递归函数通过调用它自身来完成定义。这就是说,在函数的定义体内部存在函数本身。例如,一个求解斐波那契数列的递归程序如下:

		long int Fib(int N) {
/* 1*/		if (N == 0)
/* 2*/			return 0;
/* 3*/		else if (N == 1)
/* 4*/			return 1;
/* 5*/		else
/* 6*/			return Fib(N - 1) + Fib(N - 2);}

在函数Fib的定义中,第六行出现了对自身的调用。想要得到Fib(N)的值,首先必须得到Fib(N-1)和Fib(N-2)的值。

递归的基本法则

1.基准情形。必须总有某些基准情形,它无需递归就能解出。

基准情形决定了递归程序何时结束。倘若没有定义基准情形,程序将无法算出答案,陷入反复的调用中。在上述的程序中,前四行正是在定义基础情形,它规定当N<=1时直接返回,而不需要递归。这样,本递归程序才能够终止。否则程序会像奥尔加团长一样不要停下来啊!

							  |ₘₙⁿ▏n█▏ 、⺍█▏ ⺰ʷʷィ█◣▄██◣◥██████▋◥████ █▎███▉ █▎◢████◣⌠ₘ℩██◥█◣\≫██ ◥█◣█▉  █▊█▊  █▊█▊  █▋█▏  █▙█ ​(来自萌娘百科)

2.不断推进。对于那些需要递归求解的情形,每一次递归调用都必须要使求解情况朝接近基准情形的方向推进。

这是说,递归程序必须严格具有朝终止方向运行的趋势。如果其中发生了循环定义等情况,程序也将无法终止。一个简单的例子是:为了求解A,我们必须首先算出B,而A的值又是计算B时必须使用的。

3.设计法则。假设所有的递归调用都能运行。

这是一条重要的法则,它说明递归是建立在假设“同一问题的所有较小实例均可以正确运行”这一前提上的。递归程序将把这些较小问题的解结合起来形成现行问题的解。这利用了数学归纳法的思想。

4.合成效益法则。在求解同一个问题的同一实例时,切勿在不同的递归调用中做重复性的工作。

“计算任何事情不要超过一次。”

前三条法则考虑了递归程序的正确性,第四条法则考虑的事情则是算法的效率问题。我们再看一次这个求解斐波那契数列的算法:

		long int Fib(int N) {
/* 1*/		if (N == 0)
/* 2*/			return 0;
/* 3*/		else if (N == 1)
/* 4*/			return 1;
/* 5*/		else
/* 6*/			return Fib(N - 1) + Fib(N - 2);}

初看起来,这个程序用非常少的代码行数正确实现了功能,给出的答案算是无懈可击的。然而,对本问题来说,这个程序并不是优秀的代码,其原因就在于它违背了递归基本法则的第四条(虽然这个代码是大一高级程序设计语言课的常客)。违背合成效益法则的后果是当输入的N非常大时(实际上小于100的数就已经够呛),这个程序的效率非常感人。这个极其简单的程序的运行时间以指数级别增长。以下是分析:

令T(N)为函数Fib(N)的运行时间。
假设T(0)=T(1)=1,为一个时间单位。
则Fib(N)函数的运行时间可表示为:(N>=2)
T(N)=T(N-1)+T(N-2)+2
(“2”表示第一行的判断和第三行的加法所消耗的时间)
又由Fib(N)=Fib(N-1)+Fib(N-2)
根据数学归纳法可知,T(N)>=Fib(N)
然后再次利用数学归纳法证明N>4时,Fib(N)>=(3/2)^N:
基准情形:N=5时,成立。
假设当N=K时成立,现证明N=K+1时成立。

Fib(K+1=Fib(K)+Fib(K-1>=(3/2)^(K+1)+(3/2)^K
>=(3/2)^(K+1)*(10/9)
>=(3/2)^(K+1)

证毕。T(N)>=(3/2)^N,Fib(N)以指数级增长。

这个程序之所以缓慢,是因为重复做了大量的工作。

举例来说,当N=5时,程序首先转去计算Fib(4)和Fib(3)的值,而在计算Fib(4)时程序又会继续转去计算Fib(3)和Fib(2)。注意,此时Fib(3)已经被重复计算了两次。

计算过的信息没有被合理地保留,导致同一个数据被多次计算,浪费了大量时间。合成效益法则就是为了防止这一情况而诞生的。

如果利用一个数组存储每次计算的值,运行时间将实质性地减少。下面给出代码:

#define MAX_SIZE 100000
int a[MAX_SIZE] = { 0 };
int count_1 = 0;
int count_2 = 0;long int Fib_2(int N) {count_2++;if (N == 0)return 0;else if (N == 1)return 1;else if (a[N] == 0) {a[N] = Fib_2(N - 1) + Fib_2(N - 2);}return a[N];
}

当N=40时,两种程序的运行次数和运行时间(毫秒)比较:

当N=40时,两种程序的运行次数和运行时间比较
下面给出完整代码:

#include<iostream>
#include<ctime>
using namespace std;#define MAX_SIZE 100000
int a[MAX_SIZE] = { 0 };
int count_1 = 0;
int count_2 = 0;long int Fib_1(int N) {count_1++;if (N == 0)return 0;else if (N == 1)return 1;elsereturn Fib_1(N - 1) + Fib_1(N - 2);
}long int Fib_2(int N) {count_2++;if (N == 0)return 0;else if (N == 1)return 1;else if (a[N] == 0) {a[N] = Fib_2(N - 1) + Fib_2(N - 2);}return a[N];
}int main() {int n = 40;clock_t start_time_1, end_time_1, start_time_2, end_time_2;start_time_1 = clock();cout << Fib_1(n) << endl;end_time_1 = clock();cout << "运行次数:" << count_1 << " 运行时间:" << (double)(end_time_1 - start_time_1)*1000 / CLOCKS_PER_SEC << "MS" << endl;start_time_2 = clock();cout << Fib_2(n) << endl;end_time_2 = clock();cout << "运行次数:" << count_2 << " 运行时间:" << (double)(end_time_2 - start_time_2)*1000 / CLOCKS_PER_SEC << "MS" << endl;return 0;
}

总结

编写递归程序时,需要把四条基本准则牢记于心。
1.基准情形。
2.不断推进。
3.设计法则。
4.合成效益法则。
参考资料:《数据结构与算法分析:C语言描述》(第二版)

这篇关于递归基本法则简论(以斐波那契数列为例)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/200256

相关文章

C#连接SQL server数据库命令的基本步骤

《C#连接SQLserver数据库命令的基本步骤》文章讲解了连接SQLServer数据库的步骤,包括引入命名空间、构建连接字符串、使用SqlConnection和SqlCommand执行SQL操作,... 目录建议配合使用:如何下载和安装SQL server数据库-CSDN博客1. 引入必要的命名空间2.

Java中的数组与集合基本用法详解

《Java中的数组与集合基本用法详解》本文介绍了Java数组和集合框架的基础知识,数组部分涵盖了一维、二维及多维数组的声明、初始化、访问与遍历方法,以及Arrays类的常用操作,对Java数组与集合相... 目录一、Java数组基础1.1 数组结构概述1.2 一维数组1.2.1 声明与初始化1.2.2 访问

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁

ModelMapper基本使用和常见场景示例详解

《ModelMapper基本使用和常见场景示例详解》ModelMapper是Java对象映射库,支持自动映射、自定义规则、集合转换及高级配置(如匹配策略、转换器),可集成SpringBoot,减少样板... 目录1. 添加依赖2. 基本用法示例:简单对象映射3. 自定义映射规则4. 集合映射5. 高级配置匹

SQL BETWEEN 语句的基本用法详解

《SQLBETWEEN语句的基本用法详解》SQLBETWEEN语句是一个用于在SQL查询中指定查询条件的重要工具,它允许用户指定一个范围,用于筛选符合特定条件的记录,本文将详细介绍BETWEEN语... 目录概述BETWEEN 语句的基本用法BETWEEN 语句的示例示例 1:查询年龄在 20 到 30 岁

mysql中insert into的基本用法和一些示例

《mysql中insertinto的基本用法和一些示例》INSERTINTO用于向MySQL表插入新行,支持单行/多行及部分列插入,下面给大家介绍mysql中insertinto的基本用法和一些示例... 目录基本语法插入单行数据插入多行数据插入部分列的数据插入默认值注意事项在mysql中,INSERT I

mapstruct中的@Mapper注解的基本用法

《mapstruct中的@Mapper注解的基本用法》在MapStruct中,@Mapper注解是核心注解之一,用于标记一个接口或抽象类为MapStruct的映射器(Mapper),本文给大家介绍ma... 目录1. 基本用法2. 常用属性3. 高级用法4. 注意事项5. 总结6. 编译异常处理在MapSt

MyBatis ResultMap 的基本用法示例详解

《MyBatisResultMap的基本用法示例详解》在MyBatis中,resultMap用于定义数据库查询结果到Java对象属性的映射关系,本文给大家介绍MyBatisResultMap的基本... 目录MyBATis 中的 resultMap1. resultMap 的基本语法2. 简单的 resul

Java 枚举的基本使用方法及实际使用场景

《Java枚举的基本使用方法及实际使用场景》枚举是Java中一种特殊的类,用于定义一组固定的常量,枚举类型提供了更好的类型安全性和可读性,适用于需要定义一组有限且固定的值的场景,本文给大家介绍Jav... 目录一、什么是枚举?二、枚举的基本使用方法定义枚举三、实际使用场景代替常量状态机四、更多用法1.实现接

git stash命令基本用法详解

《gitstash命令基本用法详解》gitstash是Git中一个非常有用的命令,它可以临时保存当前工作区的修改,让你可以切换到其他分支或者处理其他任务,而不需要提交这些还未完成的修改,这篇文章主要... 目录一、基本用法1. 保存当前修改(包括暂存区和工作区的内容)2. 查看保存了哪些 stash3. 恢