web-dev-qa-db-ja.com

現代のC ++でクラシックソートアルゴリズムを実装する方法

C++標準ライブラリのstd::sortアルゴリズム(およびそのいとこstd::partial_sortおよびstd::nth_element)は、ほとんどの実装にあります より基本的なソートアルゴリズムの複雑でハイブリッドな融合 (選択ソート、挿入ソート、クイックソート、マージなど)ソート、またはヒープソート。

ここや、 https://codereview.stackexchange.com/ などの姉妹サイトには、バグ、複雑さ、およびこれらの古典的な並べ替えアルゴリズムの実装の他の側面に関連する多くの質問があります。提供される実装のほとんどは、生のループで構成され、インデックス操作と具象型を使用し、通常、正確さと効率の観点から分析するのは簡単ではありません。

質問:上記の古典的なソートアルゴリズムは、現代のC++を使用してどのように実装できますか?

  • 生のループはありませんが、標準ライブラリの<algorithm>のアルゴリズム構成ブロックを組み合わせます
  • イテレータインターフェイスおよびインデックス操作および具象型の代わりにtemplatesを使用
  • C++ 14スタイル、完全な標準ライブラリ、およびauto、テンプレートエイリアス、透過コンパレータ、ポリモーフィックラムダなどの構文ノイズリデューサーを含みます。

  • ソートアルゴリズムの実装の詳細については、 WikipediaRosetta Code または http://www.sorting-algorithms.com/ を参照してください。
  • Sean Parentの規約 (スライド39)に従って、生のループは、2つの関数の構成よりも長いfor- loopですオペレーター。したがって、f(g(x));またはf(x); g(x);またはf(x) + g(x);は生のループではなく、以下のselection_sortおよびinsertion_sortのループでもありません。
  • Scott Meyersの用語に従い、現在のC++ 1yをすでにC++ 14として示し、C++ 98とC++ 03を両方ともC++ 98として示しているので、そのことで私に火をつけないでください。
  • @Mehrdadのコメントで示唆されているように、答えの最後に、C++ 14、C++ 11、C++ 98、BoostおよびC++ 98の4つの実装をLive Exampleとして提供します。
  • 答え自体は、C++ 14の観点からのみ提示されます。関連する場合は、さまざまな言語バージョンが異なる構文とライブラリの違いを示します。
316
TemplateRex

別の小さくてエレガントなもの 元はコードレビューで見つかりました 。共有する価値があると思いました。

カウントソート

かなり特殊ですが、 counting sort は単純な整数ソートアルゴリズムであり、ソートする整数の値がそれほど離れていない場合、非常に高速になることがよくあります。たとえば、0から100の間であることが知られている100万個の整数のコレクションをソートする必要がある場合は、おそらく理想的です。

符号付き整数と符号なし整数の両方で機能する非常に単純なカウント並べ替えを実装するには、並べ替えるコレクション内の最小要素と最大要素を見つける必要があります。それらの違いにより、割り当てるカウントの配列のサイズがわかります。次に、コレクションの2回目のパスを実行して、すべての要素の出現回数をカウントします。最後に、すべての整数の必要な数を元のコレクションに書き戻します。

template<typename ForwardIterator>
void counting_sort(ForwardIterator first, ForwardIterator last)
{
    if (first == last || std::next(first) == last) return;

    auto minmax = std::minmax_element(first, last);  // avoid if possible.
    auto min = *minmax.first;
    auto max = *minmax.second;
    if (min == max) return;

    using difference_type = typename std::iterator_traits<ForwardIterator>::difference_type;
    std::vector<difference_type> counts(max - min + 1, 0);

    for (auto it = first ; it != last ; ++it) {
        ++counts[*it - min];
    }

    for (auto count: counts) {
        first = std::fill_n(first, count, min++);
    }
}

ソートする整数の範囲が小さい(通常、ソートするコレクションのサイズより大きくない)ことがわかっている場合にのみ役立ちますが、ソートのカウントをより一般的にすると、ベストケースでは遅くなります。範囲が小さいことがわからない場合は、代わりに 基数ソートska_sort 、または スプレッドソート などの別のアルゴリズムを使用できます。

詳細の省略

  • アルゴリズムで受け入れられる値の範囲の境界をパラメーターとして渡して、コレクションを最初に通過するstd::minmax_elementを完全に取り除くことができます。これにより、他の方法で有用な範囲の制限がわかっている場合に、アルゴリズムがさらに高速になります。 (正確である必要はありません;定数0から100を渡すことは、真の境界が1から0であることを見つけるために、100万個の要素を余分に渡すよりもmuch優れています。 95. 0から1000でさえ価値があります;余分な要素はゼロで1回書き込まれ、1回読み取られます)。

  • countsをその場で成長させることは、別の最初のパスを回避する別の方法です。成長する必要があるたびにcountsサイズを2倍にすると、ソートされた要素ごとに償却O(1)時間が得られます(指数関数的成長が重要であることの証明については、ハッシュテーブル挿入コスト分析を参照してください)。新しいmaxの最後に成長すると、std::vector::resizeを使用して新しいゼロ要素を簡単に追加できます。その場でminを変更し、先頭に新しいゼロ要素を挿入するには、ベクトルを成長させた後、std::copy_backwardを使用します。次に、std::fillを使用して、新しい要素をゼロにします。

  • countsインクリメントループはヒストグラムです。データの反復性が高く、ビンの数が少ない場合、 複数の配列での展開 を使用すると、同じビンへのストア/リロードのシリアル化データ依存性ボトルネックを減らすことができます。これは、開始時にゼロにカウントを増やし、終了時にループオーバーすることを意味しますが、0から100の数百万の例のほとんどのCPUでは、特に入力がすでに(部分的に)ソートされている場合、同じ数の長い実行があります。

  • 上記のアルゴリズムでは、min == maxチェックを使用して、すべての要素が同じ値を持つ場合に早期に戻ります(この場合、コレクションはソートされます)。代わりに、コレクションが既に並べ替えられているかどうかを完全に確認しながら、追加の時間を無駄にすることなくコレクションの極端な値を見つけることができます(最初のパスがまだminとmaxを更新する余分な作業でメモリボトルネックの場合)。ただし、このようなアルゴリズムは標準ライブラリには存在せず、1つを記述することは、残りのカウントソート自体を記述するよりも面倒です。読者の練習問題として残されています。

  • アルゴリズムは整数値でのみ機能するため、静的アサーションを使用して、ユーザーが明らかな型ミスをしないようにすることができます。コンテキストによっては、std::enable_if_tによる置換の失敗が優先される場合があります。

  • 最新のC++はクールですが、将来のC++はさらにクールになる可能性があります。 構造化バインディング および Ranges TS の一部により、アルゴリズムがさらにクリーンになります。

14
Morwenn