【问题标题】:Why is the performance of these two functions so drastically different despite the same Big-O?尽管 Big-O 相同,为什么这两个函数的性能差异如此之大?
【发布时间】:2020-06-25 17:44:13
【问题描述】:

我正在学习 Big-O 符号,不是为了上课,只是通过自己阅读来学习。我正在阅读 Pythonds 并进行了练习,您的任务是编写“非最佳”Python 函数来查找列表中的最小数字。该函数应将每个数字与列表中的每个其他数字进行比较:O(n**2)。

这是我想出的:

def myFindmin(alist):
    return[x for x in alist if all(x<=y for y in alist)][0]

这就是我正在阅读的书给出的内容:

def findmin(alist):
    overallmin=alist[0]
    for i in alist:
        issmallest=True
        for j in alist:
            if i>j:
                issmallest=False
        if issmallest:
            overallmin=i
    return overallmin

显然,书本版本削减了更多变量等,但根据我所学到的,这两个函数的 Big-O 表示法应该是 O(n**2),不是吗?他们都将每个数字与其他数字进行比较,这使得 n**2 成为函数的主要部分,是吗?

但是,当我将 myFindmin() 函数与书中的 findmin() 函数与最佳 min() 函数进行比较时,我得到了三个截然不同的结果:

if __name__=='__main__':
    for l in range(1000,10001,1000):
        thelist=[randrange(100000)for x in range(l)]
        print('size: %d'%l)
        for f in[findmin,myFindmin,min]:
            start=time()
            r=f(thelist)
            end=time()
            print('    function: %s \ttime: %f'%(f.__name__,end-start))

他们甚至没有接近:

...
size: 8000
    function: findmin   time: 1.427166
    function: myFindmin time: 0.004312
    function: min       time: 0.000138
size: 9000
    function: findmin   time: 1.830869
    function: myFindmin time: 0.005525
    function: min       time: 0.000133
size: 10000
    function: findmin   time: 2.280625
    function: myFindmin time: 0.004846
    function: min       time: 0.000145

如您所见,myFindmin() 不如最优线性 O(n) min() 函数快,但仍比 O(n**2) findmin() 函数快得多。我认为 myFindmin 应该是 O(n**2) 但它似乎既不是 O(n) 也不是 O(n**2) 那么这里到底发生了什么? myFindmin 的 Big-O 是什么?

更新

如果我在 all 语句的嵌套循环中添加括号:

def myFindmin(alist):
    return[x for x in alist if all([x<=y for y in alist])][0]

这实际上使 myFindmin 始终比 findmin 慢:

size: 8000
    function: findmin   time: 1.512061
    function: myFindmin time: 1.846030
    function: min       time: 0.000093
size: 9000
    function: findmin   time: 1.925281
    function: myFindmin time: 2.327998
    function: min       time: 0.000104
size: 10000
    function: findmin   time: 2.390210
    function: myFindmin time: 2.922537
    function: min       time: 0.000118

所以这里发生的是,在 original myFindmin 中,整个列表不是通过列表推导生成的,它实际上是由 all() 本身通过生成器表达式生成的,该生成器表达式也执行惰性求值,这意味着一旦发现错误值,它就会停止评估 AND 生成。

如果我添加括号,那么会发生什么,列表会通过不执行惰性评估的列表推导生成和评估。 每次都会生成整个列表,然后将其传递给 all() 以进行惰性重新评估。

由于原始 myFindmin() 的 Big-O 为 O(nlogn),新的 myFindmin()(带括号)将具有 O(n^2+nlogn) 的 Big-O,这反映在结果时间中.有关为什么原始 myFindmin() 为 O(nlogn) 的解释,请参阅我已标记为最佳答案的 Amit 的答案。

谢谢大家!

【问题讨论】:

  • 本书版本中的a是什么?
  • @Barmar 打错字,抱歉,我更正了
  • 列表理解的实现不同于底层 for 循环的实现。列表推导在某些情况下更快。 medium.com/quick-code/…
  • all() 在最坏的情况下只有 O(n)。如果发现虚假值,它会提前退出。
  • @IjonTichy 无法解释这种程度的差异。列表理解本质上是一个底层的 for 循环,只是做了一些小的优化。差异是微不足道的,而不是数量级

标签: python algorithm time-complexity big-o


【解决方案1】:

您的代码实际上是O(nlogn)(平均情况)而不是O(n^2)

看看all(x&lt;=y for y in alist),回想一下要产生False,只要一个元素大于xno need to go through all valuesalist就足够了。

假设您的列表是随机(且均匀地)打乱的,让我们检查需要遍历多少元素:

x is the highest element: traverse n elements
x is the 2nd highest element: traverse n/2 elements
x is the 3rd highest element: traverse n/3 elements
....
x is the smallest element: traverse 1 element

所以,你的算法的实际复杂度是:

T(n) = n/1 + n/2 + n/3 + ... + n/n =
     = n(1 + 1/2 + 1/3 + .... + 1/n)

现在注意1 + 1/2 + .... + 1/nharmonic seriesit is in O(logn) 之和

这为您提供了O(nlogn) 的复杂度,而不是O(n^2) 的平均案例复杂度。

【讨论】:

  • 嗯,这听起来是对的......但为了证明这一点,我将 all(x
  • @JonRed 对不起,我无法理解:This brought myFindmin in line with findmin but why is all() not ditching the list when fed the list in this manner?。 (不是陈述也不是问题)。抱歉,我不是以英语为母语的人。你能改写一下吗?
  • 我想我明白发生了什么。简而言之,你是对的。没有括号,比较是通过 all() 的惰性求值完成的。通过添加括号,通过列表推导对列表中的每个值执行评估,然后由 all() 简单地读取。这就是为什么添加括号会使 myFidmin() 变为 O(n^2)。
  • @JonRed 因为它首先创建一个列表,然后将该列表传递给all。没有括号,它创建了一个惰性求值的生成器,它被传递给allall(或任何其他函数)不知道您传递给它的 表达式 是什么,只是该表达式的计算结果是什么。当它到达all 时,它已经是表达式产生的任何东西了。
猜你喜欢
  • 2011-12-03
  • 1970-01-01
  • 1970-01-01
  • 2012-11-19
  • 1970-01-01
  • 2012-01-20
  • 2019-09-13
  • 2021-10-27
  • 1970-01-01
相关资源
最近更新 更多