[英]Python - Modifying beginning and end of list in one iteration vs modifying one end at a time using two iterations speed
在查看关于除自我之外的数组乘积的讨论时,我尝试了两种不同的技术,但我无法理解这一点。 为什么使用一次迭代(解决方案 1)在开头和结尾修改列表比从开头修改列表然后使用两个 for 循环(解决方案 2)反向修改列表要慢?
我的困惑来自哪里:
因此解决方案1应该是:
解决方案 2 将是:
所以它们在技术上都是 O(n),但解决方案 2 仍然有第二次迭代,但不知何故它运行得更快,不一样,更快!
我也知道 Leetcode 在判断运行速度方面并不是最好的,因为它变化很大,但我已经运行了很多次,它总是显示解决方案 2 运行得更快。 我不明白我在这里缺少什么。
解决方案 1
ans = [1] * len(nums)
left = 1
right = 1
for i in range(len(nums)):
ans[i] *= left
ans[-1-i] *= right
left *= nums[i]
right *= nums[-1-i]
return ans
解决方案 2
prod = 1
ans = [1]*len(nums)
for x in range(0,len(nums)):
ans[x] *= prod
prod *= nums[x]
prod = 1
for x in range(len(nums)-1, -1 , -1):
ans[x] *= prod
prod *= nums[x]
return ans
它不一定更快。 你没有具体说明你是如何得到结果的。 我在 CPython 和 PyPy 中对其进行了测试,而解决方案 2 在 CPython 中胜出,解决方案 1 在 PyPy 中胜出。 (请参阅下面的测试代码。)
$ python --version
Python 3.7.6
$ python test.py
Solution 1 time: 2.9916164000001118
Solution 2 time: 2.6632864999996855
$ python test.py
Solution 1 time: 2.857291400000122
Solution 2 time: 2.854712400000153
$ python test.py
Solution 1 time: 2.7937206999999944
Solution 2 time: 2.5544856999999865
$ pypy3 --version
Python 3.6.12 (7.3.3+dfsg-3~ppa1~ubuntu18.04, Feb 25 2021, 20:14:47)
$ pypy3 test.py
Solution 1 time: 0.07995370000026014
Solution 2 time: 0.09105890000000727
$ pypy3 test.py
Solution 1 time: 0.07695659999990312
Solution 2 time: 0.08727580000004309
$ pypy3 test.py
Solution 1 time: 0.07859189999999217
Solution 2 time: 0.09762659999978496
好吧,还要注意 CPython 比 PyPy 慢得多。 解释常规 CPython。 与运行编译代码相比,解释 Python 代码非常非常慢。 Python 解释器循环,执行字节码操作。 每次都必须解释循环中的每个操作码。 但是,for 循环本身的基础结构,即调用迭代器并检查它是否应该继续循环的代码,特别是range
迭代器本身根本没有执行任何解释的 Python 代码。 它们都是预编译的本机代码。 与在循环内执行 Python 代码指令相比,它们的成本可以忽略不计。 解决方案 1 在循环内做了更多的工作。 不多,但每次都要做两次额外的减法。
相比之下,在 PyPy 中,您可以期望所有内容都被编译为本机代码。 这使循环体中的 Python 代码更快。 快得多,现在它的成本与实现range
迭代器的代码相当。 在这一点上,您没有两次应用迭代器这一事实确实足以让解决方案 1 胜出。
说了这么多,我可能是错的,很难确定代码将如何执行。 而且我还没有完成此处需要确定的详尽剖析 - 我刚刚提出了一个合理的解释,也可能一种算法比另一种算法对缓存更不友好。 或 CPU 中更好的管道。 我只是认为这种事情不会对解释型 CPython 产生太大影响。
我确实发现,如果您更改解决方案 1 以删除减法,它的运行时间与 CPython 中的解决方案 2 大致相同。 当然,它得到了错误的答案,但这确实让我觉得我的解释是合理的。
您链接到一个 Leetcode 问题语句,该语句约束输入,以保证nums
的每个前缀和每个后缀都有一个适合 32 位 integer 的乘积。 (我假设它们是有符号整数。)这对nums
的大小和值有很大的限制。 除非nums
中有0
(这使问题变得微不足道),否则nums
不能有超过 31 个不是1
或-1
的值。 这就是为什么我选择了一个在 [1..7] 范围内包含 20 个整数的测试数组。 然而,这对于谈论渐近复杂性来说是非常小的。 渐近复杂度告诉您当 N 变得“足够大”以至于您可以忽略恒定时间因素时算法的行为方式。 如果 N 的上限很小,您可能永远不会达到“足够大”。
即使算法复杂性是一个有用的工具,它仍然无法告诉您两种 O(N) 算法中的哪一种会更快。 它所能告诉你的是,如果两种算法具有不同的时间复杂度,那么有一些N 高于它,具有较低复杂度的算法将始终更快。
import timeit
import random
def s1(nums):
ans = [1] * len(nums)
left = 1
right = 1
for i in range(len(nums)):
ans[i] *= left
ans[-1-i] *= right
left *= nums[i]
right *= nums[-1-i]
return ans
def s2(nums):
prod = 1
ans = [1]*len(nums)
for x in range(0,len(nums)):
ans[x] *= prod
prod *= nums[x]
prod = 1
for x in range(len(nums)-1, -1 , -1):
ans[x] *= prod
prod *= nums[x]
return ans
def main():
r = random.Random(12345)
nums = [r.randint(1,7) for i in range(20)]
print('Solution 1 time:', timeit.timeit(lambda:s1(nums),number=500000))
print('Solution 2 time:', timeit.timeit(lambda:s2(nums),number=500000))
if __name__ == '__main__':
main()
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.