【问题标题】:Why bisect slower than sort为什么二分比排序慢
【发布时间】:2016-07-26 18:29:20
【问题描述】:

我知道 bisect 使用二进制搜索来保持列表的排序。但是我做了一个计时测试,这些值正在被读取和排序。但是,与我的知识相反,保留这些值然后对它们进行排序会以高差异赢得时机。有经验的用户能否解释一下这种行为?这是我用来测试时间的代码。

import timeit

setup = """
import random
import bisect
a = range(100000)
random.shuffle(a)
"""

p1 = """
b = []
for i in a:
    b.append(i)
b.sort()
"""

p2 = """
b = []
for i in a:
    bisect.insort(b, i)
"""

print timeit.timeit(p1, setup=setup, number = 1)
print timeit.timeit(p2, setup=setup, number = 1)

# 0.0593081859178
# 1.69218442959
# Huge difference ! 35x faster.

在第一个过程中,我一个接一个地取值,而不是仅仅对a 进行排序以获得文件读取等行为。而且它非常难于平分。

【问题讨论】:

  • 因为 timsort 是一种高效的排序算法,而列表插入很慢?
  • 这可能是原因,但是 bisect 的优势是什么?
  • 什么意思?如果您已经有一个排序列表,并且想要保持排序,那是最好的方法。许多其他操作在排序列表上效率更高。
  • bisect 适用于您搜索列表的频率远高于向列表添加项目的频率。
  • 插入排序。

标签: python algorithm performance sorting


【解决方案1】:

对列表进行排序大约需要O(N*log(N)) 时间。将 N 项附加到列表需要 O(N) 时间。连续做这些事情大约需要O(N*log(N)) 时间。

平分列表需要O(log(n)) 时间。将项目插入列表需要O(N) 时间。在 for 循环中同时执行 N 次需要 O(N * (N + log(n))) == O(N^2) 时间。

O(N^2)O(N*log(N)) 差,所以你的p1 比你的p2 快。

【讨论】:

    【解决方案2】:

    在 bisect 情况下,您的算法复杂度会更差...

    bisect 的情况下,您有N 操作(每个操作的平均成本为log(N) 来查找插入点,然后额外的O(N) 步骤来插入项目)。 总复杂度:O(N^2)

    使用sort,您只需一个Nlog(N) 步骤(加上N O(1) 步骤首先构建列表)。 总复杂度:O(Nlog(N))

    另请注意,sort 是在经过高度优化的 C 代码中实现的(bisect 并没有那么优化,因为它最终会更频繁地调用各种比较函数...)

    【讨论】:

      【解决方案3】:

      要了解时差,让我们看看你在那里实际做了什么。

      在您的第一个示例中,您将获取一个空列表,并将项目附加到其中,并在最后对其进行排序。

      附加到列表真的很便宜,它有一个 O(1) 的amortized time complexity。它不可能是真正的恒定时间,因为底层数据结构,一个简单的数组,最终需要随着列表的增长而扩展。这是经常进行的,这会导致分配一个新数组并复制数据。这有点贵。但总的来说,we still say this is O(1)

      接下来是排序。 Python 正在使用Timsort,这非常有效。这是平均和最坏情况下的 O(n log n)。所以总的来说,我们在O(n log n) 之后得到了固定的时间,所以排序是这里唯一重要的事情。总的来说,这非常简单而且非常快。

      第二个例子使用bisect.insort。这利用列表和二进制搜索来确保列表始终排序

      基本上,在每次插入时,它都会使用二进制搜索来找到插入新值的正确位置,然后正确移动所有项目以在该索引处为新值腾出空间。二进制搜索很便宜,平均为 O(log n),所以这不是问题。单独换档也不是那么困难。在最坏的情况下,我们需要将所有项目向右移动一个索引,所以我们得到 O(n)(这基本上是 insert operation on lists)。

      所以总的来说,我们最坏的情况是线性时间。然而,我们在每一次迭代中都这样做。所以当插入n 元素时,我们每次都有 O(n) 。这导致二次复杂度 O(n²)。这是一个问题,最终会减慢整个进程。

      那么这告诉我们什么? Sorted inserting 进入列表以获得排序结果并不是真正的高性能。当我们只做一些操作时,我们可以使用bisect 模块来保持已经排序的列表有序,但是当我们实际上有未排序的数据时,将数据作为一个整体进行排序会更容易。

      【讨论】:

      • 公平地说,timsort 在你刚刚将一个元素添加到一个已经排序的列表之后也会发光,因为它会有一个真的长的"gallop" 阶段。它可能仍然具有与 bisect.insort 相当的性能...
      【解决方案4】:

      数据结构中的插入和删除操作有时会非常昂贵,尤其是在传入数据值的分布是随机的情况下。然而,排序可能会出乎意料的快。

      一个关键的考虑因素是您是否可以“累积所有值”,然后将它们排序一次,然后“一次全部”使用排序结果。如果可以,那么排序几乎总是非常快。

      如果您还记得旧的科幻电影(当计算机被称为“巨型大脑”并且电影总是有旋转的磁带驱动器时),那就是他们应该做的那种处理:应用 sorted 更新到 also-sorted 母带,以生成新的 still-sorted 母带。不需要随机访问。 (这是一件好事,因为当时我们真的做不到。)它仍然是一种处理大量数据的有效方式。

      【讨论】:

        猜你喜欢
        • 2017-03-18
        • 2017-07-23
        • 2020-02-21
        • 2016-02-18
        • 2021-06-19
        • 1970-01-01
        • 2023-03-20
        • 2021-12-09
        • 2016-09-08
        相关资源
        最近更新 更多