应届生必考的斐波那契数列 优化版本

应届生,必考,数列,优化,版本 · 浏览次数 : 271

小编点评

**斐波那契递归算法** 斐波那契递归算法是一种递归算法,用于计算由两个数字组成的所有数字。斐波那契数列的递推公式如下: ``` F(0) = 0 F(1) = 1 F(n) = F(n-1) + F(n-2) ``` 斐波那契递归算法的递归调用会随着数字的增加而增长,导致栈溢出错误。 **循环递归算法** 循环递归算法是一种递归算法,用于计算由两个数字组成的所有数字,而避免栈溢出错误。循环递归算法的结构如下: ``` function Fibonacci(n): if n == 0: return 0 if n == 1: return 1 return Fibonacci(n-1) + Fibonacci(n-2) ``` **比较** 斐波那契递归算法和循环递归算法在计算效率和时间复杂度之间取得了平衡。循环递归算法比斐波那契递归算法具有更好的性能,但它也容易出现栈溢出错误。 **尾递归算法** 尾递归算法是一种递归算法,用于计算由两个数字组成的所有数字,而避免栈溢出错误。尾递归算法的结构如下: ``` function Fibonacci(n): if n == 1 || n == 2: return n return Fibonacci(n-1) + Fibonacci(n-2) ``` 尾递归算法比斐波那契递归算法和循环递归算法具有更好的性能,但它也更加复杂。 **结论** 斐波那契递归算法和循环递归算法都是用于计算由两个数字组成所有数字的递归算法。循环递归算法比斐波那契递归算法具有更好的性能,但它也容易出现栈溢出错误。尾递归算法比斐波那契递归算法和循环递归算法具有更好的性能,但它也更加复杂。

正文

  • 开题引入斐波那契
    • 代码演示: 递归、循环
  • 递归 vs 循环
    • 时间复杂复高,指数型O(2^n); 推导过程
    • 占用线程堆栈, 可能导致栈满异常
  • 压测演示
  • 20230816补充尾递归

斐波那契数列

打入门软件开发,斐波那契数列便是绕不过去的简单编程算法。

一个老生常谈的思路是递归,另外是循环,今天借此机会回顾并演示时间复杂度在编程中的重要性。

斐波那契 递归算法 1,1,2,3,5,8,13,21,34,55

常规递归

递归算法的应用场景是:

  • 将大规模问题,拆解成几个小规模的同样问题
  • 拆解具备终止场景
func Fibonacci(n int) (r int) {
	if n == 1 || n == 2 {
		r = 1
		return
	} else {
		return Fibonacci(n-1) + Fibonacci(n-2)
	}
}

循环

为什么能想到循环, 斐波那契数组也有循环的含义:
第n个数字是循环指针i从第1个数字移动到第n-2个数字时, 第n-2个数字pre+第n-1个数字next的和。

func Fibonacci2(n int) (r int) {
   if n==1 || n==2  {
     return 1
   }
   pre,next int :=1,1
   for i:=0; i<=n-1-2; i++ {
       tmp:= pre+next
       pre = next
       next = tmp
   }
}

单元测试如下:

func TestFibonacci(t *testing.T) {
	t.Logf("Fibonacci(1) = %d, Fibonacci2(1)= %d ", Fibonacci(1), Fibonacci2(1))
	t.Logf("Fibonacci(3) = %d, Fibonacci2(3)= %d ", Fibonacci(3), Fibonacci2(3))
	t.Logf("Fibonacci(8) = %d, Fibonacci2(8)= %d ", Fibonacci(8), Fibonacci2(8))
}

go test ./ -v
=== RUN   TestFibonacci
    m_test.go:8: Fibonacci(1) = 1, Fibonacci2(1)= 1 
    m_test.go:9: Fibonacci(3) = 2, Fibonacci2(3)= 2 
    m_test.go:10: Fibonacci(8) = 21, Fibonacci2(8)= 21 
--- PASS: TestFibonacci (0.00s)
PASS
ok      example.github.com/test 3.359s

常规递归的问题

(1) 函数调用存在压栈过程,会在线程栈(一般2M)上留下栈帧,斐波那契人递归算法:是函数自己调用自己,在终止条件后栈帧开始收敛,但是在此之前有可能已经撑爆线程栈。

