現代のC ++でクラシックな並べ替えアルゴリズムを実装する方法は?


331

std::sortアルゴリズム(そのいとこstd::partial_sortstd::nth_elementC ++標準ライブラリからの)は、ほとんどの実装である複数の基本ソートアルゴリズムの複雑ハイブリッド融合そのような選択ソート、挿入ソート、クイックソート、マージソート、又はヒープソートなど、。

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

質問:上記の古典的な並べ替えアルゴリズムは、最新のC ++を使用してどのように実装できますか?

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

  • ソートアルゴリズムの実装の詳細については、WikipediaRosetta Code、またはhttp://www.sorting-algorithms.com/を参照してください。
  • ショーン親の規則(スライド39)、生のループであるforオペレータと長い2つの関数の組成より-loop。そうf(g(x));か、f(x); g(x);あるいはf(x) + g(x);生のループではありません、そしてどちらのループしているselection_sortinsertion_sort以下。
  • 私はスコットマイヤーズの用語に従い、現在のC ++ 1yをすでにC ++ 14として示し、C ++ 98とC ++ 03の両方をC ++ 98として示しています。
  • @Mehrdadのコメントで示唆されているように、回答の最後にライブ例として4つの実装を提供します。C++ 14、C ++ 11、C ++ 98、BoostおよびC ++ 98です。
  • 答え自体はC ++ 14に関してのみ示されています。必要に応じて、さまざまな言語バージョンが異なる構文とライブラリの違いを示します。

8
質問にC ++ Faqタグを追加すると便利ですが、他のタグの少なくとも1つを失う必要があります。私はバージョンを削除することをお勧めします(これは一般的なC ++の質問であり、実装はほとんどのバージョンで利用可能であり、多少の調整が必要です)。
Matthieu M.

@TemplateRexよく、技術的には、FAQでない場合、この質問は広すぎます(推測-私は反対投票しませんでした)。ところで 良い仕事、たくさんの有用な情報、ありがとう:)
BartoszKP

回答:


388

アルゴリズム構築ブロック

まず、標準ライブラリのアルゴリズム構築ブロックをアセンブルします。

#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_notC ++ 98人のニーズに対し、std::find_ifstd::not1機能の周りにオブジェクト。

C ++スタイル

一般に受け入れられるC ++ 14スタイルはまだありません。良くも悪くも、Scott Meyersの草案であるEffective Modern C ++とHerb Sutterの改良されたGotWに厳密に従っています。次のスタイルの推奨事項を使用します。

  • Herb Sutterの"Almost Always Auto"とScott Meyersの"Prefer auto to specific type宣言"の推奨事項。これは、その明快さには異議が唱えられますが、その簡潔さは卓越しています。
  • Scott Meyersの「オブジェクトの識別(){}作成時」{}で、古き良き括弧で囲まれた初期化の代わりに括弧で囲まれた初期化を一貫して選択します()(一般的なコードで最も多くのvexing解析の問題を回避するため)。
  • スコット・マイヤーズさん「のtypedefにエイリアス宣言を好みます」。テンプレートの場合、これはとにかく必須であり、typedef時間を節約し、一貫性を追加する代わりにどこでもそれを使用します。
  • for (auto it = first; it != last; ++it)すでにソートされたサブ範囲のループ不変チェックを可能にするために、いくつかの場所でパターンを使用しています。生産コードでは、使用while (first != last)して++firstループ内のどこかには、わずかに良いかもしれません。

選択ソート

選択ソートはどのような方法でもデータに適応しないため、実行時間は常にになり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 ++ 14C ++ 11C ++ 98およびBoostC ++ 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_elementO(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_heapand を使用するのが「だまされている」と考える場合は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_heappop_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 ++ 14C ++ 11C ++ 98およびBoostC ++ 98)です。LOCの大きな違いに注意してください。C++ 11 / C ++ 14には約130のLOC、C ++ 98、Boost 190(+ 50%)とC ++ 98が270(+ 100%)以上必要です。


