为什么在Python中从列表中删除不必要的项时计算时间会减少

时间:2017-08-11 20:45:02

标签: python time-complexity fibonacci

过去几天我一直在努力更好地理解计算复杂性以及如何改进Python代码。为此,我尝试了不同的函数来计算Fibonacci数,比较如果我进行小的更改,脚本运行的时间。

我正在使用列表计算斐波那契数字,从列表中添加元素-2和-1的总和。

我很困惑地发现,如果我在循环中添加.pop(),删除列表中不需要的元素,我的脚本运行得更快。我不明白为什么会这样。循环中的每一步计算机都会做一件事。所以我未经训练的直觉表明这会增加计算时间。当列表很长时,“查找”列表的最后一个元素会慢得多吗?

这是我的代码:

import time
import numpy as np

def fib_stack1(n):
    """ Original function """
    assert type(n) is int, 'Expected an integer as input.'
    if n < 2:
        return n
    else:
        stack = [0, 1]
        for i in range(n-1):
            stack.append(stack[-1] + stack[-2])
        return stack[-1]

def fib_stack2(n):
    """ Modified function """
    assert type(n) is int, 'Expected an integer as input.'
    if n < 2:
        return n
    else:
        stack = [0, 1]
        for i in range(n-1):
            stack.append(stack[-1] + stack[-2])
            ### CHANGE ###
            stack.pop(-3)
            ##############
        return stack[-1] 


rec1 = []
rec2 = []
for _ in range(10):
    t1 = time.time()
    fib_stack1(99999)  
    t2 = time.time()
    rec1.append(t2-t1)
    t1 = time.time()
    fib_stack2(99999)  
    t2 = time.time()
    rec2.append(t2-t1)
print(np.array(rec1).mean())
print(np.array(rec2).mean())

输出如下:

# Original 
0.26878631115
# Modified
0.145034956932

3 个答案:

答案 0 :(得分:6)

list连续方式将其元素存储在内存中。

因此append对象的list方法需要不时地调整分配的内存块(不是每次都调用append),幸运的是)

有时,系统可以调整原位&#34;就地&#34; (在当前内存块之后分配更多内存),有时不会:它必须找到一个足够大的内存块来存储新列表。

当调整大小不是&#34;就地&#34;时,需要复制现有数据。 (请注意,当列表的大小减少时,不会发生这种情况)

因此,如果列表中复制的元素较少,则操作会更快。

请注意list.append仍然非常快。在列表末尾添加是最快的方式(与insert相比,每次必须移动元素以释放其#34;插槽&#34;)

答案 1 :(得分:6)

  

当列表很长时,“查找”列表的最后一个元素会慢得多吗?

不,列表长度对查找速度没有影响。这些是arraylists,而不是链接列表。这更可能与内存分配或缓存性能有关。垃圾收集器也参与其中。

当您删除不需要的列表元素时,Python永远不必为列表分配更大的缓冲区。它也可以重用为A = np.empty( (len(files), 100, 100) ) 对象分配的内存,而不是从OS请求更多内存。考虑到你的整数有多大,重用他们的记忆是一件大事。 (内存分配的细节取决于Python版本和底层标准库分配器.Python 2有int的免费列表,但不是int s; Python 3没有{{{的免费列表1}} s。Python本身不会为大对象重用分配,但底层分配器可能正在做某事。)

此外,当你必须继续分配新的整数时,特别是那些像99999th斐波纳契数一样大的整数,你不会从CPU的缓存中获得太多的好处。主内存访问比缓存慢得多。

最后,你的long的分配模式(大量的分配,而不是那么多的对象引用计数降到0)会触发Python的循环检测器系统,也就是垃圾收集器,这需要时间来运行和接触很多内存不需要触摸,损害缓存性能。暂时disabling the collector在我自己的测试中为int产生了显着的加速,特别是在Python 3上。

答案 2 :(得分:3)

不,查找列表中的任何元素都是在相同的时间内完成的(计算机科学中所谓的恒定时间行为)。将调用添加到pop确实会增加每次循环迭代所需的工作量,但列表永远不会超过3个元素。在您的第一个版本中,列表会在每次迭代中增长,并且此类操作可以完全免费非常昂贵,具体取决于附加 >列表实际上已分配的内存,这是一种无法直接访问的信息。

基本上,当您实例化一个列表时,会预先分配一些额外的空间,为将来的append腾出空间而牺牲&#34;浪费&#34;空间。如果列表被填满,则需要进一步扩大以进一步append s发生,因此这些特定的附加要比通常更昂贵。如果数组末尾的内存中已经存在其他一些数据,则必须将列表元素中的所有数据(实际上只是指针)复制到新的内存位置,新列表可以存储在一个连续的内存块中。

有关列表增长行为的更多信息(仅在CPython中,因为这是特定于实现的),请参阅例如here