Sibainu Relax Room

愛犬の柴犬とともに過ごす部屋

VBAでクイックソート その2

前回では500,000件ほどでダウンしましたので、別な方法でクイックソートを考えてみました。

EXCELでクイックソートコードを作成

エクセルのA列に、ソートしたいデータを作りC列にソートした結果を表示しています。1,045,075件をソートしてみましたところ40秒でできました。

私のPCのスペックは、CPU INTEL i7-3632QM memory 16GB 9年ほど前のものですので、現在のPCだともっと早いかもしれません。

EXCELのソート機能、メニューの「データ」「並べ替え」列A-セルの値-昇順で行ったところおよそ60秒かかりました。

私のソートが勝利しました。

EXCELのVBAコード

ソートを実行します。

copy

Private Sub QuickSortB()
    Dim SortArray           As Variant
    Dim EndRow              As Long

    With ActiveSheet
        EndRow = .Cells(.Rows.Count, 1).End(xlUp).Row
        SortArray = .Range(.Cells(1, 1), .Cells(EndRow, 1)).Value
    End With

    Call QuickB(SortArray, LBound(SortArray, 1), UBound(SortArray, 1))

    With ActiveSheet
        .Range(.Cells(LBound(SortArray, 1), 3),
               .Cells(UBound(SortArray, 1), 3)) = SortArray
    End With

End Sub 

QuickB プロシージャの作成

Pivot_Posi 関数で、ソート対象の配列の中の任意の値を Pivot として、そのインデックスを取得します。この関数を実行すると、Pivot のインデクスよりも若いインデクスの配列には Pivot よりも小さい値を保持しています。また、Pivot よりも大きいインデックスの配列は Pivot よりも大きい値を保持しています。Pivot のインデクスよりも若いインデクスの配列、Pivot よりも大きいインデックスの配列と分け再帰して実行します。

copy

Private Sub QuickB(ByRef SortArray As Variant, _
                   ByVal topPosi As Long, _
                   ByVal tailPosi As Long)
    Dim PivotPosi           As Long

    If tailPosi - topPosi <= 0 Then
        '再帰の終了条件です。
    Else
        PivotPosi = Pivot_Posi(SortArray, topPosi, tailPosi)
        Call QuickB(SortArray, topPosi, PivotPosi - 1)
        Call QuickB(SortArray, PivotPosi + 1, tailPosi)
    End If

End Sub 

Pivot_Posi関数

この関数は、SortArray:ソート対象の配列、ソート範囲を指定する先頭インデックス:topPosi、テイルインデックス:tailPosi を引数とし、戻り値は、関数の中で指定した値( Pivot )がソートしたときにあるべきインデックスです。

copy

Private Function Pivot_Posi(ByRef SortArray As Variant, _
                            ByVal topPosi As Long, _
                            ByVal tailPosi As Long)
    Dim Pivot               As String
    Dim smallPosi           As Long
    Dim largePosi           As Long
    Dim BUF                 As String

    '軸を配列の中央の値にします。(下図 置き換え0)
    Pivot = SortArray(Int((topPosi + tailPosi) / 2), 1)
    'topPosiのインデックスにある値と入れ替えます。
    SortArray(Int((topPosi + tailPosi) / 2), 1) = SortArray(topPosi, 1)
    SortArray(topPosi, 1) = Pivot

    'topPosi は Pivot のインデックスなので、探査は topPosi +1 となります。
    smallPosi = topPosi + 1
    largePosi = tailPosi

    '無限ループを開始します。
    Do While True

        'この例だと、上に Pivot より小さい値の領域、下に超える値の領域とします。
        '上から下へ探査して Pivot より大きい値のインデックスを探します。
        'このループは Pivot より小さい値の場合進み、超える場合は進みません。
        'したがって、大きい値のインデックスで止まります。
        '= を忘れずに必ずつけます。忘れると無限ループになります。
        Do While SortArray(smallPosi, 1) <= Pivot And smallPosi < tailPosi
            smallPosi = smallPosi + 1
        Loop

        '逆に、下から上へ探査して Pivot より小さい値のインデックスを探します。
        'このループは Pivot を超える値の場合進み、超えない場合は進みません。
        'したがって、 Pivot 以下の値のインデックスで止まります。
        Do While SortArray(largePosi, 1) > Pivot And largePosi > topPosi
            largePosi = largePosi - 1
        Loop

        '入れ子のループが終わり交差(smallPosiとlargePosi)しているか調べます。
        If smallPosi < largePosi Then
            '交差していなければ置き換えます。(下図 置き換え1)
            BUF = SortArray(smallPosi, 1)
            SortArray(smallPosi, 1) = SortArray(largePosi, 1)
            SortArray(largePosi, 1) = BUF
        Else
            'smallPosiとlargePosiは交差し領域の区分は終わったので、ループを抜けます。
            Exit Do
        End If

    Loop

    'smallPosiとlargePosiは交差して無限ループを抜けているので、
    'smallPosi は Pivot を超える値の最終位置、
    'また largePosi は Pivot 以下の値の最終位置となります。
    'したがって、Pivot のあるべきインデックスが求まったので、
    'largePosi の値と、topPosi の値(Pivot)と置き換えます。(下図 置き換え2)
    SortArray(topPosi, 1) = SortArray(largePosi, 1)
    SortArray(largePosi, 1) = Pivot

    Pivot_Posi = largePosi
    
End Function 

交差について

インデックスを1から10までの配列で、値は配列1の場合を考えてみます。Pivot をINDEXが 1 で、値が 12 とします。SmallPosi は 2 で、 LargePosi は 10 となります。上のコードの While の入れ子の最初で、SmallPosi の INDEX が 2 から 1 づつ増え INDEX 5 で止まります。2つ目の入れ子で、LargePosi の INDEX が 10 から 1 つづ減って INDEX 6 で止まります。次に IF の条件  smallPosi < largePosi を満たすため置き換え(置き換え1)が行われ配列2になります。

次に、外の While が最初から行われ、SmallPosi が示す INDEX 5 は 15 ではなく 8 であるから 1 つ増え SmallPosi は 6 となり、LargePosi が示す INDEX 6 は 8 ではなく 15 であるからは 1 つ減り 5 となります。図で見るように矢印線(赤色→)は交差することになります。

そして、LargePosi が Pivot より小さい値の境界になっているのが分かると思います。また、LargePosi が ソートした時の Pivot の正しい INDEX であることも理解できると思います。

IF文の条件を満たさなったので、無限ループを抜け LargePosi が示す INDEX 5 の 8 を Pivot TopPosi の INDEX にコピーし、Pivot の値を正しい INDEX の LargePosi にコピーします。(置き換え2)
配列3 のようになります。

配列3の INDEX 1 から 4 までの配列4( QuickB(SortArray, 1, 4) )、配列3の INDEX 6 から 10 までの配列5( QuickB(SortArray, 6, 10) ) を再帰します。

配列は参照して使われますので、配列1、配列2、配列3、配列4、配列5は同じ実体(プロシージャ QuickSortB で作成した配列 SortArray )を参照します。