繁体   English   中英

Python - 在一次迭代中修改列表的开头和结尾与使用两次迭代速度一次修改一个结尾

[英]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 是 O(n)(这在两种算法中都是相同的,所以它不应该有所作为)
  • 设置/获取列表中的项目是 O(n)
  • 每次循环遍历一个列表是 O(n)

因此解决方案1应该是:

  • O(n) 用于初始化列表
  • 迭代的 O(n)
  • 一些 O(1) 用于获取/设置列表中的元素
  • = O(n + n) = O(n)

解决方案 2 将是:

  • O(n) 用于初始化列表
  • 第一次迭代 O(n)
  • 第二次迭代的 O(n)
  • 一些 O(1) 用于获取/设置列表中的元素
  • = O(n + n + n) = O(n)

所以它们在技术上都是 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

为什么解决方案 2 在 CPython 中会更快?

好吧,还要注意 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM