为什么对于连续下降的数字列表,内置sorted()会更慢?

32 投票
2 回答
2869 浏览
提问于 2025-04-14 18:09

我对四个相似的列表进行了排序。列表 d 的排序时间总是比其他列表要长得多,而其他列表的排序时间差不多:

a:  33.5 ms
b:  33.4 ms
c:  36.4 ms
d: 110.9 ms

这是为什么呢?

测试脚本(在线尝试这个!):

from timeit import repeat

n = 2_000_000
a = [i // 1 for i in range(n)]  # [0, 1, 2, 3, ..., 1_999_999]
b = [i // 2 for i in range(n)]  # [0, 0, 1, 1, 2, 2, ..., 999_999]
c = a[::-1]                     # [1_999_999, ..., 3, 2, 1, 0]
d = b[::-1]                     # [999_999, ..., 2, 2, 1, 1, 0, 0]

for name in 'abcd':
    lst = globals()[name]
    time = min(repeat(lambda: sorted(lst), number=1))
    print(f'{name}: {time*1e3 :5.1f} ms')

2 个回答

13

正如 @jpa 的 回答 所解释的,abc 只有一个“运行”,而 d 有一百万个。我们可以通过比较的次数来看看这个效果:

a:  1999999 comparisons, 1.000 per element
b:  1999999 comparisons, 1.000 per element
c:  1999999 comparisons, 1.000 per element
d: 10710650 comparisons, 5.355 per element

一个长的运行意味着我们只需要比较每对相邻的元素一次,仅此而已。对于 n 个元素来说,这就是 n-1 次比较。

因为 ab 是升序排列的,所以它们已经排好序了,不需要再做任何处理。c 是降序的,只需将它反转一下,也就完成了。

对于 d,两个元素的运行首先会通过二进制插入排序合并成 32 到 64 个元素的运行。然后这些更长的运行会不断合并成更大更大的运行,直到整个列表都排好序。

我尝试了不同的 n 值。对于 d,每个元素的比较次数大约在 5 次左右,随着 n 增加到 2 的幂次时,比较次数上升到大约 5.3,然后又降到大约 4.7。一般来说,d 需要 O(n) 次比较。它不需要 n log n 次比较,因为合并时使用了“跳跃”(一种指数搜索),在这种情况下只需要对数级别的比较。因此,与普通的归并排序不同,这里的比较次数的递归关系不是 T(n) = 2T(n/2) + n,而是 T(n) = 2T(n/2) + log(n),这也是 O(n)。

不过,运行时间并不是 O(n),因为仍然需要 n log n 次元素的移动。但这属于底层操作,相对比较快,和 O(n) 的慢比较相比,速度要快很多。如果你测量不同 n 值的时间,你可能会觉得它是 O(n) 的时间。

用于计数的脚本(在线尝试这个!):

n = 2_000_000
a = [i // 1 for i in range(n)]
b = [i // 2 for i in range(n)]
c = a[::-1]
d = b[::-1]

class Int:
    def __init__(self, value):
        self.value = value
    def __lt__(self, other):
        global comparisons
        comparisons += 1
        return self.value < other.value

for name in 'abcd':
    lst = globals()[name]
    comparisons = 0
    sorted(map(Int, lst))
    print(f'{name}: {comparisons} comparisons, {comparisons/len(lst) :5.3f} per element')
42

正如btilly和Amadan在评论中提到的,这个问题和Timsort排序算法的工作原理有关。关于这个算法的详细描述可以在这里找到。

Timsort通过识别已经排序的元素序列来加快对部分排序数组的操作。

一个序列可以是“升序”的,也就是不减少:

a0 <= a1 <= a2 <= ...

或者是“降序”的,也就是严格减少:

a0 > a1 > a2 > ...

需要注意的是,一个序列至少要有两个元素,除非我们从数组的最后一个元素开始。

你的数组abc每个都只有一个序列。而数组d有100万个序列。

降序序列不能用>=的原因是为了保持排序的稳定性,也就是说要保持相等元素的顺序:

降序的定义是严格的,因为主要的排序过程会将降序序列就地反转,变成升序序列。反转是通过一种简单快速的方法完成的,即“从两端开始交换元素,向中间靠拢”,如果这个片段中有相等的元素,这样做可能会破坏稳定性。使用严格的降序定义可以确保降序序列中的元素是不同的。

Python 3.11对timsort进行了稍微改进的版本,有时被称为powersort,但它使用相同的序列检测方法,因此性能是一样的。

撰写回答