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

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

相关文章

基本知识点

1、c++的输入加上ios::sync_with_stdio(false);  等价于 c的输入,读取速度会加快(但是在字符串的题里面和容易出现问题) 2、lower_bound()和upper_bound() iterator lower_bound( const key_type &key ): 返回一个迭代器,指向键值>= key的第一个元素。 iterator upper_bou

【IPV6从入门到起飞】5-1 IPV6+Home Assistant(搭建基本环境)

【IPV6从入门到起飞】5-1 IPV6+Home Assistant #搭建基本环境 1 背景2 docker下载 hass3 创建容器4 浏览器访问 hass5 手机APP远程访问hass6 更多玩法 1 背景 既然电脑可以IPV6入站,手机流量可以访问IPV6网络的服务,为什么不在电脑搭建Home Assistant(hass),来控制你的设备呢?@智能家居 @万物互联

C 语言的基本数据类型

C 语言的基本数据类型 注:本文面向 C 语言初学者,如果你是熟手,那就不用看了。 有人问我,char、short、int、long、float、double 等这些关键字到底是什么意思,如果说他们是数据类型的话,那么为啥有这么多数据类型呢? 如果写了一句: int a; 那么执行的时候在内存中会有什么变化呢? 橡皮泥大家都玩过吧,一般你买橡皮泥的时候,店家会赠送一些模板。 上

FreeRTOS-基本介绍和移植STM32

FreeRTOS-基本介绍和STM32移植 一、裸机开发和操作系统开发介绍二、任务调度和任务状态介绍2.1 任务调度2.1.1 抢占式调度2.1.2 时间片调度 2.2 任务状态 三、FreeRTOS源码和移植STM323.1 FreeRTOS源码3.2 FreeRTOS移植STM323.2.1 代码移植3.2.2 时钟中断配置 一、裸机开发和操作系统开发介绍 裸机:前后台系

Java 多线程的基本方式

Java 多线程的基本方式 基础实现两种方式: 通过实现Callable 接口方式(可得到返回值):

Java基础回顾系列-第一天-基本语法

基本语法 Java基础回顾系列-第一天-基本语法基础常识人机交互方式常用的DOS命令什么是计算机语言(编程语言) Java语言简介Java程序运行机制Java虚拟机(Java Virtual Machine)垃圾收集机制(Garbage Collection) Java语言的特点面向对象健壮性跨平台性 编写第一个Java程序什么是JDK, JRE下载及安装 JDK配置环境变量 pathHe

PHP实现二叉树遍历(非递归方式,栈模拟实现)

二叉树定义是这样的:一棵非空的二叉树由根结点及左、右子树这三个基本部分组成,根据节点的访问位置不同有三种遍历方式: ① NLR:前序遍历(PreorderTraversal亦称(先序遍历)) ——访问结点的操作发生在遍历其左右子树之前。 ② LNR:中序遍历(InorderTraversal) ——访问结点的操作发生在遍历其左右子树之中(间)。 ③ LRN:后序遍历(PostorderT

oracle11.2g递归查询(树形结构查询)

转自: 一 二 简单语法介绍 一、树型表结构:节点ID 上级ID 节点名称二、公式: select 节点ID,节点名称,levelfrom 表connect by prior 节点ID=上级节点IDstart with 上级节点ID=节点值 oracle官网解说 开发人员:SQL 递归: 在 Oracle Database 11g 第 2 版中查询层次结构数据的快速

Gradle的基本使用

新建一个项目后,在项目文件夹下创建build.gradle文件,并加入内容:       apply plugin: 'eclipse'。    然后在终端运行gradle eclipse即可构建eclipse IDE的开发环境。    gradle默认值:gradle有些目录是有默认值存在,建议项目的配置,承袭了maven的风格,如:         java的源码目录:src/mai

QML入门之基本元素

元素分为可视元素与非可视元素,可能元素例如Rectangle、Button等。非可视元素如Timer(定时器)、MouseArea(鼠标区域)等。非可视元素一般用于操作可视元素。 基础元素 Item Item(基础元素对象)是所有可视元素的基础对象,它们都继承自Item。可是元素存在以下共有属性。 Group(分组)Properties(属性)Geometry(几何属性)x