数据结构与算法--再谈递归与循环(斐波那契数列)

再谈递归与循环

  • 在某些算法中,可能需要重复计算相同的问题,通常我们可以选择用递归或者循环两种方法。递归是一个函数内部的调用这个函数自身。循环则是通过设置计算的初始值以及终止条件,在一个范围内重复运算。比如,我们求累加1+2+3+…n,这个既可以用循环也可以用递归
 /*** 递归实现* */public Long addFrom1N(long n){return n <= 0 ? 0 : n + addFrom1N(n-1);}/*** 循环实现* */public long addForm1N_interative(long n){if(n <=0){return 0;}long result= 0;for (long i = 1; i < n; i++) {result +=i;}return result;}
  • 如上案例实现,递归代码简洁,循环代码比较多,同样的在之前的文章二叉树实现原理在树的前序,中序,后序遍历的代码中,递归实现也明显比循环实现要简洁的多,所以我们尽量用递归来表达我们的算法思想。

  • 递归的缺点:

    • 递归优点显著,由于是函数自身的调用,而函数调用有时间与空间的消耗:每一次调用都需要内存栈分配空间保存参数返回地址以及临时变量,而往栈里压入与弹出数据也需要时间,那就自然递归实现的效率比同等条件下循环要低下了
    • 另外递归中可能有很多计算是重复的,这个比较致命,对性能带来很大影响。
    • 除了效率,递归还有可能出现调用栈溢出问题。因为每个进程栈空间有限,当递归次数太多,超出栈容量,导致栈溢出。
案例分析:斐波那契数列
  • 题目:写一个函数,输入n,求斐波那契(Fibonacci)数列的第n项。斐波那契数列的定义如下:
f(n) = f(n-1) + f(n-2) , n>1
f(n) = 0 , n=0
f(n) = 1 , n=1
斐波那契数列递归实现
  • 记得谭浩强版本的C语言中讲解递归的时候就是用的斐波那契数列的案例,所以对这个问题非常的熟悉。看到之后自然就能提供如下代码:
 /*** f(n) = f(n-1) + f(n-2)* n > 2* n=1 : f(1) = 1* n=2 : f(2) = 1* f(3) = f(1) + f(2) = 1 + 1 = 2;*/public static Long getFibonacci(int n) {if (n <= 0L) {return 0L;}if (n == 1L || n == 2L) {return 1L;}return getFibonacci(n - 1) + getFibonacci(n - 2);}
  • 教科书上只是为了讲解递归,这个案例正好比较合适,并不表示是最优解,其实以上方法是一种存在严重效率问题的解法,如下分析:
    • 我们求解f(10) 需要求解f(9),f(8),继而需要先求解f(8),f(7),…我们可以用树形结构来说明这种依赖求解关系:

在这里插入图片描述

  • 如上图中分解,树中很多节点是重复的,而且重复的节点数会随着n的增大指数级别的增大,我们可以用以上算法测试第100项的值,慢的你怀疑人生。
我认为的最优解:动态规划(循环实现)
  • 改进方法并不复杂,上述代码中是因为大量重复计算,我们只要避免重复计算就行了。比如我们将已经计算好的数列保存到一个临时变量,下次计算直接查找前一次计算的结果,就无须重复计算之前的值。
  • 例如我们从下往上算,根据f(0) 和f(1) 求f(2), 继续f(1),f(2) 求f(3),依次类推得出第n项。很容易得出解。而且时间复杂度控制在O(n)
  • 如下代码实现:
 /*** 动态规划求值* */public static Long getFibonacciGreat(int n) {if (n <= 0L) {return 0L;}if (n == 1L || n == 2L) {return 1L;}Long answer = 1L;Long last = 1L;Long nextLast = 1L;for (Long i = 0L; i < n - 2; i++) {answer = last + nextLast;nextLast = last;last = answer;}return answer;}
时间复杂度更优O(logn)但是复杂度过于高的解法
  • 一般以上解法是最优解,但是如果在追求时间复杂度最优的算法场景下,我们有更快的O(logn)的算法。由于这种算法需要一个比较生僻的数学公式(离散数学没学好的代价),因此很少有人会去写这种算法,此处我们只介绍该算法,不递推数学公式(不会),如下:
  • 先介绍数学公式如下:

