阅读 62

Python中斐波那契数列的四种写法

在这些时候,我可以附和着笑,项目经理是决不责备的。而且项目经理见了孔乙己,也每每这样问他,引人发笑。孔乙己自己知道不能和他们谈天,便只好向新人说话。有一回对我说道,“你学过数据结构吗?”我略略点一点头。他说,“学过数据结构,……我便考你一考。斐波那契数列用Python怎样写的?”我想,讨饭一样的人,也配考我么?便回过脸去,不再理会。孔乙己等了许久,很恳切的说道,“不能写罢?……我教给你,记着!这些字应该记着。将来做项目经理的时候,写账要用。”我暗想我和项目经理的等级还很远呢,而且我们项目里也用不到斐波那契数列;又好笑,又不耐烦,懒懒的答他道,“谁要你教,不是f(n) = f(n-1)+f(n-2)吗?”孔乙己显出极高兴的样子,将两个指头的长指甲敲着办公桌,点头说,“对呀对呀!……斐波那契数列在python中有四种写法,你知道么?”我愈不耐烦了,努着嘴走远。孔乙己刚拿出笔记本电脑,想要写几段程序,见我毫不热心,便又叹一口气,显出极惋惜的样子。

斐波那契数列的定义

F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)

CODE

本次介绍Python中斐波那契数列的四种写法,第一种写法比较常见,第二种写法也比较常见.(鲁迅听了想打人).咳咳.第一种依赖于递归,第二种依赖与循环,前两种算法都是可以在几乎所有编程语言里面都能都快速移植的.我们先从这两种介绍

第一种:递归

