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 等),而后只使用其中的个别元素。

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

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

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

 

posted @ 2018-03-20 17:18  皇昭仪  阅读(1089)  评论(0编辑  收藏  举报