背景

众所周知,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. 弹出框Alert

    selenium提供了三个处理alert的方法 注意:首先需要切换窗口到alert driver.switch_to.alert() (1)点击确定按钮 driver.switch_to.alert. ...

  2. cb36a_c++_STL_算法_区间的比较equal_mismatch_lexicographical_compare

    *cb36a_c++_STL_算法_区间的比较equal_mismatch_lexicographical_compare 区间:容器中的全部数据或者部分数据,都叫做区间 equal(b,e,b2), ...

  3. JS新界面关闭原界面刷新的多种形式

    ------------恢复内容开始------------ 1.新界面为当前界面弹出的形式: 第一步:首先原界面的原生JS事件需要放到 (function () {  ......  })(); 中 ...

  4. 用OpenPyXL处理Excel表格 - 向sheet读取、写入数据

    假设一个名叫"模板"的excel表格里有四个sheet,名字分别是['平台', '制冷', '洗衣机', '空调'] 1.读取 from openpyxl import load_ ...

  5. selenium(3)-针对鼠标的操作

    背景 用selenium做自动化,有时候会遇到需要模拟鼠标操作才能进行的情况,比如单击.双击.点击鼠标右键.拖拽等等. 而selenium给我们提供了一个类来处理这类事件-----------Acti ...

  6. thinkphp3.2 where 条件查询

    thinkphp3.2 where 条件查询 在连贯操作中条件where的操作有时候自己很晕,所以整理下,有助于使用 查询条件 支持的表达式查询,tp不区分大小写 含义 TP运算符 SQL运算符 例子 ...

  7. js事件入门(1)

    1.事件相关概念 1.1 什么是事件? 事件是用户在访问页面时执行的操作,也就是用户访问页面时的行为.当浏览器探测到一个事件时,比如鼠标点击或者按键.它可以触发与这个事件相关的JavaScript对象 ...

  8. CListCtrl 控件即使跟新数据,即时刷新以及属性设置

    用 m_CtrItem.Update( i );来即使跟新每行的数据,因为有时用某些函数如SetItemText()来设置某一行一列的数据是,控件上面的显示数据没有即使跟新,这是就有update来跟新 ...

  9. asp .net core发布订阅kafka

    Kafka是一种高吞吐量的分布式发布订阅消息系统,有如下特性: 通过O的磁盘数据结构提供消息的持久化,这种结构对于即使数以TB的消息存储也能够保持长时间的稳定性能. 高吞吐量:即使是非常普通的硬件Ka ...

  10. JavaScript基础Literal 与 Constructor(008)

    JavaScript支持以字面声名法(Literal)的方式来声名对象和数组,相对于构造函数(constructor)的方式,Literal的声 名方式更简洁,更易读,也更少导致Bug.事实上,JSO ...