[f(n)f(n−1)f(n−1)f(n−2)]=[1110]n−1\left[ \begin{matrix} f(n) &f(n-1) \\ f(n-1) & f(n-2) \end{matrix} \right] = \left[ \begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right] ^{n-1} [f(n)f(n1)f(n1)f(n2)]=[1110]n1

  • 如上数学公式可以用数学归纳法证明,有了这个公式我们只需要求如下矩阵的值,既可以的到f(n)的值,
    [1110]n−1\left[ \begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right] ^{n-1} [1110]n1

  • 那么我们只需要求,基础矩阵的乘方问题。如果只是简单的从0~n循环,n次方需要n次运算,那么时间复杂度还是O(n),并不比之前的方法快,但是我们可以考虑乘方的如下性质
    [1110]\left[ \begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right] [1110]

  • 情况一
    an=an/2∗an/2,n为偶数a^n = a^{n/2}* a^{n/2} , n为偶数 an=an/2an/2,n

  • 情况二
    an=a(n−1)/2∗a(n−1)/2∗a,n为奇数a^n = a^{(n-1)/2}* a^{(n-1)/2}*a , n为奇数 an=a(n1)/2a(n1)/2a,n

  • 从上面公式我们看出,要求n次方,我们可以先求n/2次方,再把n/2次方平凡就可以。这可以用递归的思路来实现。

  • 我们用如下方式实现,因为存在矩阵的计算,用代码实现比较繁琐,如下:

/*** 矩阵对象定义* @author liaojiamin* @Date:Created in 15:21 2021/3/16*/
public class Matrix2By2 {private long m_00;private long m_01;private long m_10;private long m_11;public Matrix2By2(){this.m_00 = 0;this.m_01 = 0;this.m_10 = 0;this.m_11 = 0;}public Matrix2By2(long m00, long m01, long m10, long m11){this.m_00 = m00;this.m_01 = m01;this.m_10 = m10;this.m_11 = m11;}public long getM_00() {return m_00;}public void setM_00(long m_00) {this.m_00 = m_00;}public long getM_01() {return m_01;}public void setM_01(long m_01) {this.m_01 = m_01;}public long getM_10() {return m_10;}public void setM_10(long m_10) {this.m_10 = m_10;}public long getM_11() {return m_11;}public void setM_11(long m_11) {this.m_11 = m_11;}
}*** 获取斐波那契数列* @author liaojiamin* @Date:Created in 12:06 2021/2/2*/
public class Fibonacci {/*** 矩阵乘法求值* */public static Matrix2By2 matrixMultiply(Matrix2By2 matrix1, Matrix2By2 matrix2){return new Matrix2By2(matrix1.getM_00()*matrix2.getM_00() + matrix1.getM_01()*matrix2.getM_10(),matrix1.getM_00()*matrix2.getM_01() + matrix1.getM_01()*matrix2.getM_11(),matrix1.getM_10()*matrix2.getM_00() + matrix1.getM_11()*matrix2.getM_10(),matrix1.getM_10()*matrix2.getM_01() + matrix1.getM_11()*matrix2.getM_11());}/*** 矩阵乘方实现* */public static Matrix2By2 matrixPower(int n){if( n<= 0){return new Matrix2By2();}Matrix2By2 matrix = new Matrix2By2();if(n==1){matrix = new Matrix2By2(1,1,1,0);}else if(n%2 == 0){matrix = matrixPower(n/2);matrix = matrixMultiply(matrix, matrix);}else if(n%2 == 1){matrix = matrixPower((n-1)/2);matrix = matrixMultiply(matrix, matrix);matrix = matrixMultiply(matrix, new Matrix2By2(1,1,1,0));}return matrix;}public static long getFibonacciBest(int n){if(n == 0){return 0;}if (n <= 0) {return 0;}if (n == 1) {return 1;}Matrix2By2 powerNminus2 = matrixPower(n-1);return powerNminus2.getM_00();}public static void main(String[] args) {System.out.println("-------------------------1");System.out.println(System.currentTimeMillis()/1000);System.out.println(getFibonacci(40));System.out.println(System.currentTimeMillis()/1000);System.out.println("-------------------------2");System.out.println(System.currentTimeMillis()/1000);System.out.println(getFibonacciGreat(40));System.out.println(System.currentTimeMillis()/1000);System.out.println("-------------------------3");System.out.println(System.currentTimeMillis()/1000);System.out.println(getFibonacciBest(40));System.out.println(System.currentTimeMillis()/1000);}
}
  • 时间复杂度:设为f(n),其中n 是矩阵的幂次。从上述代码中不难得出f(n) = f(n/2) + O(1) 。利用主定理,可以解得f(n) = O(log⁡n\log^{n}logn)

  • 空间复杂度:每一次递归调用时新建了一个变量matrixPower(n/2)。由于代码需要执行log⁡2n\log_2^{n}log2n次,即递归深度是log⁡2n\log_2^{n}log2n ,所以空间复杂度是O(log⁡n\log^{n}logn)

