【问题标题】:Execution time difference between x += y and x = x + y [duplicate]x += y 和 x = x + y 之间的执行时间差 [重复]
【发布时间】:2021-09-11 01:57:31
【问题描述】:

我试图将我的解决方案提交给 leetcode 问题,其中 xy 是列表并使用

x = x + y

给了我一个超过时间限制 然而 使用

x += y

通过了测试用例并给了我AC

两者的执行时间差是多少,两者的执行方式有什么不同?

【问题讨论】:

  • += 有一个魔术命名的快捷功能,可以就地追加。您的第一个选项必须创建一个全新的列表并替换第一个列表。
  • += 调用 __iadd__(如果已定义),然后回退到 __add__a+b 致电 __add__。在某些情况下,它是相同的。在列表的情况下,__iadd__ 大概调用了extend,因此避免将temp 复制到新列表

标签: python python-3.x list


【解决方案1】:

对于列表对象,

temp = temp + [] 

创建一个新列表,并在结果列表的大小上花费线性时间(它线性地缩放)。重要的是,它重新创建了整个新列表。如果在循环中完成,例如

x = []

for i in range(N):
    x = x + [i]

整个算法是二次时间,O(N^2)

另一方面,temp += [] 在原地工作。它不会创建新列表。它是 O(K),其中 K 是右侧列表的大小,即添加的元素数。这是因为 python 列表对象被实现为数组列表,overallocate 所以你不必在每次列表大小增加时重新分配。简单地说,这意味着将一个项目附加到列表的末尾是摊销常数时间。重要的是,这使得:

x = []

for i in range(N):
    x += [i]

线性时间,即 O(N)。

要凭经验查看此行为,您可以使用以下脚本:

import pandas as pd
import matplotlib.pyplot as plt
import time

def concatenate(N):
    result = []
    for i in range(N):
        result = result + [i]

def inplace(N):
    result = []
    for i in range(N):
        result += [i]


def time_func(N, f):
    start = time.perf_counter()
    f(N)
    stop = time.perf_counter()
    return stop - start

NS = range(0, 100_001, 10_000)
inplc = [time_func(n, inplace) for n in NS]
concat = [time_func(n, concatenate) for n in NS]

df = pd.DataFrame({"in-place":inplc, "concat": concat}, index=NS)

df.plot()
plt.savefig('in-place-vs-new-list-loop.png')

注意,在N == 100_000,串联版本需要 10 多秒,而就地扩展版本需要 0.01 秒......所以它慢了几个数量级,并且差异将继续显着增长(即二次方)随着 N 的增加而增加。

为了理解这种行为,这里是时间复杂度的非正式处理:

对于 concat,在每次迭代中,x = x + [i] 需要 i 的工作量,其中 i结果数组的长度。所以整个循环将是0 + 1 + 2 + 3 + ... + N。现在,使用 handy formula for the Nth partial sum of this well-known series 循环将需要 N*(N+1)/2 总工作。

N*(N + 1) / 2 == N^2/2 + N/2 这就是 O(N^2)

另一方面,就地扩展版本,在每次迭代中,

temp += [i]

只需要 1 (恒定) 的工作量。所以对于整个循环,它只是

1 + 1 + ... + 1(N次)

所以N总工作量,所以是O(N)

【讨论】:

  • 这是一个绝妙的答案。但是,我认为这个问题至少是我添加的前两个欺骗目标的明显重复。我愿意将这个问题合并到其中一个问题中,这也将把你的答案带到那里,以保存它并使其更加可见。如果您希望我进行合并,请告诉我。 (并且请进行任何必要的修饰,以使您的答案与目标问题相匹配。)
【解决方案2】:

表达式a = a + b 执行以下操作:

  1. 分配一个足以容纳ab 的新列表。
  2. a 复制到新缓冲区。
  3. b 复制到新缓冲区。
  4. 将名称 a 绑定到新缓冲区(这是 list.__add__ 返回的内容)。

在这种情况下分配和复制是不可避免的,不管b是空的。

表达式a += b 大致等价于list.extend,末尾有一个赋值:

  1. a 的缓冲区扩展足够的元素以容纳b。这并不总是涉及重新分配,因为从长远来看,列表增长将花费O(n) 时间。
  2. b复制到a的末尾。
  3. 将名称 a 重新绑定到同一个对象(这是 list.__iadd__ 返回的内容)。

请注意,在这种情况下,step is 是一个重新分配,因此a 的元素仅在内存移动时才被复制。由于在您的情况下b 为空,因此根本不会重新分配或复制任何内容。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-13
    • 1970-01-01
    • 1970-01-01
    • 2017-12-31
    相关资源
    最近更新 更多