T

Timsort——你从未听说过的最快的排序算法

2025-05-27

Timsort——你从未听说过的最快的排序算法

Timsort——你从未听说过的最快的排序算法
蒂姆·彼得 (Tim Peter) 的图片来自此处

Timsort 是一种对实际数据高效的排序算法,并非源于学术实验室。Tim Peters 于 2001 年为 Python 编程语言创建了 Timsort。Timsort 首先分析待排序的列表,然后根据分析结果选择合适的排序方法。

自从该算法被发明以来,它就被用作 Python、JavaAndroid平台和 GNU Octave 中的默认排序算法。

Timsort 的大 O 符号是 O(n log n)。要了解大 O 符号,请阅读此文

Timsort——你从未听说过的最快的排序算法
这里

Timsort 的排序时间与归并排序相同,比你可能知道的大多数其他排序方法都要快。Timsort 实际上利用了插入排序和归并排序,你很快就会看到。

Peters 设计 Timsort 是为了利用大多数现实世界数据集中存在的已排序元素。它将这些已排序元素称为“自然运行”。它对数据进行迭代,将元素收集到运行中,并同时将这些运行合并为一个运行。

数组中的元素少于 64 个

如果我们尝试排序的数组中的元素少于 64 个,Timsort 将执行插入排序。

插入排序是一种简单的排序方法,它对小列表最有效。对于较大的列表,它的速度相当慢,但对于小列表,它的速度非常快。插入排序的思路如下:

  • 逐一查看元素
  • 通过在正确的位置插入元素来构建排序列表

Timsort——你从未听说过的最快的排序算法
图片由我拍摄,来自我的网站skerritt.tech

在这种情况下,我们将新排序的元素插入到一个新的子数组中,该子数组从数组的开头开始。

下面是一个显示插入排序的 gif:

Timsort——你从未听说过的最快的排序算法
摘自此处

关于跑步的更多信息

如果列表大于 64 个元素,算法将首先遍历列表,寻找严格递增或递减的部分。如果部分是递减的,算法将反转该部分。

因此,如果运行次数减少,它将看起来像这样(其中运行次数以粗体显示):

Timsort——你从未听说过的最快的排序算法
图片来自我的网站skerritt.tech

如果不减少,它看起来会像这样:

Timsort——你从未听说过的最快的排序算法
图片来自我的网站skerritt.tech

最小运行 (minrun) 的大小取决于数组的大小。算法会选择最小运行,以便随机数组中的大多数运行长度等于或变为最小运行。当运行次数等于或略小于 2 的幂时,合并两个数组会更高效。Timsort 选择最小运行来尝试确保这种效率,方法是确保最小运行等于或小于 2 的幂。

该算法从 32 到 64(含)的范围内选择 minrun。它选择 minrun 的条件是,原始数组的长度除以 minrun 后等于或略小于 2 的幂。

如果运行的长度小于最小运行,则计算该运行相对于最小运行的长度。使用这个新的数字,在运行之前获取那么多元素,然后执行插入排序来创建新的运行。

因此,如果 minrun 为 63 且运行长度为 33,则执行 63-33 = 30。然后从运行结束之前抓取 30 个元素,因此这是来自 run[33] 的 30 个项目,然后执行插入排序以创建新的运行。

这部分完成后,我们现在应该在列表中有一堆已排序的运行。


合并

图片显示龙珠 Z 角色合并

Timsort 现在会执行归并排序,将运行结果合并在一起。然而,Timsort 会确保在归并排序过程中保持稳定性和合并平衡。

为了保持稳定性,我们不应该交换两个相等值的数字。这不仅能保持它们在列表中的原始位置,还能提高算法的执行速度。我们稍后会讨论合并余额。

当 Timsort 找到运行结果时,它会将它们添加到堆栈中。一个简单的堆栈如下所示:

Timsort——你从未听说过的最快的排序算法
图片来自我的网站skerritt.tech

想象一下一叠盘子。你不能从底部取盘子,所以你必须从顶部取。同样的道理也适用于堆叠。

Timsort 在执行归并排序时,会尝试平衡两种相互竞争的需求。一方面,我们希望尽可能延迟合并,以便利用后续可能出现的模式。另一方面,我们更希望尽快进行合并,以便利用刚刚找到的运行仍然位于内存层级较高位置的运行。我们也不能将合并延迟“太久”,因为记住尚未合并的运行会消耗内存,而堆栈的大小是固定的。

为了确保我们达成这一妥协,Timsort 会跟踪堆栈中最近的三个项目,并创建两个必须适用于这些项目的定律:

  1. A > B + C

  2. B > C

其中 A、B 和 C 是堆栈中最新的三个项目。

用蒂姆·彼得斯自己的话来说:

事实证明,一个很好的折衷方案是保留堆栈条目上的两个不变量,其中 A、B 和 C 是三个最右边尚未合并的切片的长度

通常,合并不同长度的相邻运行很困难。更困难的是,我们必须保持稳定性。为了解决这个问题,Timsort 会预留临时内存。它会将两个运行中较小的一个(分别称为运行 A 和运行 B)放入该临时内存中。

奔腾

奔腾的骏马 gif

当 Timsort 合并 A 和 B 时,它注意到一个运行结果已经连续多次“获胜”。如果结果发现 A 运行结果包含的数字远小于 B 运行结果,那么 A 运行结果最终会回到原来的位置。合并这两个运行结果需要大量工作,最终却一无所获。

数据通常会具有一些预先存在的内部结构。Timsort 假设,如果 A 的很多次运行值都低于 B 的值,那么 A 的值很可能会持续小于 B 的值。