解法比较
  • 用不同方法求解斐波那契数列的时间效率有很大区别。第一种基于递归的解法,时间复杂度效率低,时间开发中不可能会用
  • 第二种将递归算法用循环实现,极大提高效率
  • 第三种方法将斐波那契数列转换炒年糕矩阵n次方求解,少有这种算法出现,此处只是提出这种解法而已
变种题型
  • 处理斐波那契数列这种问题,还有不少算法原理与斐波那契数列是一致的,例如:

  • 题目:一只青蛙一次可以跳一个台阶,也可以跳两个台阶。求解青蛙跳上n个台阶有多少中跳法

  • 分析

    • 最简单情况,如果总共只有一节台阶,只有一种解法,如果有两个台阶,有两种跳法,
    • 一般情况,n级台阶看出是n的函数,记f(n), 当 n> 2 时候,第一次条就有两种不同选择,
    • 第一种跳1级,此时后面的台阶的跳法等于f(n-1) ,那么总的跳法是 1 * f(n-1) = f(n -1)
    • 第二种跳2级,此时后面的台阶跳法等于f(n-2) ,那么总的跳法是 1*f(n-2) = f(n-2)
    • 所以n级台阶的不同跳法是 (n) = f(n-1) + f(n-2),实际上就是斐波那契数列

上一篇:数据结构与算法–查找与排序另类用法
下一篇:数据结构与算法–位运算

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/310436.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

同步异步多线程这三者关系,你能给面试官一个满意的回答吗?

前几天一位朋友去面试&#xff0c;面试官问了他同步&#xff0c;异步&#xff0c;多线程之间是什么关系&#xff0c;异步比同步高效在哪&#xff1f;多线程比单线程高效在哪&#xff1f;由于回答的不好&#xff0c;让我帮他捋一下&#xff0c;其实回答这个问题不难&#xff0c;…

分布式事务框架seata

seata 前两篇文中总结了一下分布式事务已经现阶段常用的解决方案&#xff0c;现在来讨论一下现有的分布式事务框架seata&#xff0c;点击此处是seata的官网seata致力于微服务框架下提供高性能和简单易用的分布式事务服务。它提供了AT&#xff0c;TCC&#xff0c;Saga &#xf…

[一起读源码]走进C#并发队列ConcurrentQueue的内部世界 — .NET Core篇

在上一篇《走进C#并发队列ConcurrentQueue的内部世界》中解析了Framework下的ConcurrentQueue实现原理&#xff0c;经过抛砖引玉&#xff0c;得到了一众大佬的指点&#xff0c;找到了.NET Core版本下的ConcurrentQueue源码&#xff0c;位于以下地址&#xff1a;https://github.…

Java语法基础50题训练(上)

题目1: 有两只老虎&#xff0c;一只体重为180kg&#xff0c;一只体重为200kg&#xff0c;请用程序实现判断两只老虎的体重是否相同。 代码如下: public class OperatorTest {public static void main (String[] args) {int w1 180;int w2 200;boolean ans w1 w2?true:f…

EFCore.Sharding(EFCore开源分表框架)

简介本框架旨在为EF Core提供Sharding(即读写分离分库分表)支持,不仅提供了一套强大的普通数据操作接口,并且降低了分表难度,支持按时间自动分表扩容,提供的操作接口简洁统一.源码地址:EFCore.SHarding引言读写分离分库分表一直是数据库领域中的重难点,当数据规模达到单库极限的…

分布式事务 -- seata框架AT模式实现原理

Seata AT 模式 上一节中我们提到AT模式是基于XA事务模型演变过来的&#xff0c;所以他的整体机制也是一个改进版本的两阶段提交协议。 第一阶段&#xff1a;业务数据和回滚日志记录在同一个本地事务中提交&#xff0c;释放本地锁和链接资源第二阶段&#xff1a;提交异步化&…

[Java基础]数据输入

