C++標準ライブラリのstd::sort
アルゴリズム(およびそのいとこstd::partial_sort
およびstd::nth_element
)は、ほとんどの実装にあります より基本的なソートアルゴリズムの複雑でハイブリッドな融合 (選択ソート、挿入ソート、クイックソート、マージなど)ソート、またはヒープソート。
ここや、 https://codereview.stackexchange.com/ などの姉妹サイトには、バグ、複雑さ、およびこれらの古典的な並べ替えアルゴリズムの実装の他の側面に関連する多くの質問があります。提供される実装のほとんどは、生のループで構成され、インデックス操作と具象型を使用し、通常、正確さと効率の観点から分析するのは簡単ではありません。
質問:上記の古典的なソートアルゴリズムは、現代のC++を使用してどのように実装できますか?
<algorithm>
のアルゴリズム構成ブロックを組み合わせますauto
、テンプレートエイリアス、透過コンパレータ、ポリモーフィックラムダなどの構文ノイズリデューサーを含みます。注:
for
- loopですオペレーター。したがって、f(g(x));
またはf(x); g(x);
またはf(x) + g(x);
は生のループではなく、以下のselection_sort
およびinsertion_sort
のループでもありません。別の小さくてエレガントなもの 元はコードレビューで見つかりました 。共有する価値があると思いました。
かなり特殊ですが、 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 の一部により、アルゴリズムがさらにクリーンになります。