背景

众所周知,Haskell语言是一门函数式编程语言。函数式编程语言的一大特点就是数值和对象都是不可变的,而这与经常需要对状态目前的值进行修改的动态规划算法似乎有些“格格不入”,本文对几乎可以说是动态规划的最简单特例:斐波那契数列的求解提出几种算法(不包括矩阵快速幂优化、Monad和通项公式计算),探讨一下函数式编程如何结合动态规划。

自底向上写法

算法1:

f' 1 _ b = b
f' n a b = f' (n - 1) b (a + b)
f n = f' n 0 1

尾递归,所以本质上和其他语言循环递推计算是一样的,但是如果编译器没看出来而真的用递归去算可能会爆栈。

算法2:

f' (a, b) _ = (b, a + b)
f n = snd (foldl f' (0, 1) (take (n - 1) (repeat 0)))

和上面算法一样,但是用fold来写的话可以保证编译器优化掉递归而不会爆栈。同时,在我看来,fold的过程体现了状态的变化,初状态通过一步步的计算得到末状态,正和动态规划的思想相契合,所以我认为这是动态规划的递推形式在函数式编程语言里最好的写法。

算法3:

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
f n = fibs !! n

摘自https://wiki.haskell.org/The_Fibonacci_sequence,这个应该是最接近斐波那契数列的数学递推式的写法了,构造一个无限序列(fibs),然后描述序列是由0、1、fibs[0:n]和fibs[1:n+1]相加构成,最后当执行函数f n时,才开始对fibs[n]进行计算。这个算法充满了数学的味道,但如果从计算机的角度则非常难分析,当然也有可能是我太弱,至少我是写不出这样的算法。原网页还有很多类似的算法,但是我智商不给用很多看不懂。

自顶向下写法

算法1:

f 0 = 0
f 1 = 1
f x = (f (x - 1)) + (f (x - 2))

秉承了自顶向下写法一贯的好懂,但是作为教科书级的待优化代码,也是最慢的。

算法2:

a = map f [0..]
f 0 = 0
f 1 = 1
f x = a !! (x - 1) + a !! (x - 2)

这个可以说是比较标准的函数式语言里的记忆化搜索了,先声明一个无穷序列a存储的值是对0到无穷施加函数f的结果,但由于惰性求值的机制,一开始a的值是没有经过计算的,只有在递归的过程中遇到了要求a[x-1]和a[x-2]时才会去求,而求a[x-1]和a[x-2]就是求f(x-1)和f(x-2),实现了搜索,同时当a[x]求出来之后,如果之后还需要a[x]就直接取值就行了,因此也实现了记忆化。美中不足的是,haskell里的list是用链表实现的,因此取索引需要O(n)的复杂度,比较慢。

算法3:

import Data.Sequence
f n = let
a = fromFunction (n + 1) f'
f' 0 = 0
f' 1 = 1
f' x = a `index` (x - 1) + a `index` (x - 2)
in f' n

和上面那个算法差不多,但是使用Sequence代替list,Sequence用的是BST,索引复杂度是O(logn),虽然还是有点浪费,不过也差不多了。haskell里有一个array,虽然支持O(1)索引,但是没有什么map、fromFunction之流可以用。有一个库Vector据说可以符合这样的要求(惰性求值+map或fromFunction+O(1)索引),不过因为不是标准库所以我没尝试。注意Sequence不支持无限长度,同时fromFunction传给f'的是length-1,故传的值是n+1。

总结

目前看来,haskell计算斐波那契数列的方法中,自底向上法效率最高且容易懂的应该是使用fold的方法,自顶向下法效率最高的应该是利用一个O(1)索引且支持惰性求值的数据结构作为记忆表进行记忆化搜索的方法。显然,使用自底向上更优一些,这在其他范式的语言也是一样,没有递归负担,容易优化(滚动数组省空间、特定问题使用单调队列、斜率优化等),而且部分函数式编程语言不支持惰性求值,则直接关上了记忆化搜索的大门。总的说来,斐波那契数列还是一个极为简单的例子,函数式语言实现动态规划,仍然是值得深入研究的一个问题。

关于Haskell计算斐波那契数列的思考的更多相关文章

  1. 使用并行的方法计算斐波那契数列 (Fibonacci)

    更新:我的同事Terry告诉我有一种矩阵运算的方式计算斐波那契数列,更适于并行.他还提供了利用TBB的parallel_reduce模板计算斐波那契数列的代码(在TBB示例代码的基础上修改得来,比原始 ...

  2. Android NDK入门实例 计算斐波那契数列一生成jni头文件

    最近要用到Android NDK,调用本地代码.就学了下Android NDK,顺便与大家分享.下面以一个具体的实例计算斐波那契数列,说明如何利用Android NDK,调用本地代码.以及比较本地代码 ...

  3. 以计算斐波那契数列为例说说动态规划算法(Dynamic Programming Algorithm Overlapping subproblems Optimal substructure Memoization Tabulation)

    动态规划(Dynamic Programming)是求解决策过程(decision process)最优化的数学方法.它的名字和动态没有关系,是Richard Bellman为了唬人而取的. 动态规划 ...

  4. 用递归方法计算斐波那契数列(Recursion Fibonacci Sequence Python)

    先科普一下什么叫斐波那契数列,以下内容摘自百度百科: 斐波那契数列(Fibonacci sequence),又称黄金分割数列.因意大利数学家列昂纳多·斐波那契(Leonardoda Fibonacci ...

  5. shell脚本计算斐波那契数列

    计算斐波那契数列 [1,1,2,3,5,8,,,,,] #!/bin/bash n=$ num=( ) i= while [[ $i -lt $n ]] do let num[$i]=num[$i-] ...

  6. java 递归及其经典应用--求阶乘、打印文件信息、计算斐波那契数列

    什么是递归 我先看下百度百科的解释: 一种计算过程,如果其中每一步都要用到前一步或前几步的结果,称为递归的.用递归过程定义的函数,称为递归函数,例如连加.连乘及阶乘等.凡是递归的函数,都是可计算的,即 ...

  7. X86汇编——计算斐波那契数列程序(详细注释和流程图说明)

    X86汇编实现斐波那契数列 程序说明: 输入斐波那契数列的项数, 然后依次输出斐波那契数列, 输入的项数小于256且为数字, 计算的项数不能超过2^16次方, 输入失败是 不会回显数字 因为存结果是A ...

  8. python计算斐波那契数列

    斐波那契数列就是黄金分割数列 第一项加第二项等于第三项,以此类推 第二项加第三项等于第四项 代码如下 这一段代码实现fib(n)函数返回第n项,PrintFN(m,n,i)函数实现输出第i项斐波那契数 ...

  9. Android NDK入门实例 计算斐波那契数列二生成.so库文件

    上一篇文章输生成了jni头文件,里面包含了本地C代码的信息,提供我们引用的C头文件.下面实现本地代码,再用ndk-build编译生成.so库文件.由于编译时要用到make和gcc,这里很多人是通过安装 ...

随机推荐

  1. 兄弟打印机MFC代码示范

    m_strModel.LoadString(IDS_MODEL_STRING); //IDS_MODEL_STRING,字符串控件的ID,资源视图-String Table里面设置 m_strSour ...

  2. c++. Run-Time Check Failure #2 - Stack around the variable 'cc' was corrupted.

    Run-Time Check Failure #2 - Stack around the variable 'cc' was corrupted. char cc[1024];   //此处如果索引值 ...

  3. 05.DRF-Django REST framework 简介

    一.明确REST接口开发的核心任务 分析一下上节的案例,可以发现,在开发REST API接口时,视图中做的最主要有三件事: 将请求的数据(如JSON格式)转换为模型类对象 操作数据库 将模型类对象转换 ...

  4. 一个老牌程序员说:做Java开发,怎么可以不会这 20 种类库和 API

  5. 从 0 开始机器学习 - 神经网络反向 BP 算法!

    最近一个月项目好忙,终于挤出时间把这篇 BP 算法基本思想写完了,公式的推导放到下一篇讲吧. 一.神经网络的代价函数 神经网络可以看做是复杂逻辑回归的组合,因此与其类似,我们训练神经网络也要定义代价函 ...

  6. spring boot actuator端点高级进阶metris指标详解、git配置详解、自定义扩展详解

    https://www.cnblogs.com/duanxz/p/3508267.html 前言 接着上一篇<Springboot Actuator之一:执行器Actuator入门介绍>a ...

  7. 删库吧,Bug浪——我们在同一家摸鱼的公司

    那些口口声声, Bug越来越难写人的,应该盯着你们: 像我一样,我盯着你们,满眼恨意. IT积攒了几十年的漏洞, 所有的死机.溢出.404和超时, 像是专门为你们准备的礼物. 圈复杂度.魔鬼变量.内存 ...

  8. Python3-collections模块-容器数据类型

    Python3中的collections模块实现了一些专业的容器数据类型 最常用的容器数据类型 字典.列表和元组.集合都已经被Python默认导入,但在实现一些特定的业务时,collections模块 ...

  9. mybatis缓存之一级缓存(二)

    这篇文章介绍下mybatis的一级缓存的生命周期 一级缓存的产生 一级缓存的产生,并不是看mappper的xml文件的select方法,看下面的例子 mapper.xml <select id= ...

  10. 入门大数据---SparkSQL联结操作

    一. 数据准备 本文主要介绍 Spark SQL 的多表连接,需要预先准备测试数据.分别创建员工和部门的 Datafame,并注册为临时视图,代码如下: val spark = SparkSessio ...