Scanner使用的基本步骤: 1.导包: import java.util.Scanner;2.创建对象: Scanner sc new Scanner(System.in);3.接收数据: int i sc.nextInt();代码如下: import java.util.Scanner;public class OperatorTest {public static void main (String[] args) {//创建对象Scan…

k8s中流量分离以及资源隔离实战

源宝导读&#xff1a;明源云客的终端用户越来越多&#xff0c;也涌现出线上流量活动的场景&#xff0c;大量的访问和接口请求导致服务器出现较高负载。本文将介绍云客团队为了缓解服务器压力&#xff0c;通过K8S进行分流与资源隔离的实践过程。一、背景PaaS和B2C的主要客户云客…

怎样实现WPF Prism Module的国际化和本地化?

English | 简体中文上一篇有简单介绍主工程的国际化&#xff0c;使用的资源字典(XAML)实现的。这几天我添加了几个Prism模块(Module)&#xff0c;发现子模块使用资源字典的方式实现国际化和本地化不好做&#xff0c;没有找到比较好的参考文章&#xff0c;所以换了一种方式&…

dotNET Core 3.X 使用 Jwt 实现接口认证

在前后端分离的架构中&#xff0c;前端需要通过 API 接口的方式获取数据&#xff0c;但 API 是无状态的&#xff0c;没有办法知道每次请求的身份&#xff0c;也就没有办法做权限的控制。如果不做控制&#xff0c;API 就对任何人敞开了大门&#xff0c;只要拿到了接口地址就可以…

数据结构与算法--代码鲁棒性案例分析

代码鲁棒性 鲁棒是robust的音译&#xff0c;就是健壮性。指程序能够判断输入是否符合规范&#xff0c;对不合要求的输入能够给出合理的结果。容错性是鲁棒的一个重要体现。不鲁棒的代码发生异常的时候&#xff0c;会出现不可预测的异常&#xff0c;或者程序奔溃。由于鲁棒性非…

【半译】两个gRPC的C#库:grpc-dotnet vs Grpc.Core

grpc-dotnet 是在2019年随着 .NET Core 3.0 一起发布的一个gPRC官方库。在ASP.NET Core 的 gRPC项目模板里面就使用了这个库。.NET Core 3.0之前难道不可以使用gRPC吗&#xff1f;目前&#xff0c;gRPC 在.NET上有两种官方实现&#xff1a;Grpc.Core&#xff1a;这个是原来的gR…

[Java基础]String对象的特点(易错点)

String对象的特点: 1.通过new创建的字符串对象&#xff0c;每一次new都会申请一个内存空间&#xff0c;虽然内容相同&#xff0c;但是地址值不同。 2.以""方式给出的字符串&#xff0c;只要字符串相同(顺序和大小写)&#xff0c;无论在程序代码中出现几次&#xff0…

数据结构与算法--解决问题的方法- 二叉树的的镜像

解决问题的思路 工作中遇到的问题可能用到的数据结构由很多&#xff0c;并且各种数据结构都不简单&#xff0c;我们不可能光凭借想象就能得到问题的解法&#xff0c;因此画图是在家具问题过程中用来帮助自己分析&#xff0c;推理的常用手段。很多问题比较抽象&#xff0c;不容…

使用dnSpy调试asp.net core源码

环境&#xff1a;window 10vs2019 16.5.1dnspy v6.1.4.netcore3.1参考&#xff1a;.Net反编译技术详解及4个反编译工具介绍一、关于dnSpydnSpy是近几年的新秀&#xff0c;功能远比ILSpy强大&#xff0c;甩.net Reflector几条街&#xff0c;被汉化、破解、逆向方面的人才奉为神器…

数据结构与算法--解决问题的方法-顺时针打印矩阵

顺时针打印矩阵 题目输入一个矩阵&#xff0c;按照从外向里顺时针的顺序依次打印每一个数字。例如下案例&#xff1a; 如上图矩阵&#xff0c;顺时针打印&#xff1a;1,2,3,4,8,12,16,15,14,13,9,5,6,7,1,10 以上问题看起来比较复杂&#xff0c;但是又没有涉及到复杂的数据结…

.NET与鲲鹏共展翅,昇腾九万里(二)

在上一篇文章 .NET与鲲鹏共展翅&#xff0c;昇腾九万里&#xff08;一&#xff09;中&#xff0c;我们通过在鲲鹏架构的Euler系统上跑Docker的方式把dotnet core 跑起来了&#xff0c;有读者反馈说“还是走docker喽&#xff0c;你这个标题应该改成鲲鹏和docker两条鲸鱼的故事”…

[Java基础]final和static修饰符

final: final修饰局部变量时: static&#xff1a; static访问特点:

优化委托的 DynamicInvoke

优化委托的 DynamicInvokeIntro委托方法里有一个 DynamicInvoke 的方法&#xff0c;可以在不清楚委托实际类型的情况下执行委托方法&#xff0c;但是用 DynamicInvoke 去执行的话会比直接用 Invoke 的方法会慢上很多&#xff0c;差了两个数量级&#xff0c;所以在知道委托类型的…