Tim Peters关于Timsort排序算法的说明


This describes an adaptive, stable, natural mergesort, modestly called
timsort (hey, I earned it ). It has supernatural performance on many
kinds of partially ordered arrays (less than lg(N!) comparisons needed, and
as few as N-1), yet as fast as Python's previous highly tuned samplesort
hybrid on random arrays.

In a nutshell, the main routine marches over the array once, left to right,
alternately identifying the next run, then merging it into the previous
runs "intelligently". Everything else is complication for speed, and some
hard-won measure of memory efficiency.



Comparison with Python's Samplesort Hybrid(与Python的Samplesort混合算法的对比)

  • timsort can require a temp array containing as many as N//2 pointers,
    which means as many as 2*N extra bytes on 32-bit boxes. It can be
    expected to require a temp array this large when sorting random data; on
    data with significant structure, it may get away without using any extra
    heap memory. This appears to be the strongest argument against it, but
    compared to the size of an object, 2 temp bytes worst-case (also expected-
    case for random data) doesn't scare me much.

  • timsort可能需要一个临时数组,其中包含多达N//2个指针,这意味着在32位系统上可能需要多达2*N个额外字节。在对随机数据进行排序时,可能需要这么大的临时数组;对于具有显著结构的数据,它可能在不使用任何额外堆内存的情况下完成。这似乎是反对它的最有力的论点,但与对象的大小相比,最坏情况下2个临时字节(对于随机数据也是预期情况)并不让我感到担心。

    It turns out that Perl is moving to a stable mergesort, and the code for
    that appears always to require a temp array with room for at least N
    pointers. (Note that I wouldn't want to do that even if space weren't an
    issue; I believe its efforts at memory frugality also save timsort
    significant pointer-copying costs, and allow it to have a smaller working

  • Across about four hours of generating random arrays, and sorting them
    under both methods, samplesort required about 1.5% more comparisons
    (the program is at the end of this file).

  • 在大约四个小时的生成随机数组并使用两种方法对其进行排序的过程中,samplesort需要比较多约1.5%(程序在本文件的末尾)。

  • In real life, this may be faster or slower on random arrays than
    samplesort was, depending on platform quirks. Since it does fewer
    comparisons on average, it can be expected to do better the more
    expensive a comparison function is. OTOH, it does more data movement
    (pointer copying) than samplesort, and that may negate its small
    comparison advantage (depending on platform quirks) unless comparison
    is very expensive.

  • 在实际应用中,对于随机数组,它可能比samplesort更快或更慢,这取决于平台的特性。由于平均比较次数较少,可以预期在比较函数更昂贵的情况下,它的性能会更好。另一方面,它比samplesort进行更多的数据移动(指针复制),这可能会抵消其较小的比较优势(取决于平台的特性),除非比较非常昂贵。

  • On arrays with many kinds of pre-existing order, this blows samplesort out
    of the water. It's significantly faster than samplesort even on some
    cases samplesort was special-casing the snot out of. I believe that lists
    very often do have exploitable partial order in real life, and this is the
    strongest argument in favor of timsort (indeed, samplesort's special cases
    for extreme partial order are appreciated by real users, and timsort goes
    much deeper than those, in particular naturally covering every case where
    someone has suggested "and it would be cool if list.sort() had a special
    case for this too ... and for that ...").

  • 在具有许多种预先存在的顺序的数组上,timsort远远超过了samplesort。即使在一些samplesort特殊处理的情况下,timsort也明显快于samplesort。我相信在现实生活中,列表往往具有可利用的部分顺序,这是支持timsort的最有力的论点(实际上,samplesort对极端部分顺序的特殊情况是用户所欣赏的,而timsort比这些情况更深入,特别是自然地涵盖了每种情况,其中有人建议“如果list.sort()也有一个特殊情况来处理这个...还有那个...”)。

  • Here are exact comparison counts across all the tests in sortperf.py,
    when run with arguments "15 20 1".

  • 下面是在sortperf.py的所有测试中的精确比较计数,当使用参数"15 20 1"运行时。

    First the trivial cases, trivial for samplesort because it special-cased
    them, and trivial for timsort because it naturally works on runs. Within
    an "n" block, the first line gives the # of compares done by samplesort,
    the second line by timsort, and the third line is the percentage by
    which the samplesort count exceeds the timsort count:


      n   \sort   /sort   =sort
-------  ------  ------  ------
  32768   32768   32767   32767  samplesort
          32767   32767   32767  timsort
          0.00%   0.00%   0.00%  (samplesort - timsort) / timsort

  65536   65536   65535   65535
          65535   65535   65535
          0.00%   0.00%   0.00%

 131072  131072  131071  131071
         131071  131071  131071
          0.00%   0.00%   0.00%

 262144  262144  262143  262143
         262143  262143  262143
          0.00%   0.00%   0.00%

 524288  524288  524287  524287
         524287  524287  524287
          0.00%   0.00%   0.00%

1048576 1048576 1048575 1048575
        1048575 1048575 1048575
          0.00%   0.00%   0.00%

The algorithms are effectively identical in these cases, except that
timsort does one less compare in \sort.

Now for the more interesting cases. lg(n!) is the information-theoretic
limit for the best any comparison-based sorting algorithm can do on
average (across all permutations). When a method gets significantly
below that, it's either astronomically lucky, or is finding exploitable
structure in the data.


      n   lg(n!)    *sort     3sort    +sort    ~sort     !sort
-------  -------   ------  --------  -------  -------  --------
  32768   444255   453084    453604    32908   130484    469132  samplesort
                   449235     33019    33016   188720     65534  timsort
                    0.86%  1273.77%   -0.33%  -30.86%   615.86%  %ch from timsort

  65536   954037   973111    970464    65686   260019   1004597
                   963924     65767    65802   377634    131070
                    0.95%  1375.61%   -0.18%  -31.15%   666.46%

 131072  2039137  2100019   2102708   131232   555035   2161268
                  2058863    131422   131363   755476    262142
                    2.00%  1499.97%   -0.10%  -26.53%   724.46%

 262144  4340409  4461471   4442796   262314  1107826   4584316
                  4380148    262446   262466  1511174    524286
                    1.86%  1592.84%   -0.06%  -26.69%   774.39%

 524288  9205096  9448146   9368681   524468  2218562   9691553
                  9285454    524576   524626  3022584   1048574
                    1.75%  1685.95%   -0.03%  -26.60%   824.26%

1048576 19458756 19950541  20307955  1048766  4430616  20433371
                 19621100   1048854  1048933  6045418   2097150
                    1.68%  1836.20%   -0.02%  -26.71%   874.34%

Discussion of cases:

sort: There's no structure in random data to exploit, so the theoretical
limit is lg(n!). Both methods get close to that, and timsort is hugging
it (indeed, in a
marginal* sense, it's a spectacular improvement --
there's only about 1% left before hitting the wall, and timsort knows
darned well it's doing compares that won't pay on random data -- but so
does the samplesort hybrid). For contrast, Hoare's original random-pivot
quicksort does about 39% more compares than the limit, and the median-of-3
variant about 19% more.

3sort and !sort: No contest; there's structure in this data, but not of
the specific kinds samplesort special-cases. Note that structure in !sort
wasn't put there on purpose -- it was crafted as a worst case for a
previous quicksort implementation. That timsort nails it came as a
surprise to me (although it's obvious in retrospect).

+sort: samplesort special-cases this data, and does a few less compares
than timsort. However, timsort runs this case significantly faster on all
boxes we have timings for, because timsort is in the business of merging
runs efficiently, while samplesort does much more data movement in this
(for it) special case.

~sort: samplesort's special cases for large masses of equal elements are
extremely effective on ~sort's specific data pattern, and timsort just
isn't going to get close to that, despite that it's clearly getting a
great deal of benefit out of the duplicates (the # of compares is much less
than lg(n!)). ~sort has a perfectly uniform distribution of just 4
distinct values, and as the distribution gets more skewed, samplesort's
equal-element gimmicks become less effective, while timsort's adaptive
strategies find more to exploit; in a database supplied by Kevin Altis, a
sort on its highly skewed "on which stock exchange does this company's
stock trade?" field ran over twice as fast under timsort.

However, despite that timsort does many more comparisons on ~sort, and
that on several platforms ~sort runs highly significantly slower under
timsort, on other platforms ~sort runs highly significantly faster under
timsort. No other kind of data has shown this wild x-platform behavior,
and we don't have an explanation for it. The only thing I can think of
that could transform what "should be" highly significant slowdowns into
highly significant speedups on some boxes are catastrophic cache effects
in samplesort.

But timsort "should be" slower than samplesort on ~sort, so it's hard
to count that it isn't on some boxes as a strike against it .





~sort:samplesort对大量相等元素的特殊情况非常有效,适用于~sort的特定数据模式,而timsort则无法接近这一点,尽管它显然从重复元素中获得了很大的好处(比较次数远远小于lg(n!))。~sort具有完全均匀分布的4个不同值,随着分布的偏斜程度增加,samplesort的相等元素技巧变得不那么有效,而timsort的自适应策略则找到了更多可利用的内容;在Kevin Altis提供的数据库中,对高度倾斜的“这家公司的股票在哪个证券交易所交易?”字段进行排序时,timsort的运行速度是samplesort的两倍以上。



A detailed description of timsort follows.

下面是 timsort 的详细描述。


count_run() returns the # of elements in the next run. A run is either
"ascending", which means non-decreasing:

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

or "descending", which means strictly decreasing:

a0 > a1 > a2 > ...

Note that a run is always at least 2 long, unless we start at the array's
last element.

The definition of descending is strict, because the main routine reverses
a descending run in-place, transforming a descending run into an ascending
run. Reversal is done via the obvious fast "swap elements starting at each
end, and converge at the middle" method, and that can violate stability if
the slice contains any equal elements. Using a strict definition of
descending ensures that a descending run contains distinct elements.

If an array is random, it's very unlikely we'll see long runs. If a natural
run contains less than minrun elements (see next secion), the main loop
artificially boosts it to minrun elements, via a stable binary insertion sort
applied to the right number of array elements following the short natural
run. In a random array, all runs are likely to be minrun long as a
result. This has two primary good effects:

  1. Random data strongly tends then toward perfectly balanced (both runs have
    the same length) merges, which is the most efficient way to proceed when
    data is random.

  2. Because runs are never very short, the rest of the code doesn't make
    heroic efforts to shave a few cycles off per-merge overheads. For
    example, reasonable use of function calls is made, rather than trying to
    inline everything. Since there are no more than N/minrun runs to begin
    with, a few "extra" function calls per merge is barely measurable.



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

a0 > a1 > a2 > ...






Computing minrun

If N < 64, minrun is N. IOW, binary insertion sort is used for the whole
array then; it's hard to beat that given the overheads of trying something

When N is a power of 2, testing on random data showed that minrun values of
16, 32, 64 and 128 worked about equally well. At 256 the data-movement cost
in binary insertion sort clearly hurt, and at 8 the increase in the number
of function calls clearly hurt. Picking some power of 2 is important
here, so that the merges end up perfectly balanced (see next section). We
pick 32 as a good value in the sweet range; picking a value at the low end
allows the adaptive gimmicks more opportunity to exploit shorter natural

Because sortperf.py only tries powers of 2, it took a long time to notice
that 32 isn't a good choice for the general case! Consider N=2112:

divmod(2112, 32)
(66, 0)

If the data is randomly ordered, we're very likely to end up with 66 runs
each of length 32. The first 64 of these trigger a sequence of perfectly
balanced merges (see next section), leaving runs of lengths 2048 and 64 to
merge at the end. The adaptive gimmicks can do that with fewer than 2048+64
compares, but it's still more compares than necessary, and-- mergesort's
bugaboo relative to samplesort --a lot more data movement (O(N) copies just
to get 64 elements into place).

If we take minrun=33 in this case, then we're very likely to end up with 64
runs each of length 33, and then all merges are perfectly balanced. Better!

What we want to avoid is picking minrun such that in

q, r = divmod(N, minrun)

q is a power of 2 and r>0 (then the last merge only gets r elements into
place, and r<minrun is small compared to N), or r=0 and q a little larger
than a power of 2 (then we've got a case similar to "2112", again leaving
too little work for the last merge to do).

Instead we pick a minrun in range(32, 65) such that N/minrun is exactly a
power of 2, or if that isn't possible, is close to, but strictly less than,
a power of 2. This is easier to do than it may sound: take the first 6
bits of N, and add 1 if any of the remaining bits are set. In fact, that
rule covers every case in this section, including small N and exact powers
of 2; merge_compute_minrun() is a deceptively simple function.

如果N < 64,minrun的值就是N。换句话说,整个数组都使用二分插入排序;在考虑到其他更复杂方法的开销后,很难超越这种方法的效率。



divmod(2112, 32)
(66, 0)




q, r = divmod(N, minrun)

相反,我们选择一个在范围(32, 65)内的minrun,使得N/minrun恰好是2的幂次方,或者如果不可能,就接近但严格小于2的幂次方。这比听起来要简单得多:取N的前6位,并在剩余位中有任何位设置时加1。实际上,这个规则涵盖了本节中的每种情况,包括小的N和精确的2的幂次方;merge_compute_minrun()是一个看似简单的函数。

The Merge Pattern

In order to exploit regularities in the data, we're merging on natural
run lengths, and they can become wildly unbalanced. That's a Good Thing
for this sort! It means we have to find a way to manage an assortment of
potentially very different run lengths, though.

Stability constrains permissible merging patterns. For example, if we have
3 consecutive runs of lengths

A:10000  B:20000  C:10000

we dare not merge A with C first, because if A, B and C happen to contain
a common element, it would get out of order wrt its occurence(s) in B. The
merging must be done as (A+B)+C or A+(B+C) instead.

So merging is always done on two consecutive runs at a time, and in-place,
although this may require some temp memory (more on that later).

When a run is identified, its base address and length are pushed on a stack
in the MergeState struct. merge_collapse() is then called to see whether it
should merge it with preceding run(s). We would like to delay merging as
long as possible in order to exploit patterns that may come up later, but we
like even more to do merging as soon as possible to exploit that the run just
found is still high in the memory hierarchy. We also can't delay merging
"too long" because it consumes memory to remember the runs that are still
unmerged, and the stack has a fixed size.



A:10000 B:20000 C:10000



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:

  1. A > B+C
  2. B > C

Note that, by induction, #2 implies the lengths of pending runs form a
decreasing sequence. #1 implies that, reading the lengths right to left,
the pending-run lengths grow at least as fast as the Fibonacci numbers.
Therefore the stack can never grow larger than about log_base_phi(N) entries,
where phi = (1+sqrt(5))/2 ~= 1.618. Thus a small # of stack slots suffice
for very large arrays.

If A <= B+C, the smaller of A and C is merged with B (ties favor C, for the
freshness-in-cache reason), and the new run replaces the A,B or B,C entries;
e.g., if the last 3 entries are

A:30  B:20  C:10

then B is merged with C, leaving

A:30  BC:30

on the stack. Or if they were

A:500  B:400:  C:1000

then A is merged with B, leaving

AB:900  C:1000

on the stack.


A > B+C
B > C
注意,通过归纳,不变量2意味着待处理run的长度形成一个递减序列。不变量1意味着,从右到左读取长度时,待处理run的长度至少以斐波那契数列的增长速度增长。因此,堆栈的大小永远不会超过大约log_base_phi(N)个条目,其中phi = (1+sqrt(5))/2约等于1.618。因此,对于非常大的数组,只需要很少的堆栈空间。

如果A <= B+C,则将A和C中较小的那个与B合并(如果相等,则优先选择C,原因是缓存的新鲜度),并用新的run替换A、B或B、C条目;例如,如果最后3个条目是:

A:30 B:20 C:10

A:30 BC:30

A:500 B:400 C:1000

AB:900 C:1000

In both examples, the stack configuration after the merge still violates
invariant #2, and merge_collapse() goes on to continue merging runs until
both invariants are satisfied. As an extreme case, suppose we didn't do the
minrun gimmick, and natural runs were of lengths 128, 64, 32, 16, 8, 4, 2,
and 2. Nothing would get merged until the final 2 was seen, and that would
trigger 7 perfectly balanced merges.

The thrust of these rules when they trigger merging is to balance the run
lengths as closely as possible, while keeping a low bound on the number of
runs we have to remember. This is maximally effective for random data,
where all runs are likely to be of (artificially forced) length minrun, and
then we get a sequence of perfectly balanced merges (with, perhaps, some
oddballs at the end).

OTOH, one reason this sort is so good for partly ordered data has to do
with wildly unbalanced run lengths.




Merge Memory

Merging adjacent runs of lengths A and B in-place is very difficult.
Theoretical constructions are known that can do it, but they're too difficult
and slow for practical use. But if we have temp memory equal to min(A, B),
it's easy.

If A is smaller (function merge_lo), copy A to a temp array, leave B alone,
and then we can do the obvious merge algorithm left to right, from the temp
area and B, starting the stores into where A used to live. There's always a
free area in the original area comprising a number of elements equal to the
number not yet merged from the temp array (trivially true at the start;
proceed by induction). The only tricky bit is that if a comparison raises an
exception, we have to remember to copy the remaining elements back in from
the temp area, lest the array end up with duplicate entries from B. But
that's exactly the same thing we need to do if we reach the end of B first,
so the exit code is pleasantly common to both the normal and error cases.

If B is smaller (function merge_hi, which is merge_lo's "mirror image"),
much the same, except that we need to merge right to left, copying B into a
temp array and starting the stores at the right end of where B used to live.

A refinement: When we're about to merge adjacent runs A and B, we first do
a form of binary search (more on that later) to see where B[0] should end up
in A. Elements in A preceding that point are already in their final
positions, effectively shrinking the size of A. Likewise we also search to
see where A[-1] should end up in B, and elements of B after that point can
also be ignored. This cuts the amount of temp memory needed by the same

These preliminary searches may not pay off, and can be expected not to
repay their cost if the data is random. But they can win huge in all of
time, copying, and memory savings when they do pay, so this is one of the
"per-merge overheads" mentioned above that we're happy to endure because
there is at most one very short run. It's generally true in this algorithm
that we're willing to gamble a little to win a lot, even though the net
expectation is negative for random data.

在原地合并长度为A和B的相邻run非常困难。虽然已经有理论上的构造可以实现这一点,但它们对于实际使用来说过于复杂和缓慢。但是,如果我们有一个大小等于min(A, B)的临时内存,就很容易实现。



一个改进:当我们要合并相邻的run A和B时,我们首先进行一种二分搜索(稍后详细介绍),以确定B[0]应该在A中的哪个位置。在该点之前的A元素已经处于最终位置,有效地缩小了A的大小。同样,我们还搜索以确定A[-1]应该在B中的哪个位置,之后的B元素也可以忽略。这样可以减少所需的临时内存量。


Merge Algorithms

merge_lo() and merge_hi() are where the bulk of the time is spent. merge_lo
deals with runs where A <= B, and merge_hi where A > B. They don't know
whether the data is clustered or uniform, but a lovely thing about merging
is that many kinds of clustering "reveal themselves" by how many times in a
row the winning merge element comes from the same run. We'll only discuss
merge_lo here; merge_hi is exactly analogous.

Merging begins in the usual, obvious way, comparing the first element of A
to the first of B, and moving B[0] to the merge area if it's less than A[0],
else moving A[0] to the merge area. Call that the "one pair at a time"
mode. The only twist here is keeping track of how many times in a row "the
winner" comes from the same run.

If that count reaches MIN_GALLOP, we switch to "galloping mode". Here
we search B for where A[0] belongs, and move over all the B's before
that point in one chunk to the merge area, then move A[0] to the merge
area. Then we search A for where B[0] belongs, and similarly move a
slice of A in one chunk. Then back to searching B for where A[0] belongs,
etc. We stay in galloping mode until both searches find slices to copy
less than MIN_GALLOP elements long, at which point we go back to one-pair-
at-a-time mode.

merge_lo()和merge_hi()是耗时最多的部分。merge_lo处理A <= B的run,而merge_hi处理A > B的run。它们并不知道数据是聚集还是均匀分布,但合并的一个美妙之处在于,许多类型的聚集通过连续多次获胜的合并元素来"展现出来"。在这里,我们只讨论merge_lo;merge_hi完全类似。


如果该计数达到MIN_GALLOP,我们切换到"奔跑模式"。在这种模式下,我们在B中搜索 A[0]应该插入的位置,并将该位置之前的所有B元素一次性移动到合并区域,然后将A[0]移动到合并区域。然后我们在A中搜索B[0]应该插入的位置,并类似地一次性移动A的一部分。然后再次在B中搜索A[0]应该插入的位置,以此类推。我们在奔跑模式下保持,直到两个搜索找到要复制的片段长度小于MIN_GALLOP个元素,此时我们回到一对一次模式。


Still without loss of generality, assume A is the shorter run. In galloping
mode, we first look for A[0] in B. We do this via "galloping", comparing
A[0] in turn to B[0], B[1], B[3], B[7], ..., B[2j - 1], ..., until finding
the k such that B[2
(k-1) - 1] < A[0] <= B[2**k - 1]. This takes at most
roughly lg(B) comparisons, and, unlike a straight binary search, favors
finding the right spot early in B (more on that later).

After finding such a k, the region of uncertainty is reduced to 2*(k-1) - 1
consecutive elements, and a straight binary search requires exactly k-1
additional comparisons to nail it. Then we copy all the B's up to that
point in one chunk, and then copy A[0]. Note that no matter where A[0]
belongs in B, the combination of galloping + binary search finds it in no
more than about 2
lg(B) comparisons.

If we did a straight binary search, we could find it in no more than
ceiling(lg(B+1)) comparisons -- but straight binary search takes that many
comparisons no matter where A[0] belongs. Straight binary search thus loses
to galloping unless the run is quite long, and we simply can't guess
whether it is in advance.

If data is random and runs have the same length, A[0] belongs at B[0] half
the time, at B[1] a quarter of the time, and so on: a consecutive winning
sub-run in B of length k occurs with probability 1/2**(k+1). So long
winning sub-runs are extremely unlikely in random data, and guessing that a
winning sub-run is going to be long is a dangerous game.

OTOH, if data is lopsided or lumpy or contains many duplicates, long
stretches of winning sub-runs are very likely, and cutting the number of
comparisons needed to find one from O(B) to O(log B) is a huge win.

仍然不失一般性,假设A是较短的run。在奔跑模式下,我们首先在B中寻找A[0]。我们通过"奔跑"的方式来进行,依次将A[0]与B[0]、B[1]、B[3]、B[7]、...、B[2j - 1]、...进行比较,直到找到满足条件的k,使得B[2(k-1) - 1] < A[0] <= B[2**k - 1]。这最多需要大约lg(B)次比较,并且与直接的二分搜索不同,它更倾向于在B中早期找到正确的位置(稍后详细介绍)。

找到这样的k之后,不确定性的区域缩小为2*(k-1) - 1个连续元素,直接的二分搜索需要额外的k-1次比较来确定位置。然后我们一次性复制到该位置之前的所有B元素,然后复制A[0]。请注意,无论A[0]在B中的位置如何,奔跑+二分搜索的组合最多需要大约2lg(B)次比较。



另一方面,如果数据不平衡、不均匀或包含许多重复项,那么长时间的连续获胜子run非常可能出现,将查找一个获胜子run所需的比较次数从O(B)降低到O(log B)将带来巨大的收益。

Galloping compromises by getting out fast if there isn't a long winning
sub-run, yet finding such very efficiently when they exist.

I first learned about the galloping strategy in a related context; see:

"Adaptive Set Intersections, Unions, and Differences" (2000)
Erik D. Demaine, Alejandro López-Ortiz, J. Ian Munro

and its followup(s). An earlier paper called the same strategy
"exponential search":

"Optimistic Sorting and Information Theoretic Complexity"
Peter McIlroy
SODA (Fourth Annual ACM-SIAM Symposium on Discrete Algorithms), pp
467-474, Austin, Texas, 25-27 January 1993.

and it probably dates back to an earlier paper by Bentley and Yao. The
McIlory paper in particular has good analysis of a mergesort that's
probably strongly related to this one in its galloping strategy.



"Adaptive Set Intersections, Unions, and Differences" (2000) - Erik D. Demaine, Alejandro López-Ortiz, J. Ian Munro
"Optimistic Sorting and Information Theoretic Complexity" - Peter McIlroy, SODA (Fourth Annual ACM-SIAM Symposium on Discrete Algorithms), pp 467-474, Austin, Texas, 25-27 January 1993



Galloping with a Broken Leg

So why don't we always gallop? Because it can lose, on two counts:

  1. While we're willing to endure small per-run overheads, per-comparison
    overheads are a different story. Calling Yet Another Function per
    comparison is expensive, and gallop_left() and gallop_right() are
    too long-winded for sane inlining.

  2. Ignoring function-call overhead, galloping can-- alas --require more
    comparisons than linear one-at-time search, depending on the data.

2 requires details. If A[0] belongs before B[0], galloping requires 1

compare to determine that, same as linear search, except it costs more
to call the gallop function. If A[0] belongs right before B[1], galloping
requires 2 compares, again same as linear search. On the third compare,
galloping checks A[0] against B[3], and if it's <=, requires one more
compare to determine whether A[0] belongs at B[2] or B[3]. That's a total
of 4 compares, but if A[0] does belong at B[2], linear search would have
discovered that in only 3 compares, and that's a huge loss! Really. It's
an increase of 33% in the number of compares needed, and comparisons are
expensive in Python.






index in B where    # compares linear  # gallop  # binary  gallop
A[0] belongs        search needs       compares  compares  total
----------------    -----------------  --------  --------  ------
               0                    1         1         0       1

               1                    2         2         0       2

               2                    3         3         1       4
               3                    4         3         1       4

               4                    5         4         2       6
               5                    6         4         2       6
               6                    7         4         2       6
               7                    8         4         2       6

               8                    9         5         3       8
               9                   10         5         3       8
              10                   11         5         3       8
              11                   12         5         3       8

In general, if A[0] belongs at B[i], linear search requires i+1 comparisons
to determine that, and galloping a total of 2*floor(lg(i))+2 comparisons.
The advantage of galloping is unbounded as i grows, but it doesn't win at
all until i=6. Before then, it loses twice (at i=2 and i=4), and ties
at the other values. At and after i=6, galloping always wins.

We can't guess in advance when it's going to win, though, so we do one pair
at a time until the evidence seems strong that galloping may pay. MIN_GALLOP
is 8 as I type this, and that's pretty strong evidence. However, if the data
is random, it simply will trigger galloping mode purely by luck every now
and again, and it's quite likely to hit one of the losing cases next. 8
favors protecting against a slowdown on random data at the expense of giving
up small wins on lightly clustered data, and tiny marginal wins on highly
clustered data (they win huge anyway, and if you're getting a factor of
10 speedup, another percent just isn't worth fighting for).



Galloping Complication

The description above was for merge_lo. merge_hi has to merge "from the
other end", and really needs to gallop starting at the last element in a run
instead of the first. Galloping from the first still works, but does more
comparisons than it should (this is significant -- I timed it both ways).
For this reason, the gallop_left() and gallop_right() functions have a
"hint" argument, which is the index at which galloping should begin. So
galloping can actually start at any index, and proceed at offsets of 1, 3,
7, 15, ... or -1, -3, -7, -15, ... from the starting index.

In the code as I type it's always called with either 0 or n-1 (where n is
the # of elements in a run). It's tempting to try to do something fancier,
melding galloping with some form of interpolation search; for example, if
we're merging a run of length 1 with a run of length 10000, index 5000 is
probably a better guess at the final result than either 0 or 9999. But
it's unclear how to generalize that intuition usefully, and merging of
wildly unbalanced runs already enjoys excellent performance.



Comparing Average # of Compares on Random Arrays

Here list.sort() is samplesort, and list.msort() this sort:

import random
from time import clock as now

def fill(n):
    from random import random
    return [random() for i in xrange(n)]

def mycmp(x, y):
    global ncmp
    ncmp += 1
    return cmp(x, y)

def timeit(values, method):
    global ncmp
    X = values[:]
    bound = getattr(X, method)
    ncmp = 0
    t1 = now()
    t2 = now()
    return t2-t1, ncmp

format = "%5s  %9.2f  %11d"
f2     = "%5s  %9.2f  %11.2f"

def drive():
    count = sst = sscmp = mst = mscmp = nelts = 0
    while True:
        n = random.randrange(100000)
        nelts += n
        x = fill(n)

        t, c = timeit(x, 'sort')
        sst += t
        sscmp += c

        t, c = timeit(x, 'msort')
        mst += t
        mscmp += c

        count += 1
        if count % 10:

        print "count", count, "nelts", nelts
        print format % ("sort",  sst, sscmp)
        print format % ("msort", mst, mscmp)
        print f2     % ("", (sst-mst)*1e2/mst, (sscmp-mscmp)*1e2/mscmp)


I ran this on Windows and kept using the computer lightly while it was
running. time.clock() is wall-clock time on Windows, with better than
microsecond resolution. samplesort started with a 1.52% #-of-comparisons
disadvantage, fell quickly to 1.48%, and then fluctuated within that small
range. Here's the last chunk of output before I killed the job:

count 2630 nelts 130906543
sort 6110.80 1937887573
msort 6002.78 1909389381
1.80 1.49

We've done nearly 2 billion comparisons apiece at Python speed there, and
that's enough .

For random arrays of size 2 (yes, there are only 2 interesing ones),
samplesort has a 50%(!) comparison disadvantage. This is a consequence of
samplesort special-casing at most one ascending run at the start, then
falling back to the general case if it doesn't find an ascending run
immediately. The consequence is that it ends up using two compares to sort
[2, 1]. Gratifyingly, timsort doesn't do any special-casing, so had to be
taught how to deal with mixtures of ascending and descending runs
efficiently in all cases.


您的电子邮箱地址不会被公开。 必填项已用*标注