• python 程序的计算代价(复杂度)


    要点概论

    1. 时间开销

    2. 空间开销

    3. Python 程序的时间复杂度实例

    4. 程序实现和效率陷阱

    1. 时间开销

      在考虑 python 程序的时间开销时,有一个问题特别需要注意:python 程序中的很多基本操作不是常量时间的。

      

      下面是一些情况:

      1)基本算术运算时常量时间操作【注:】,逻辑运算时常量时间运算。

      2)组合对象的操作有些是常量时间的,有些不是,例如:

        ① 复制和切片操作通常需要线性时间(与长度有关,是 O(n))时间操作。

        ② list 和 tuple 的元素访问和元素赋值,是常量时间的。

        ③ dict 操作的情况比较复杂【补充一个链接地址】。

      3)字符串也应该看作组合对象,但许多操作不是常量时间的。

      4)创建对象也需要付出空间和时间,空间和时间代价都与对象大小有关。

        对于组合对象,这里可能有需要构造的一个个元素,元素有大小问题,

        整体看还有元素个数问题。通常应看作线性时间和线性空间操作(以元素个数作为规模)。

      

      Python 结构和操作的效率问题:

      1)构造新结构,如构造 list ,set 等。构造新的空结构(空表,空集合等)是常量时间操作,

        而构造一个包含 n 个元素的结构,则至少需要 O(n) 时间。统计说明,分配长度为 n 个

        元素的存储块的时间代价是 O(n)。

      2)一些 list 操作的效率:表元素访问的元素修改是常量时间操作,但一般的加入、

        删除元素操作(即使只加入一个元素)都是 O(n) 时间操作。

      3) 字典 dict 操作的效率:主要操作是加入新的键值对和基于键查找关联值。

        它们的最坏情况复杂度是 O(n) ,但平均复杂度是 O(l) 。

        也就是说,一般而言字典操作的效率很高,但偶然也会出现效率低的情况。

    2. 空间开销

      在程序里使用任何类型的对象,都需要付出空间的代价。建立一个表或者元组,至少要占用元素个数那么多的空间。

      如果一个表的元素个数与问题规模线性相关,建立它的空间付出至少为 O(n) (如果元素也是新创建的,还需考虑元素本身的存储开销)。

      需要注意两点:

      1)Python 的各种组合数据对象都没有预设的最大元素个数。在实际使用中,这些结构能根据元素个数的增长自动扩充存储空间。

        从空间占用的角度看,其实际开销在存续期间可能变大,但通常不会自动缩小(即使后来元素变得很少了)。

      2)还应该注意 Python 自动存储管理系统的影响。举个例子:如果在程序里建立了一个表,此后一直将其作为某个全局变量的值,

        这个对象就会始终存在并占用存储空间。如果将其作为某个函数里局部变量的值,或者虽然作为全局变量的值,但后来通过

        赋值将其抛弃,这个表对象就可以被回收。

    3. Python 程序的时间复杂度实例

      一个简单的例子。假设需要把得到的一系列数据存入一个表,其中得到一个数据是 O(l) 常量时间操作。代码如下:

    data = []
    
    whille 还有数据:
        x = 下一数据
        data.insert(0,x)  #把所有数据加在表的前面
    
    或者写为:
    
    data = []
    while 还有数据:
        x = 下一数据
        data.insert(len(data),x)  #新数据加在最后,或写append(x)

      前一程序段需要 O(n) 的时间才能完成工作,而后一个程序段只需要 O(n) 时间。造成这种情况与 list 的实现方式有关。

      另一个例子,建立一个表,其中包含从 0 到 10000 X n - 1 的整数值:

    # 先编写一个计算时间的装饰器
    import time
    def timer(test):
        def inner(*args):
            start = time.time()
            test(*args)
            end = time.time() - start
            print(end)
        return inner
    
    @timer
    def test1(n):
        lst = []
        for i in range(n*10000):
            lst = lst + [i]
        return lst
    test1(5)        # 3.522050380706787
    
    @timer
    def test2(n):
        lst = []
        for i in range(n*10000):
            lst.append(i)
        return lst
    test2(5)      # 0.004010200500488281
    
    @timer
    def test3(n):
        return [i for i in range(n*10000)]
    test3(5)    # 0.0020058155059814453
    
    @timer
    def test4(n):
        return list(range(n*10000))
    test4(5)    # 0.001033782958984375
    View Code

      测试环境为小米 pro 15.6 笔记本【i5处理器】。

      试着尝试变化 n 值查看不同函数的增长趋势。

    4. 程序实现和效率陷阱

      设计一个算法,通过对它的分析可能得到抽象算法的时间与空间复杂度。

      进而,采用某个变成语言可以做出该算法的实现。

      那么问题来了,算法的实现(程序)的时间开销与原算法的时间复杂度之间有什么关系?

      理想情况是:

      作为算法的实现,相应程序的时间开销增加趋势应该达到原算法的时间复杂度。但是,如果实现做得不好,其实现程序也可能比这差。

      前一话题就有这样的例子,例如函数 test1 :最常见的错误做法就是毫无必要地构造一些可能很大的复杂结构。

      例如在递归定义的函数里的递归调用中构造复杂的结构(如 list 等),而后只使用其中的个别元素。

      从局部看,这样做使得常量时间的操作变成了线性时间操作。

      但从递归算法的全局看,这种做法经常会使多项式时间算法变成指数时间算法。

      也就是说,把原来有用的算法变成了基本无用的算法。

  • 相关阅读:
    Spring Bean的作用域类型
    spring depends-on
    spring bean parent属性详解
    spring中autowire的用法
    Spring容器的属性配置详解的六个专题
    Spring bean注入方式
    Spring入门示例
    如何从官网下载Spring
    Hibernate 缓存
    [转]javascript Date format(js日期格式化)
  • 原文地址:https://www.cnblogs.com/HZY258/p/8610103.html
Copyright © 2020-2023  润新知