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

举报
宇宙之一粟 发表于 2022/07/31 21:48:20 2022/07/31
【摘要】 Timsort - 你从未听说过的最快的排序算法Timsort:为现实世界构建的非常快速、O(nlogNn logNnlogN)、稳定的排序算法 —— 不是在学术界构建的。Timsort 是一种对现实世界数据有效的排序算法,而不是在学术实验室中创造的。Tim Peters在 2001 年为 Python 编程语言创建了 Timsort 。Timsort 首先分析它要排序的列表,然后根据对列...

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

Timsort:为现实世界构建的非常快速、O( n l o g N n logN )、稳定的排序算法 —— 不是在学术界构建的。

Timsort 是一种对现实世界数据有效的排序算法,而不是在学术实验室中创造的。Tim Peters在 2001 年为 Python 编程语言创建了 Timsort 。Timsort 首先分析它要排序的列表,然后根据对列表的分析选择一种方法。

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

Timsort 的大 O 符号是 O(n log n)。了解 Big O 表示法

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

彼得斯设计的 Timsort 使用已经排序的元素,这些元素存在于大多数现实世界的数据集中。它把这些已经排序的元素称为 “自然运行”。它在数据上进行迭代,将元素收集到运行中,同时将这些运行合并成一个。

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

插入排序是一种简单的排序,对小列表最有效。它对较大的列表相当慢,但对小列表却非常快。插入排序的原理如下:

  • 逐一查看要素

  • 通过在正确位置插入元素来构建排序列表

这是一个跟踪表,显示了插入排序如何对列表 [34, 10, 64, 51, 32, 21] 进行排序

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

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

更多运行原理

如果列表大于 64 个元素,算法将在列表中进行第一次传递,寻找严格意义上的增加或减少的部分。如果该部分是递减的,它将反转该部分。

因此,如果运行是递减的,它将看起来像这样(其中运行是黑体)。

如果没有递减,它看起来像这样:

minrun的大小是根据数组的大小来决定的。该算法选择它,使随机数组中的大多数运行都是,或成为minrun的长度。当运行的数量等于或略小于2的幂时,合并2个数组会更有效率。Timsort选择minrun是为了试图确保这种效率,确保minrun等于或小于2的幂。

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

如果运行的长度小于minrun,你就计算出远离minrun的那个运行的长度。使用这个新的数字,你在运行的前面抓取那么多的项目,并执行插入排序来创建一个新的运行。

因此,如果minrun是63,而run的长度是33,你就做63-33=30。然后你从run的末端抓取30个元素,所以这是run[33]中的30个项目,然后执行插入排序来创建一个新的run。

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

归并

image showing dragonball z charecters merging

Timsort现在执行归并排序,将运行合并在一起。然而,Timsort确保在合并排序的同时保持稳定性和合并平衡。

为了保持稳定性,我们不应该交换2个等值的数字。这不仅可以保持它们在列表中的原始位置,而且可以使算法更快。我们很快就会讨论合并平衡的问题。

当Timsort找到运行时,它将它们添加到一个堆栈中。一个简单的堆栈看起来像这样。

想象一下,一摞盘子。你不能从底部拿盘子,所以你必须从顶部拿。一堆东西也是如此。

Timsort试图在mergesort运行时平衡两种相互竞争的需求。一方面,我们希望尽可能地推迟合并,以便利用以后可能出现的模式。但我们更希望尽快进行合并,以利用刚刚发现的运行在内存层次中仍然很高的运行。我们也不能把合并的时间拖得 “太长”,因为记住那些仍未合并的运行会消耗内存,而且堆栈的大小是固定的。

为了确保我们有这样的妥协,Timsort跟踪了堆栈上最近的三个项目,并创建了两个必须对这些项目成立的定律。

  1. A > B + C

  2. B > C

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

用蒂姆·彼得斯本人的话来说:

What turned out to be a good compromise maintains two invariants on the stack entries, where A, B and C are the lengths of the three righmost not-yet merged slices

结果是一个很好的妥协,在堆栈条目上保持了两个不变量,其中A、B和C是最右边的三个尚未合并的片子的长度。

通常情况下,将不同长度的相邻运行合并到位是很难的。更加困难的是,我们必须要保持稳定性。为了解决这个问题,Timsort设置了临时内存。它将两个运行中较小的(同时调用运行A和B)放入该临时内存。

Galloping

当Timsort在合并A和B的时候,它注意到一个运行已经连续 "赢 "了很多次。如果结果是A运行的数字完全小于B运行的数字,那么A运行就会回到它原来的位置。合并这两个运行将涉及大量的工作,但却一无所获。

更多时候,数据会有一些预先存在的内部结构。Timsort假设,如果很多运行A的值比运行B的值低,那么很可能A的值会继续比B小。

然后Timsort将进入奔腾模式。Timsort不对A[0]和B[0]进行相互检查,而是对a[0]中b[0]的适当位置进行二进制搜索。这样,Timsort可以将A的一整段移到合适的位置。然后Timsort在B中搜索A[0]的适当位置,然后Timsort就可以一次将B的一整节移动到位。

让我们看看这个动作。Timsort检查B[0](它是5),并使用二进制搜索在A中寻找正确位置。

那么,B[0]属于A列表的后面。现在Timsort检查A[0](也就是1)在B的正确位置。现在我们知道,B属于A的末尾,A属于B的开头。

事实证明,如果B[0]的适当位置非常接近A的开头(或反之),这种操作是不值得的。因此,如果没有回报,驰骋模式很快就会退出。此外,Timsort注意到了这一点,并通过增加进入驰骋模式所需的只赢A或只赢B的连续次数,使以后更难进入驰骋模式。如果驰骋模式得到了回报,Timsort会让它更容易重新进入。

简而言之,Timsort做了2件令人难以置信的事。

  • 在具有预先存在的内部结构的阵列上有很好的表现

  • 能够保持一个稳定的排序

以前,为了实现稳定的排序,你必须将列表中的项目用整数压缩起来,并将其作为一个图元数组进行排序。

Code

如果你对代码不感兴趣,请随意跳过这部分。这一部分下面还有一些信息。

下面的源代码是基于我和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])

Timsort 实际上内置于 Python 中,因此此代码仅用作解释器。要使用 Timsort,只需编写:

list.sort()

或者

sorted(list)

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

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

✨😁

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。