アルゴリズム構築ブロック
まず、標準ライブラリのアルゴリズム構築ブロックをアセンブルします。
#include <algorithm> // min_element, iter_swap,
// upper_bound, rotate,
// partition,
// inplace_merge,
// make_heap, sort_heap, push_heap, pop_heap,
// is_heap, is_sorted
#include <cassert> // assert
#include <functional> // less
#include <iterator> // distance, begin, end, next
- そのような非会員としてイテレータツール
std::begin()
/ std::end()
並びにとはstd::next()
++ 11以降Cのとしてのみ利用可能です。C ++ 98の場合、これらを自分で作成する必要があります。そこBoost.Rangeからの代替をしているboost::begin()
/ boost::end()
し、中Boost.Utilityからboost::next()
。
- この
std::is_sorted
アルゴリズムは、C ++ 11以降でのみ使用できます。C ++ 98の場合、これはstd::adjacent_find
手書きの関数オブジェクトに関して実装できます。Boost.Algorithmもboost::algorithm::is_sorted
代替としてを提供します。
- この
std::is_heap
アルゴリズムは、C ++ 11以降でのみ使用できます。
構文上の利点
C ++ 14は、引数に多態的に作用する形式の透過的なコンパレータを提供std::less<>
します。これにより、イテレータのタイプを提供する必要がなくなります。これをC ++ 11のデフォルトの関数テンプレート引数と組み合わせて使用して、比較として使用するソートアルゴリズムとユーザー定義の比較関数オブジェクトを持つソートアルゴリズムの単一のオーバーロードを作成でき<
ます。
template<class It, class Compare = std::less<>>
void xxx_sort(It first, It last, Compare cmp = Compare{});
C ++ 11では、再利用可能なテンプレートエイリアスを定義して、並べ替えアルゴリズムのシグネチャに小さな混乱を追加するイテレータの値型を抽出できます。
template<class It>
using value_type_t = typename std::iterator_traits<It>::value_type;
template<class It, class Compare = std::less<value_type_t<It>>>
void xxx_sort(It first, It last, Compare cmp = Compare{});
C ++ 98では、2つのオーバーロードを記述し、詳細なtypename xxx<yyy>::type
構文を使用する必要があります。
template<class It, class Compare>
void xxx_sort(It first, It last, Compare cmp); // general implementation
template<class It>
void xxx_sort(It first, It last)
{
xxx_sort(first, last, std::less<typename std::iterator_traits<It>::value_type>());
}
- もう1つの構文上の優れた点は、C ++ 14が多機能ラムダ(
auto
関数テンプレート引数のように推定されるパラメーターを使用)を介してユーザー定義のコンパレーターをラップできることです。
- C ++ 11には、上記のテンプレートエイリアスを使用する必要がある単相ラムダのみがあります
value_type_t
。
- C ++ 98、1でスタンドアロン関数オブジェクトを書いたり、冗長に頼る必要があるのいずれか
std::bind1st
/ std::bind2nd
/ std::not1
構文のタイプ。
- Boost.Bindは
boost::bind
、_1
/ および_2
プレースホルダー構文でこれを改善します。
- C ++ 11とも有する超えて
std::find_if_not
C ++ 98人のニーズに対し、std::find_if
とstd::not1
機能の周りにオブジェクト。
C ++スタイル
一般に受け入れられるC ++ 14スタイルはまだありません。良くも悪くも、Scott Meyersの草案であるEffective Modern C ++とHerb Sutterの改良されたGotWに厳密に従っています。次のスタイルの推奨事項を使用します。
選択ソート
選択ソートはどのような方法でもデータに適応しないため、実行時間は常にになりO(N²)
ます。ただし、選択ソートには、スワップの数を最小限に抑えるという特性があります。アイテムを交換するコストが高いアプリケーションでは、選択ソートが非常に適切なアルゴリズムになる場合があります。
標準ライブラリを使用して実装するには、を繰り返し使用std::min_element
して残りの最小要素を見つけ、iter_swap
それを所定の位置に入れ替えます。
template<class FwdIt, class Compare = std::less<>>
void selection_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
for (auto it = first; it != last; ++it) {
auto const selection = std::min_element(it, last, cmp);
std::iter_swap(selection, it);
assert(std::is_sorted(first, std::next(it), cmp));
}
}
selection_sort
既に処理[first, it)
された範囲がループ不変としてソートされていることに注意してください。の最小要件は、のランダムアクセス反復子と比較して、前方std::sort
反復子です。
詳細は省略:
- 選択ソートは、早期テスト
if (std::distance(first, last) <= 1) return;
(または順方向/双方向反復子の場合:) で最適化できますif (first == last || std::next(first) == last) return;
。
- 以下のための双方向イテレータ、上記試験は、間隔にわたってループと組み合わせることができる
[first, std::prev(last))
最後の要素が最小残りの要素であることが保証され、スワップを必要としないので、。
挿入ソート
これはO(N²)
最悪の場合の基本的なソートアルゴリズムの1つですが、データがほぼソートされている場合(アダプティブであるため)または問題のサイズが小さい場合(オーバーヘッドが低いため)、挿入ソートは最適なアルゴリズムです。これらの理由により、また安定しているため、挿入ソートは、マージソートやクイックソートなどのオーバーヘッドの高い分割統治ソートアルゴリズムの再帰的な基本ケース(問題サイズが小さい場合)としてよく使用されます。
insertion_sort
標準ライブラリで実装するには、を繰り返し使用std::upper_bound
して、現在の要素が移動する必要がある場所を見つけ、を使用std::rotate
して残りの要素を入力範囲内で上に移動します。
template<class FwdIt, class Compare = std::less<>>
void insertion_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
for (auto it = first; it != last; ++it) {
auto const insertion = std::upper_bound(first, it, *it, cmp);
std::rotate(insertion, it, std::next(it));
assert(std::is_sorted(first, std::next(it), cmp));
}
}
insertion_sort
既に処理[first, it)
された範囲がループ不変としてソートされていることに注意してください。挿入ソートは、順方向反復子でも機能します。
詳細は省略:
- 最初の要素が確実に配置され、回転を必要としないため、挿入ソートは初期テスト
if (std::distance(first, last) <= 1) return;
(または順方向/双方向反復子:)if (first == last || std::next(first) == last) return;
と間隔[std::next(first), last)
でのループで最適化できます。
- 以下のために双方向イテレータ、挿入ポイントを見つけるためのバイナリ検索を置き換えることができ、逆線形検索標準ライブラリの使用して
std::find_if_not
アルゴリズムを。
以下のフラグメントの4つのライブ例(C ++ 14、C ++ 11、C ++ 98およびBoost、C ++ 98):
using RevIt = std::reverse_iterator<BiDirIt>;
auto const insertion = std::find_if_not(RevIt(it), RevIt(first),
[=](auto const& elem){ return cmp(*it, elem); }
).base();
- ランダム入力の
O(N²)
場合、これにより比較が行われますが、これによりO(N)
、ほとんどソートされた入力の比較が改善されます。二分探索は常にO(N log N)
比較を使用します。
- 入力範囲が小さい場合、線形検索のより良いメモリ局所性(キャッシュ、プリフェッチ)もバイナリ検索を支配する可能性があります(もちろん、これをテストする必要があります)。
クイックソート
注意深く実装すると、クイックソートは堅牢になりO(N log N)
、複雑さが予想されますが、O(N²)
最悪の場合は、逆に選択された入力データでトリガーされる可能性があります。安定したソートが必要ない場合、クイックソートは優れた汎用ソートです。
最も単純なバージョンであっても、標準ライブラリを使用して実装するクイックソートは、他の従来のソートアルゴリズムよりもかなり複雑です。用途少数イテレータユーティリティ以下のアプローチは、ロケートする中間要素入力レンジを[first, last)
、その後には、2つの呼び出しを使用し、ピボットのようにstd::partition
(あるO(N)
に等しい、より小さい要素のセグメントに三方パーティションへの入力範囲)、それぞれ、選択したピボットより大きい。最後に、ピボットよりも小さい要素と大きい要素を持つ2つの外側のセグメントが再帰的にソートされます。
template<class FwdIt, class Compare = std::less<>>
void quick_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
auto const N = std::distance(first, last);
if (N <= 1) return;
auto const pivot = *std::next(first, N / 2);
auto const middle1 = std::partition(first, last, [=](auto const& elem){
return cmp(elem, pivot);
});
auto const middle2 = std::partition(middle1, last, [=](auto const& elem){
return !cmp(pivot, elem);
});
quick_sort(first, middle1, cmp); // assert(std::is_sorted(first, middle1, cmp));
quick_sort(middle2, last, cmp); // assert(std::is_sorted(middle2, last, cmp));
}
ただし、上記の各手順を注意深く確認し、本番レベルのコードに合わせて最適化する必要があるため、クイックソートは正確かつ効率的にするためにかなりトリッキーです。特に、O(N log N)
複雑さのために、ピボットは入力データのバランスのとれたパーティションにならなければなりません。これは、一般にO(1)
ピボットでは保証できませんが、ピボットをO(N)
入力範囲の中央値として設定した場合は保証できます。
詳細は省略:
- 上記の実装は、特別な入力に対して特に脆弱です。たとえば
O(N^2)
、「オルガンパイプ」入力は複雑になります1, 2, 3, ..., N/2, ... 3, 2, 1
(中央が常に他のすべての要素よりも大きいため)。
- 入力範囲からランダムに選択された要素からの中央値3のピボット選択は、そうでなければ複雑さが悪化するほとんどソートされた入力から保護し
O(N^2)
ます。
- への2つの呼び出しで示されている 3方向のパーティション分割(ピボットよりも小さい、等しい、大きい要素の分離)
std::partition
は、O(N)
この結果を達成するための最も効率的なアルゴリズムではありません。
- 以下のためのランダムアクセスイテレータ、保証
O(N log N)
複雑さは、によって達成することができ、中央ピボット選択の使用std::nth_element(first, middle, last)
への再帰呼び出しが続く、quick_sort(first, middle, cmp)
とquick_sort(middle, last, cmp)
。
- ただし、この保証には代償が伴います。なぜなら、の
O(N)
複雑さの一定の要因は、中央値が3のピボットとそれに続く呼び出し(キャッシュに適した単一のフォワードパスオーバー)std::nth_element
のO(1)
複雑さのそれよりも高くなる可能性があるためです。データ)。O(N)
std::partition
マージソート
O(N)
余分なスペースを使用しても問題がない場合は、は、マージソートが最適です。これは、唯一の安定した O(N log N)
ソートアルゴリズムです。
標準アルゴリズムを使用して実装するのは簡単です。いくつかのイテレータユーティリティを使用して、入力範囲の中央を見つけます。 [first, last)
再帰的にソートされた2つのセグメントをで結合しますstd::inplace_merge
。
template<class BiDirIt, class Compare = std::less<>>
void merge_sort(BiDirIt first, BiDirIt last, Compare cmp = Compare{})
{
auto const N = std::distance(first, last);
if (N <= 1) return;
auto const middle = std::next(first, N / 2);
merge_sort(first, middle, cmp); // assert(std::is_sorted(first, middle, cmp));
merge_sort(middle, last, cmp); // assert(std::is_sorted(middle, last, cmp));
std::inplace_merge(first, middle, last, cmp); // assert(std::is_sorted(first, last, cmp));
}
マージソートには双方向のイテレータが必要で、ボトルネックはstd::inplace_merge
です。リンクリストをソートする場合、マージソートに必要なのはO(log N)
(再帰のために)余分なスペースだけであることに注意してください。後者のアルゴリズムはstd::list<T>::sort
、標準ライブラリで実装されています。
ヒープソート
ヒープソートは実装が簡単O(N log N)
で、インプレースソートを実行しますが、安定していません。
最初のループであるO(N)
「ヒープ化」フェーズでは、配列をヒープ順に配置します。2番目のループであるO(N log N
) "sortdown"フェーズは、最大値を繰り返し抽出し、ヒープ順序を復元します。標準ライブラリはこれを非常に簡単にします:
template<class RandomIt, class Compare = std::less<>>
void heap_sort(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
lib::make_heap(first, last, cmp); // assert(std::is_heap(first, last, cmp));
lib::sort_heap(first, last, cmp); // assert(std::is_sorted(first, last, cmp));
}
std::make_heap
and を使用するのが「だまされている」と考える場合はstd::sort_heap
、さらに1レベル深くして、それぞれstd::push_heap
とに関してこれらの関数を自分で書くことができstd::pop_heap
ます。
namespace lib {
// NOTE: is O(N log N), not O(N) as std::make_heap
template<class RandomIt, class Compare = std::less<>>
void make_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
for (auto it = first; it != last;) {
std::push_heap(first, ++it, cmp);
assert(std::is_heap(first, it, cmp));
}
}
template<class RandomIt, class Compare = std::less<>>
void sort_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
for (auto it = last; it != first;) {
std::pop_heap(first, it--, cmp);
assert(std::is_heap(first, it, cmp));
}
}
} // namespace lib
標準ライブラリは両方push_heap
とpop_heap
複雑さを指定しますO(log N)
。範囲にわたって外側ループことに注意してください[first, last)
をもたらすO(N log N)
ための複雑さmake_heap
に対し、std::make_heap
のみ有するO(N)
複雑さ。全体的なO(N log N)
複雑さはheap_sort
問題ではありません。
詳細省略:O(N)
実装make_heap
テスト中
以下は、さまざまな入力(網羅的または厳密ではない)で5つすべてのアルゴリズムをテストする4つのライブ例(C ++ 14、C ++ 11、C ++ 98およびBoost、C ++ 98)です。LOCの大きな違いに注意してください。C++ 11 / C ++ 14には約130のLOC、C ++ 98、Boost 190(+ 50%)とC ++ 98が270(+ 100%)以上必要です。