栈帧中维持着函数调用所需要的各种信息,包括函数的入参、函数的局部变量、函数执行完成后下一步要执行的指令地址、寄存器信息等。

(2) 斐波那契递归调用存在重复计算,时间复杂度是O(2^n),随着n的增加,需要计算的次数陡然增大(业内称为指数型变化)。

 f(n) = f(n-1)+f(n-2)                 // 1次计算
      = f(n-2)+f(n-3)+f(n-3)+f(n-4)   // 3次计算
      = f(n-3)+f(n-4)+f(n-4)+f(n-5)+f(n-4)+f(n-5)+f(n-5)+f(n-6)                               // 7次计算
      =......
      = f(1)+......                   //  2^n-1次计算
      
 故为斐波那契递归时间复杂度为 O(2^n)     

而我们的循环算法不存在以上问题, 第n个数字需要n -2次计算, 时间复杂度是O(n)

常见时间复杂度变化曲线

有些童鞋可能没意识到指数型的威力,举个例子, 斐波那契递归算法,第20个数字需要2^20次运算; 循环算法只要18次运算。

使用基准测试压测:

func BenchmarkF1(b *testing.B) {
	for i := 1; i < b.N; i++ {
		Fibonacci(20) //  时间复杂度  O(2^n)
	}
}

func BenchmarkF2(b *testing.B) {
	for i := 1; i < b.N; i++ {
		Fibonacci2(20) // 时间复杂度 O(n)
	}
}


go test  -bench=.  -benchmem  ./
goos: darwin
goarch: amd64
pkg: example.github.com/test
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
BenchmarkF1-8              55039             20740 ns/op               0 B/op          0 allocs/op
BenchmarkF2-8           196663548                6.080 ns/op           0 B/op          0 allocs/op
PASS
ok      example.github.com/test 3.744s

单次执行效率相形见绌,甚至斐波那契递归n=50+ 不一定能计算出来。


ok, that'all 本次快速重温递归算法相比循环的两点劣势,这里面重要的常见时间复杂度变化曲线, 需要程序员必知必会。
https://adrianmejia.com/most-popular-algorithms-time-complexity-every-programmer-should-know-free-online-tutorial-course/

20230816 尾递归

常规递归: 时间复杂度 O(2^n), 而且存在爆栈的可能。
尾递归: 是对常规递归的优化,规避了爆栈的问题, 思路是规避持续压栈,利用编译器优化让整个递归过程只保持一个函数栈。

尾递归的核心表现: 函数运行到最后一步是否是调用自身,而不是在最后一行出现调用自身。

仔细体会:

// 这是常规递归
func  diguiFunc(n int )  (r int) {
     if  n==1 
         return  1
    return  1 + diguiFunc(n-1)     //  递归调用函数自身时, 因为这一行不是直接返回值, 故上级堆栈不会释放,递归全过程会形成不断压栈,在终止时才开始出栈。
}  
/*
   f(4) = 1+ f(3)= 1+1+f(2)= 1+1+1+f(1)= 1+1+1+1 = 4
*/

// 这是尾递归
func  tailFunc(n,pre int)  (r int ) {
     if n==1
        return n
    return  tailFunc(n-1,1+ pre)          // 开始调用此函数时,上级堆栈就会释放,递归全过程只会形成一个堆栈的占用。
}
/*
 f(4,1) = f(3,1+1) = f(2,1+2) = f(1,1+3) =4
*/

故尾递归 能规避爆栈问题, 斐波那契尾递归的写法如下, 时间复杂度是O(n), 跟循环是一样的。

  func Fibonacii_tail(n int, pre int , next int) {
       if  n==1 || n==2   {
            return next
       }
      return Fibonacii_tail(n-1,next, pre+next)
  }

尾递归的实现思路是:
(1)使用递归体现循环,循环中要重复执行的代码体现在 尾递归调用的参数, 用调用自身的结果作为返回值

(2) 尾递归的终止参数是由外部驱动,在外部调用传值

与应届生必考的斐波那契数列 优化版本相似的内容:

应届生必考的斐波那契数列 优化版本