Timsort——你从未听说过的最快的排序算法

两个示例运行 A 和 B 的图像。运行必须严格增加或减少,这就是选择这些数字的原因。

然后,Timsort 将进入飞驰模式。Timsort 不会将 A[0] 和 B[0] 相互比较,而是执行二分查找,以查找 a[0] 中 b[0] 的适当位置。这样,Timsort 可以将 A 的整个部分移动到位。然后,Timsort 在 B 中搜索 A[0] 的适当位置。Timsort 会一次性将 B 的整个部分移动到位。

让我们看看实际效果。Timsort 检查 B 0,并使用二分查找在 A 中寻找正确的位置。

嗯,B[0] 应该位于 A 列表的末尾。现在 Timsort 检查 A 0是否位于 B 的正确位置。所以我们要看看数字 1 应该位于哪里。这个数字应该位于 B 的开头。现在我们知道 B 应该位于 A 的末尾,而 A 应该位于 B 的开头。

事实证明,如果 B[0] 的合适位置非常靠近 A 的开头(反之亦然),则此操作不值得。因此,如果疾驰模式没有效果,它会迅速退出。此外,Timsort 会注意到这一点,并通过增加进入疾驰模式所需的连续 A 或 B 获胜次数,使以后进入疾驰模式变得更加困难。如果疾驰模式有效,Timsort 会让重新进入变得更容易。

简而言之,Timsort 在两件事上做得非常好:

  • 在具有预先存在的内部结构的阵列上表现出色
  • 能够保持稳定的排序

以前,为了实现稳定的排序,您必须将列表中的项目与整数压缩在一起,然后将其排序为元组数组。


代码

如果您对代码不感兴趣,请跳过此部分。本节下面还有更多信息。

以下源代码基于我和 Nanda Javarma 的工作。该源代码并不完整,也与 Python 官方的 sorted() 源代码有所不同。这只是我为了大致了解 Timsort 而实现的简化版 Timsort。如果您想完整地了解 Timsort 的原始源代码,请点击此处查看。Timsort 官方是用 C 语言实现的,而不是 Python。

# based off of this code https://gist.github.com/nandajavarma/a3a6b62f34e74ec4c31674934327bbd3
# Brandon Skerritt
# https://skerritt.tech

def binary_search(the_array, item, start, end):
    if start == end:
        if the_array[start] > item:
            return start
        else:
            return start + 1
    if start > end:
        return start

    mid = round((start + end)/ 2)

    if the_array[mid] < item:
        return binary_search(the_array, item, mid + 1, end)

    elif the_array[mid] > item:
        return binary_search(the_array, item, start, mid - 1)

    else:
        return mid

"""
Insertion sort that timsort uses if the array size is small or if
the size of the "run" is small
"""
def insertion_sort(the_array):
    l = len(the_array)
    for index in range(1, l):
        value = the_array[index]
        pos = binary_search(the_array, value, 0, index - 1)
        the_array = the_array[:pos] + [value] + the_array[pos:index] + the_array[index+1:]
    return the_array

def merge(left, right):
    """Takes two sorted lists and returns a single sorted list by comparing the
    elements one at a time.
    [1, 2, 3, 4, 5, 6]
    """
    if not left:
        return right
    if not right:
        return left
    if left[0] < right[0]:
        return [left[0]] + merge(left[1:], right)
    return [right[0]] + merge(left, right[1:])

def timsort(the_array):
    runs, sorted_runs = [], []
    length = len(the_array)
    new_run = [the_array[0]]

    # for every i in the range of 1 to length of array
    for i in range(1, length):
        # if i is at the end of the list
        if i == length - 1:
            new_run.append(the_array[i])
            runs.append(new_run)
            break
        # if the i'th element of the array is less than the one before it
        if the_array[i] < the_array[i-1]:
            # if new_run is set to None (NULL)
            if not new_run:
                runs.append([the_array[i]])
                new_run.append(the_array[i])
            else:
                runs.append(new_run)
                new_run = []
        # else if its equal to or more than
        else:
            new_run.append(the_array[i])

    # for every item in runs, append it using insertion sort
    for item in runs:
        sorted_runs.append(insertion_sort(item))

    # for every run in sorted_runs, merge them
    sorted_array = []
    for run in sorted_runs:
        sorted_array = merge(sorted_array, run)

    print(sorted_array)

timsort([2, 3, 1, 5, 6, 7])
Enter fullscreen mode Exit fullscreen mode

Timsort 实际上是 Python 内置的,所以这段代码仅供参考。要使用 Timsort,只需输入:

list.sort()
Enter fullscreen mode Exit fullscreen mode

或者

sorted(list)
Enter fullscreen mode Exit fullscreen mode

如果您想掌握 Timsort 的工作原理并对其有所了解,我强烈建议您尝试自己实现它!

本文基于 Tim Peters 对 Timsort 的原始介绍,可在此处找到。


👋 你喜欢这篇文章吗?订阅我的邮件列表,这样每当我发布新内容时(大约每3-6周一次)你都会收到邮件通知✨😁 https://pages.convertkit.com/2ffbe6834c/8444e0640e

文章来源:https://dev.to/brandonskerritt/timsort-the-fastest-sorting-algorithm-you-ve-never-heard-of-2ake
PREV
如何在最难的远程工作面试中脱颖而出(Toptal 面试)Toptal 是什么?我被说服了。那么,我该如何加入?Toptal 面试攻略
NEXT
如何安全地存储 API 密钥