# 递归
def Fibonacci_Recursion_tool(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return Fibonacci_Recursion_tool(n - 1) + Fibonacci_Recursion_tool(n - 2)


def Fibonacci_Recursion(n):
    result_list = []
    for i in range(1, n + 1): result_list.append(Fibonacci_Recursion_tool(i))
    return result_list
复制代码

在代码中,对于第n个斐波那契数列我们采用递归生成.而对于获取list形式的斐波那契数列我们则采取for循环获取.

但是我们将递归拆开来看就会发现一个巨大的问题,f(n) = f(n-1)+f(n-2),为了求一个f(n),我们要多次计算前面的数值,这样既浪费了内存,又浪费了计算能力,那有没有更简单的算法呢?答案是必然的.使用循环我们就可以避免这个问题.

既然代码已经写完了,那么问题来了,到底什么是递归呢?
在这里插入图片描述

第二种:循环

def Fibonacci_Loop_tool(n):
    a, b = 0, 1
    while n > 0:
        a, b = b, a + b
        n -= 1


def Fibonacci_Loop(n):
    result_list = []
    a, b = 0, 1
    while n > 0:
        result_list.append(b)
        a, b = b, a + b
        n -= 1
    return result_list
复制代码

Fibonacci_Loop_tool 是获取 对应位数的类,而 Fibonacci_Loop 是获取获取数列的类.在这里比较特别的计算方式就在于,我们改从后向前的运算为从前到后,只是用a,b两个变量交替向前,这样就减少了生成数列的速度,当然如果想要生成某一个值的话,却需要从头开始计算在有些时候确实不太方便.

不过在介绍另外一种特殊的可以直接计算第n位数值的算法前,我们不如首先利用Python的特性来做优化一下循环生成斐波那契数列的算法.

yield关键字

对于前面的循环loop,当用其生成数列时,存在一个问题,那就是必须使用一个list来获取存储每次计算的数值,代码十分地不优雅.python是优雅的编程语言,我们使用yield关键字,就可以让代码变得优雅起来.

def Fibonacci_Yield_tool(n):
    a, b = 0, 1
    while n > 0:
        yield b
        a, b = b, a + b
        n -= 1


def Fibonacci_Yield(n):
    # return [f for i, f in enumerate(Fibonacci_Yield_tool(n))]
    return list(Fibonacci_Yield_tool(n))
复制代码

相比于之前的Fibonacci_Loop,Fibonacci_Yield 明显优雅了很多,yield关键字,可以在不打断循环的情况下从循环中返回数值,这个特性简直是nice,不过其返回的数据类型也变得有点特别,变成了generator,那么取值也就比以前不同了一点,这里有两种方式,第一种是直接转化为list,使用list()即可,这种方式比较快,速度消耗很小.除此之外还有第二种,return [f for i, f in enumerate(Fibonacci_Yield_tool(n))]列表解析,这种方式转换速度很慢,在n=1000左右时,在我的电脑上速度接不断接近于1s,但是让我们只运行Fibonacci_Yield_tool()却会发现,Fibonacci_Yield_tool的运行耗时只在10的负六次方到五次方之间.而直接转化为list则对计算速度的影响较小,接近于纯粹的计算耗时本身,几乎不存在影响.在本例子中循环算法与yield关键字(list转换)的速度接近.

第四种,矩阵求解_直接求出第n位数值

好了,Y(o)Y介绍完了前面的内容,我们终于可以开始介绍矩阵直接求解了。要知道前面的三种算法都存在一个很大的问题,那就是无论计算哪一个数值,实际上都要将前面的数值f(n-?)计算一遍。而现在我们终于可以直接求解了,想想还有点小激动呢。

在这里插入图片描述

那么我们现在就通过矩阵求解就可以避免重复计算的问题,当然这里的矩阵求解并不是指向量化,而是指原斐波那契数列求解公式的转化,将其转化为新的求解方式,利用矩阵运算直接求解第n位的数值,简单来说就是——

# 矩阵
Matrix = np.matrix('1 1;1 0')
# 其n-1 次方的第一位,也就是Matrix(11)--下标11就是斐波那契数列的解
复制代码

复杂来说,就是——

def Fibonacci_Matrix_tool(n): # 递归求解,速度慢与直接求方
    Matrix = np.matrix('1 1;1 0')
    if n == 1:
        return Matrix
    if n == 2:
        return pow(Matrix, 2)
    elif n % 2 == 1:
        return Fibonacci_Matrix_tool((n - 1) / 2) ** 2 * Matrix
    else:
        return Fibonacci_Matrix_tool(n / 2) ** 2


def Fibonacci_Matrix_tool2(n):
    Matrix = np.matrix('1 1;1 0')
    return pow(Matrix, n) # pow函数速度快于 使用双星号 "**"


def Fibonacci_Matrix(n):
    result_list = []
    for i in range(0, n): result_list.append(np.array(Fibonacci_Matrix_tool2(i))[0][0])
    return result_list
复制代码

等等,这他喵是什么玩意,你就是在难为我段子手胖虎!

在这里插入图片描述
不过话说回来,其实这个很简单的,就是菲波那切数列的求解公式,用矩阵求解而已。原理吗,其实就是斐波那契额同样公式的再简化,有兴趣的同学可以自己搜几篇论文看看,公式如下:
在这里插入图片描述

性能比较

性能对比比较简单,这里我们使用time函数进行计时。同时使用numpy类库保存到文件中,之后可以用此来绘图。具体代码如下:

def Test_Fibonacci(n, list):
    t1 = time.clock()
    Fibonacci_Recursion(n)
    t2 = time.clock()
    l1 = t2 - t1

    t1 = time.clock()
    Fibonacci_Loop(n)
    t2 = time.clock()
    l2 = t2 - t1

    t1 = time.clock()
    Fibonacci_Yield(n)
    t2 = time.clock()
    l3 = t2 - t1

    t1 = time.clock()
    Fibonacci_Matrix(n)
    t2 = time.clock()
    l4 = t2 - t1
    list.append([l1,l2, l3, l4])
    print("第%d次的测试结果为:" % n, [l1,l2, l3, l4])


def Test_Save(times_items, filename):
    times_list = []
    for i in range(1, times_items + 1): Test_Fibonacci(i, times_list)
    np.savetxt(filename, times_list)


def Test_Print(Test_Print_n):
    print(Fibonacci_Recursion(Test_Print_n))
    print(Fibonacci_Loop(Test_Print_n))
    print(Fibonacci_Yield(Test_Print_n))
    print(Fibonacci_Matrix(Test_Print_n))


times_items = 40
filename = "/home/fonttian/Data/17_DS_AI/Fibonacci/Fibonacci_all.txt"
# Test_Save(times_items,filename)
复制代码

然后我们进行多次计算最终得到了多种情况下的四种方法的性能数据。那么问题又来了,穿山甲到底说了什么 ,哪种方法是性能最好的呢?

在这里插入图片描述

我们打开我们的火眼金睛,然后第一个发现的就是一号这个内奸。从效果来看第一种效果最差,当其在35以上的运算次数时,其耗时就会达到1s,40秒的时候就会过百秒。咳咳,真是有点过分了。不顾好在其他的计算速度则仍然在十的负五次方到负六次方之间。

在这里插入图片描述

但是之后的比较就会出现一些问题。比如在早期较低的CPU频率(约12G,能分给Python8~9G)的测试中,当次数大于1000时,loop的速度开始明显不足,从这里看似乎生成器比循环要好些。但当CPU频率不再是程序瓶颈时,却又会发生逆转。如总计算频率为48G时,无论是循环还是生成器都将占用约20的计算资源,计算速度却一直持平。其实熟悉yield的人基本会预料到这种情况,因为生成器本身就是一个循环不停止的“return”。而此时生成器的一个劣势就很明显的出现了,那就是内存,Python自身的垃圾回收机制并不强,而在高频率的计算中生成器占用的内存长期得不到释放,因此会越变越多,当到达万次时,内存就可以稳定积累到1G以上,是不是十分可怕。

那么看来矩阵解法一定是最好最合适的算法喽,答:当然不是!

在这里插入图片描述

从数学上看,当计算次数比较多时,矩阵一定是最好的方法,可以直接求一个结果就是最好的方法。然而实验数据却又狠狠打了我们的脸。因为np.Matrix在大数量级运算时存在一个问题——内存溢出(导致的数值错误)。。。。。。。

在这里插入图片描述

不过整体而言,定睛一看,矩阵求解最起码也得上亿才会溢出,而且这只是使用工具的问题,换个方法写,公式不变其实是完全是可行的,只是编写会麻烦一点。而返回来目前最好的还是yield,这是python出色设计的功劳。不愧是我最爱的Python,溜了溜了。

在这里插入图片描述

文章分类
后端
文章标签