- 开题引入斐波那契 - 代码演示: 递归、循环 - 递归 vs 循环 - 时间复杂复高,指数型O(2^n); 推导过程 - 占用线程堆栈, 可能导致栈满异常 - 压测演示 - 20230816补充尾递归 ## 斐波那契数列 打入门软件开发,斐波那契数列便是绕不过去的简单编程算法。 一个老生常谈的思

面对百度的无期徒刑,幸好还有微软的必应

昨天我们通过【i博客园】公众号发布文章 被百度降权的经历:没有百度的日子,是百度给的无期徒刑 时发现,百度不但没有回心转意,反而对园子的处罚更加严厉了,博客主站(www域名)的新发内容一天内0收录。 而在去年9月21日我们完全解除对百度蜘蛛的屏蔽后(详见博文),9月25日那天一天内的百度收录有20页

[转帖]50个应知必会的Linux常识和操作

1.存放用户账号的文件在哪里? /etc/passwd 1 2.如何删除一个非空的目录? rm -rf 目录名 1 3.查看当前的工作目录用什么命令? pwd 1 4.创建一个文件夹用什么命令? mkdir 1 5.哪个Linux命令可以一次显示一页内容?上一页和下一页使用什么命令? more Sp

这几个必备的vscode插件,你安装了几个

作为一名前端开发者,vscode想必大家应该都接触过,就像大多数 IDE 一样,VSCode 也有一个扩展和主题市场,包含了数以千计质量不同的插件。 作为一名熟练掌握各种前端开发工具安装和卸载的大师兄来说,为大家安利好玩有用的工具插件是我义不容辞的责任,所以我精挑细选了九款必备的vscode插件 C

聊聊RabbitMQ消息队列

消息队列的应用可以说是业务必备的。从功能来说,解耦、异步化、延迟队列、削峰等等;在之前的项目中就用到了rabbitmq来实现消息中心、业务的异步解耦。我个人很推从的就是业务的异步解耦能力。当时的业务场景是客户在界面上可以批量提交数据,但是服务端要做校验,数据处理,入库等等系列操作,其中的校验与数据处

开发者进阶必备的9个Tips & Tricks!

优秀的开发人员市场前景是十分广阔的,但想找到一份理想的工作,仅有代码知识是不够的。优秀的工程师应该是一个终身学习者、问题的创造性解决者,着迷于整个软件世界。要成为一名优秀的开发者,应该具备哪些品质并做出哪些努力?本文给出了一些简单的tips,除了优秀的行为习惯之外,还有一些代码工具使用的小技巧。 方

LLM技术全景图:技术人必备的技术指南,一张图带你掌握从基础设施到AI应用的全面梳理

LLM技术全景图:技术人必备的技术指南,一张图带你掌握从基础设施到AI应用的全面梳理 LLM 技术图谱(LLM Tech Map)是将 LLM 相关技术进行系统化和图形化的呈现,此图谱主要特点是“专注于技术人视角”,不求从 LLM 产业角度汇聚信息,而是希望让从事相关工作或是想了解 LLM 的技术人

驱动开发:内核文件读写系列函数

在应用层下的文件操作只需要调用微软应用层下的`API`函数及`C库`标准函数即可,而如果在内核中读写文件则应用层的API显然是无法被使用的,内核层需要使用内核专有API,某些应用层下的API只需要增加Zw开头即可在内核中使用,例如本章要讲解的文件与目录操作相关函数,多数ARK反内核工具都具有对文件的管理功能,实现对文件或目录的基本操作功能也是非常有必要的。

一键自动化博客发布工具,用过的人都说好(csdn篇)

CSDN应该是大家接触到最多的博客平台了,所以一款能够发布到CSDN的自动化工具还是非常有必要的。 今天给大家讲讲自动化CSDN博客发布的思路和一些问题的解决办法。 解决问题的思路一定是最重要的,知识是死的,问题是活的,如何在工作中解决遇到的问题是我们需要面临的大问题。 前提条件 前提条件当然是先下

对Transformer的一些理解

在学习Transformer这个模型前对seq2seq架构有个了解时很有必要的 先上图 输入和输出 首先理解模型时第一眼应该理解输入和输出最开始我就非常纠结 有一个Inputs,一个Outputs(shift right)和一个Output Probabilities,首先需要借助这三个输入/输出来