C# Linqでクイックソート

qsort []     = []
qsort (x:xs) = qsort elts_lt_x ++ [x] ++ qsort elts_greq_x
                 where
                   elts_lt_x   = [y | y <- xs, y < x]
                   elts_greq_x = [y | y <- xs, y >= x]

これはHaskellのコードで、良く見かける定番のQuickSort。うん、短い。というわけでLinqでそれをやる。というネタは既出のン番煎じなのですが、気にせずやる。

// LinqでHaskell風のクイックソート
public static IEnumerable<T> QuickSort<T>(IEnumerable<T> source)
    where T : IComparable<T>
{
    if (!source.Any()) return source;
    var pivot = source.First();
    return source
        .GroupBy(x => x.CompareTo(pivot))
        .OrderBy(g => g.Key) // OrderBy使うのはどうかなー、というところはある
        .SelectMany(g => (g.Key == 0) ? g : QuickSort(g));
}

GroupBy->SelectManyと流れるように書けて美しいー。これならHaskellにも引けを取らないですね!但し問題なのは、OrderByを使用しているところ。CompareToの結果である-1, 0, 1を並べ替えるために使っているのだけど、OrderByの中身はシュワルツ変換の挟まったクイックソートそのものなので邪道な感は否めない。OrderBy使うなら、そもそもsource.OrderBy(x => x)でもいいぢゃん、って話になってしまう。

// 非GroupBy版。LookupはGroupByの即時評価版と考えていい。
public static IEnumerable<T> QuickSort<T>(IEnumerable<T> source)
    where T : IComparable<T>
{
    if (!source.Any()) return source;
    var pivot = source.First();
    var lookup = source.ToLookup(x => x.CompareTo(pivot));
    return QuickSort(lookup[-1]).Concat(lookup[0]).Concat(QuickSort(lookup[1]));
}

GroupByをToLookupに書き変えました。ToLookupはGroupByの即時評価版といった感じで、インデクサによるアクセスが可能。インデクサ使わないでそのままforeachで列挙する、なんて時はGroupByを使った方が良いです。今回はインデクサで順番を明示的に指定するため、ToLookupでパーティション切って、Concatで繋いでやれば出来あがり。やってることが非常に分かりやすくて良い。ソースの見た目も中々綺麗じゃないでしょうか。Haskellのものとも非常に近いです(ようするに++がConcatなので) ToLookupではなくWhereを使えば、見た目は更にHaskellに近づきますが、列挙が二回になるので、ここはToLookupで。

// 普通に?書いた場合。
public static void QuickSort<T>(IList<T> source, int lowerBound, int upperBound)
    where T : IComparable<T>
{
    var pivot = source[lowerBound + ((upperBound - lowerBound) >> 1)];
    var left = lowerBound - 1;
    var right = upperBound + 1;
    while (true)
    {
        while (source[++left].CompareTo(pivot) < 0) ;
        while (source[--right].CompareTo(pivot) > 0) ;
        if (left >= right) break;
        var temp = source[left];
        source[left] = source[right];
        source[right] = temp;
    }
    if (lowerBound < left - 1) QuickSort(source, lowerBound, left - 1);
    if (right + 1 < upperBound) QuickSort(source, right + 1, upperBound);
}

今度は非Linqに一般的?な書き方で。あまりヘタな書き方するとアレだなあ、と思ったので404 Blog Not Found:javascript - Array#sortはオレquicksortより遅い by ChromeのコードをC#に移植しました。あたしこの書き方嫌いなのよね、という感じにゴチャゴチャした印象は否めないというか、まあ、嫌いなのよね。一本の配列で頑張るところが、ゆとりな私としてはしんどい。ビットシフトも嫌よね。

public static IEnumerable<T> QuickSort<T>(IEnumerable<T> source)
    where T : IComparable<T>
{
    var enumerator = source.GetEnumerator();
    if (!enumerator.MoveNext()) yield break;

    var pivot = enumerator.Current;
    var less = new List<T>();
    var equal = new List<T>();
    var greater = new List<T>();
    do
    {
        switch (enumerator.Current.CompareTo(pivot))
        {
            case -1: less.Add(enumerator.Current); break;
            case 0: equal.Add(enumerator.Current); break;
            case 1: greater.Add(enumerator.Current); break;
        }
    } while (enumerator.MoveNext());

    foreach (var item in QuickSort(less)) yield return item;
    foreach (var item in equal) yield return item;
    foreach (var item in QuickSort(greater)) yield return item;
}

最後に、非Linqで、ToLookup版を再現してみたものを。do-whileがToLookupでforeachの連発がConcat。ようするに富豪的にListを作りまくるってわけなんですね!書きやすいし分かりやすいので、一本配列版よりも遥かに好き度高い。現代人はListを贅沢に大量に好きなだけ使うのです。まあ、Linq版がない状態でこの書き方が浮かぶ or 実行に移せるかどうかといったら、かなり無理ですけど。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive