我知道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
以获得类似文件读取的行为。并且非常努力地击败平分。
答案 0 :(得分:4)
在bisect情况下,你的算法复杂度会更差......
在bisect
案例中,您有N
个操作(每个操作的平均费用为log(N)
以查找插入点,然后再添加O(N)
个步骤以插入项目)。 总体复杂度:O(N^2)
。
使用sort
,您只需一个Nlog(N)
步骤(加上N
O(1)
步骤即可构建列表。 总复杂程度:O(Nlog(N))
另请注意,sort
是在非常优化的C代码中实现的(bisect
并非经过优化,因为它最终会更频繁地调用各种比较函数...)
答案 1 :(得分:3)
对列表进行排序大约需要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 :(得分:1)
要了解时差,让我们来看看你在那里做了什么。
在第一个示例中,您将获取一个空列表,并将项目附加到该列表,并在最后对其进行排序。
附加到列表非常便宜,它有一个amortized time complexity的O(1)。它不可能是真正的恒定时间,因为底层数据结构,一个简单的数组,最终需要随着列表的增长而扩展。这种情况经常发生,导致分配新数组并复制数据。这有点贵。但总的来说,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 2)。这是一个问题,最终将使整个事情变慢。
那么这告诉我们什么? Sorted inserting到列表中以获得排序结果并不是真正有效。当我们只进行一些操作时,我们可以使用bisect
模块来保存已经排序的列表,但是当我们实际上有未排序的数据时,更容易对整个数据进行排序。
答案 3 :(得分:0)
有时,数据结构中的插入和删除操作可能会非常昂贵,特别是如果传入数据值的分布是随机的。然而,排序可能意外地快速。
一个关键的考虑因素是你是否能够积累所有价值,"然后将它们排序一次,然后一次性使用排序后的结果""如果可以,那么排序几乎总是非常快。
如果你还记得那些古老的科幻电影(当电脑被称为#34;大脑和#34;以及电影总是有旋转的磁带机时),那就是那种处理方式。据说:将排序的更新应用于也已排序的主磁带,以生成新的仍然排序的主服务器。不需要随机访问。 (这是一件好事,因为当时我们真的无法做到。)仍是处理大量数据的有效方式。