一文了解 Python 中的 Timsort 排序算法
Posted 宇宙之一粟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文了解 Python 中的 Timsort 排序算法相关的知识,希望对你有一定的参考价值。
Timsort - 你从未听说过的最快的排序算法
Timsort:为现实世界构建的非常快速、O()、稳定的排序算法 —— 不是在学术界构建的。
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。
这一部分完成后,我们现在应该在一个列表中拥有一堆排序的运行。
归并
Timsort现在执行归并排序,将运行合并在一起。然而,Timsort确保在合并排序的同时保持稳定性和合并平衡。
为了保持稳定性,我们不应该交换2个等值的数字。这不仅可以保持它们在列表中的原始位置,而且可以使算法更快。我们很快就会讨论合并平衡的问题。
当Timsort找到运行时,它将它们添加到一个堆栈中。一个简单的堆栈看起来像这样。
想象一下,一摞盘子。你不能从底部拿盘子,所以你必须从顶部拿。一堆东西也是如此。
Timsort试图在mergesort运行时平衡两种相互竞争的需求。一方面,我们希望尽可能地推迟合并,以便利用以后可能出现的模式。但我们更希望尽快进行合并,以利用刚刚发现的运行在内存层次中仍然很高的运行。我们也不能把合并的时间拖得 "太长",因为记住那些仍未合并的运行会消耗内存,而且堆栈的大小是固定的。
为了确保我们有这样的妥协,Timsort跟踪了堆栈上最近的三个项目,并创建了两个必须对这些项目成立的定律。
- A > B + C
- 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 ith 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 的原始介绍,可在此处找到。
✨
以上是关于一文了解 Python 中的 Timsort 排序算法的主要内容,如果未能解决你的问题,请参考以下文章