自然的价格或如何超越QuickSort

对于算法学家来说,排序是同样的“永恒”主题,就像对诗人的爱一样。在这个领域似乎很难说一个新词,但是继续吧-他们继续提出新的排序算法(TimSort ...)。但是,每个体面的学生都知道一些基本事实。例如,众所周知,通用排序算法不能快于O(n * log(n))。这样的性能指标具有著名的QuickSort(由Hoare于1960年发明)以及合并排序(冯·诺伊曼)和堆排序。至于基本算法(“气泡”,“插入”,“选择”),它们的指标要差得多-O(n ^ 2)。但是QuickSort始终是无可争议的冠军吗?



实际上,除了性能指标外,还有其他特征通常也同样重要。其中之一就是自然。这是什么?如果在对数组进行几乎排序时速度更快,则排序称为自然排序。哪些数组可以视为“几乎已排序”?这是两个具有相同元素的数组:



{1,2,9,3,4,5,7,6,8,10}{9,1,6,3,10,5,4,2, 8.7}



即使是肉眼,您也可以看到第一个数组更有序(只有9和7是“不适当的”)。而在第二个数组中,数字是随机混合的。秩序的程度是什么?答案是已知的-倒数。如果A [j] <A [i],则 j> i的一对元素A [i]和A [j]构成逆。 (在此注释中,排序的目的始终是升序。)



现在我们可以给出一个精确的定义:如果原始数组中的反转次数减少时排序速度降低,则排序称为自然排序。

在排序世界中,自然性是一种相当“稀有的果实”-S,QuickSort和Shell排序都不具有此属性。但是有人可能会说,有一种算法是绝对自然的-它是插入排序。尽管此算法对于每个有教养的人都是众所周知的,但让我简要地重复一下它的本质。



要排序的数组从头到尾扫描一次。一旦发现第i个元素和第(i-1)个元素形成一个反转,第i个元素就被“掷回”(这是通过将数组开头的所需段向右移动一个位置来实现的)。从该描述中可以清楚地看到,如果数组中的反转很少,则该算法将很快“飞过”数组。如果根本没有反演,则该算法将在O(n)时间内完成。但是在这种情况下,QuickSort会很长且乏味,无法选择一个分隔元素,将已经排序的数组划分为多个段,等等。但是,由于存在大量反转,这种情况当然会相反:插入排序的性能将下降为O(n ^ 2),而QuickSort将成为冠军-O(n * log(n))。



从我的角度来看,这种情况提出了一个自然的问题:有多少个反演胜于自然,并且插入排序的工作速度比QuickSort快?



为了回答这个问题,我进行了一系列计算实验。其实质如下。取取范围为3000到30,000个元素的整数数组,将一定数量的反转引入其中,然后通过插入和快速排序对数组进行排序。测量了排序时间(以系统滴答为单位)。为了求平均,将每种种类重复10次。结果如下:



图片



图片显示了排序时间对引入的反转次数的依赖性。覆盆子系列当然是QuickSort,蓝色系列是插入排序。对于3万个元素的数组大小,最多约40万个反转是“自然胜利”。对于排序较少的数组,QuickSort已经更加有用。



下一张图片显示了关键的反演次数对数组大小的经验依赖性。



图片



不难看出,对于大小为n的数组,临界的反转数约为10 * n。随着更多的反转,QuickSort变得很有用。应当注意,长度为n的数组的最大反转数为n *(n-1)/ 2。值10 * n是其中很小的一部分。这不足为奇。



据说,这些实验的结果取决于很多因素(编程语言,要排序的数据类型等)。老实说,我懒于更详细地探讨这些问题。我的实验是在VBA环境中的Microsoft Excel中完成的:



测试源代码
Private Declare Function GetTickCount Lib "kernel32" () As Long

':::   

Sub JSort(A() As Long)
    n& = UBound(A, 1)
    For i& = 2 To n
        If A(i&) < A(i& - 1) Then
           j& = i& - 1
           x& = A(i&)
           Do While (A(j&) > x&)
              A(j& + 1) = A(j&)
              j& = j& - 1
              If j& = 0 Then Exit Do
           Loop
           A(j& + 1) = x&
        End If
    Next i&
End Sub

':::  

Sub QSort(A() As Long, Optional b As Long = 1, Optional e As Long = 0)
    If (e - b) <= 1 Then Exit Sub
    i& = b
    j& = e
    w& = A((i& + j&) / 2)
    Do While (i& < j&)
      Do While (A(i&) < w&)
         i& = i& + 1
      Loop
      Do While (A(j&) > w&)
         j& = j& - 1
      Loop
      If i& < j& Then
         tmp& = A(i&)
         A(i&) = A(j&)
         A(j&) = tmp&
         i& = i& + 1
         j& = j& - 1
      End If
    Loop
    If j& > b Then QSort A, b, j&
    If i& < e Then QSort A, i&, e
End Sub

':::    ( )

Sub InsInv(A() As Long, k As Long)
    n& = UBound(A, 1)
    For i& = 1 To k
        Do
           k1& = n& * Rnd
           k2& = n& * Rnd
           If (k1& <> k2&) And (k1& >= 1) And (k2& >= 1) Then Exit Do
        Loop
        tmp& = A(k1&)
        A(k1&) = A(k2&)
        A(k2&) = tmp&
    Next i&
End Sub

':::     

Function NumInv(A() As Long) As Long
    n& = UBound(A, 1)
    For i& = 1 To n& - 1
        For j& = i& + 1 To n&
            If A(j&) < A(i&) Then NumInv = NumInv + 1
        Next j&
    Next i&
End Function

':::  

Sub Gtest_1()
Dim Eta() As Long
Dim Arr() As Long
    Size& = CLng(InputBox("Sz="))
    ReDim Eta(1 To Size&) As Long
    ReDim Arr(1 To Size&) As Long
    Randomize
    For i& = 1 To Size&
        Eta(i&) = i&
    Next i&
    q# = Size& * (Size& - 1) / 2
    For iii% = 1 To 10
        InsInv Eta, CLng(iii%)
        ni# = CDbl(NumInv(Eta))
        Cells(iii%, 1).Value = ni#  
        DoEvents
        S# = 0
        For l% = 1 To 10
            For i& = 1 To Size&
                Arr(i&) = Eta(i&)
            Next i&
            TBeg& = GetTickCount
            JSort Arr
            TEnd& = GetTickCount
            S# = S# + TEnd& - TBeg&
        Next l%
        Cells(iii%, 2).Value = S#
        DoEvents
        S# = 0
        For l% = 1 To 10
            For i& = 1 To Size&
                Arr(i&) = Eta(i&)
            Next i&
            TBeg& = GetTickCount
            QSort Arr, 1, Size&
            TEnd& = GetTickCount
            S# = S# + TEnd& - TBeg&
            If Not check(Arr) Then Exit Sub
        Next l%
        Cells(iii%, 3).Value = S#
        DoEvents
    Next iii%
    MsgBox "OK"
End Sub




感谢您的关注!



PS

并感谢所有指出我的错误的人!(Timsort的拼写不正确-在第一版中有“ TeamSort”和QuickSort中缺少的“ i”)。是的!-(特别针对完美主义者)QuickSort可以“降低”到二次性能。但是在这篇文章中,我考虑的不是QuickSort最坏的情况,而是最佳的使用情况。



All Articles