13
一方で、私はあなたの使用に反対auto(と多くの人が私と一緒に反対する)、私にはよく使われている標準ライブラリのアルゴリズムを見て楽しみました。Sean Parentの講演を見た後、私はこの種のコードのいくつかの例を見たいと思っていました。また、std::iter_swapそれが中にあるのは不思議に思えますが、私は存在することを知りません<algorithm>でした。
ジョセフマンスフィールド

32
@sbabbi標準ライブラリ全体は、イテレータのコピーが安価であるという原則に基づいています。たとえば、値で渡します。イテレータをコピーするのが安くない場合は、どこでもパフォーマンスの問題が発生します。
James Kanze 2014

2
素晴らしいポスト。[std ::] make_heapの不正行為部分について。std :: make_heapが不正行為と見なされる場合、std :: push_heapも同様です。つまり、不正行為=ヒープ構造に定義された実際の動作を実装しない。push_heapも含まれているとわかりやすいでしょう。
キリン船長、2014

3
@gnzlbgもちろん、コメントアウトできるアサートです。初期のテストは、ランダムアクセス用の現在のバージョンを使用して、イテレータカテゴリごとにタグディスパッチできますif (first == last || std::next(first) == last)。後で更新するかもしれません。「省略された詳細」セクションにあるものを実装することは、Q&A全体へのリンクが含まれているため、IMOの質問の範囲を超えています。実際の単語の並べ替えルーチンの実装は困難です。
TemplateRex

3
素晴らしいポスト。けれども、nth_element私の意見では、あなたはあなたのクイックソートでだまされました。nth_elementすでに半分のクイックソートを実行します(パーティション化ステップと、関心のあるn番目の要素を含む半分の再帰を含みます)。
セルビテス2014

14

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

カウントソート

ソートはかなり特殊ですが、ソートソートは単純な整数ソートアルゴリズムであり、ソートする整数の値がそれほど離れていなければ、しばしば非常に高速になります。たとえば、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++);
    }
}

並べ替える整数の範囲が小さい(通常、並べ替えるコレクションのサイズよりも大きくない)ことがわかっている場合にのみ役立ちますが、並べ替えをより一般的にカウントすると、最良の場合に遅くなります。範囲が小さいことが知られていない場合は、別のアルゴリズムのようなAの基数ソートska_sortまたはspreadsortを代わりに使用することができます。

詳細は省略

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

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

  • countsインクリメントループがヒストグラムです。データが非常に反復的である可能性が高く、ビンの数が少ない場合は、複数のアレイ展開して、同じビンへのストア/リロードのシリアル化データ依存性のボトルネックを削減することをお勧めします。これは、最初にゼロにカウントする数が多く、最後にループする数が多いことを意味しますが、特に入力が既に(部分的に)ソートされており、同じ数の長い実行があります。

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

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

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


@TemplateRex任意の比較オブジェクトを取得できた場合、カウントソートは比較ソートになり、比較ソートはO(n log n)よりも最悪のケースになることはありません。カウントソートはO(n + r)の最悪のケースを持っています。これは、それが比較ソートになることはできないことを意味します。整数比較できますが、このプロパティはソートの実行には使用されません(std::minmax_element情報を収集するだけで使用されます)。使用されるプロパティは、整数をインデックスまたはオフセットとして使用できることと、後者のプロパティを保持しながらインクリメントできることです。
Morwenn、2016年

範囲TSは確かに非常に優れています。たとえば、最後のループが終了しているcounts | ranges::view::filter([](auto c) { return c != 0; })ため、内でゼロ以外のカウントを繰り返しテストする必要がありませんfill_n
TemplateRex

(私は中にタイプミスを発見して-私は編集に関するreggae_sortゴマそれらを保つこと?)small ratherappart
老い

@greybeard好きなことを何でもできます:p
Morwenn

counts[]オンザフライで成長させることはminmax_element、ヒストグラム作成の前に入力をトラバースすることと比較して有利になると思います。特に、これが理想的なユースケースでは、小さな範囲で多くの繰り返しがあり、countsブランチの予測ミスやサイズの倍増がほとんどなく、フルサイズにすぐに成長するためです。(もちろん、範囲の十分に小さい境界を知ることで、minmax_elementスキャン回避し、ヒストグラムループ内での境界チェック回避できます。)
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.