CPUにバインドされたコードのプロファイリングは、コンテナーに完全に一意の要素が含まれているかどうかを確認するために長い時間を費やすことを示唆しています。ソートされていない要素の大きなコンテナがあると仮定します(<
および=
定義)、これがどのように行われるかについて2つのアイデアがあります。
セットを使用する最初の:
template <class T>
bool is_unique(vector<T> X) {
set<T> Y(X.begin(), X.end());
return X.size() == Y.size();
}
要素の2番目のループ:
template <class T>
bool is_unique2(vector<T> X) {
typename vector<T>::iterator i,j;
for(i=X.begin();i!=X.end();++i) {
for(j=i+1;j!=X.end();++j) {
if(*i == *j) return 0;
}
}
return 1;
}
私はできる限りそれらをテストしました。STLに関するドキュメントを読んで収集できることから、答えは(いつものように)状況によって異なります。最初のケースでは、すべての要素が一意である場合は非常に高速だと思いますが、縮退が大きい場合、操作にはO(N ^ 2)時間がかかるようです。ネストされたイテレータアプローチの場合、反対のことが当てはまるようです。X[0]==X[1]
ただし、すべての要素が一意である場合、(当然のことながら)O(N ^ 2)時間がかかります。
これを行うためのより良い方法はありますか、おそらくこの目的のために構築されたSTLアルゴリズムですか?そうでない場合、もう少し効率を追求する提案はありますか?
set
は挿入ごとにlogN時間を要するため、最初の例はO(N log N)である必要があります。より速いOは不可能だと思います。
2番目の例は明らかにO(N ^ 2)です。係数とメモリ使用量が少ないため、場合によっては高速(または最速)になることがあります。
T
が何であるかによって異なりますが、一般的なパフォーマンスのために、オブジェクトへのポインターのベクトルを並べ替えることをお勧めします。
template< class T >
bool dereference_less( T const *l, T const *r )
{ return *l < *r; }
template <class T>
bool is_unique(vector<T> const &x) {
vector< T const * > vp;
vp.reserve( x.size() );
for ( size_t i = 0; i < x.size(); ++ i ) vp.Push_back( &x[i] );
sort( vp.begin(), vp.end(), ptr_fun( &dereference_less<T> ) ); // O(N log N)
return adjacent_find( vp.begin(), vp.end(),
not2( ptr_fun( &dereference_less<T> ) ) ) // "opposite functor"
== vp.end(); // if no adjacent pair (vp_n,vp_n+1) has *vp_n < *vp_n+1
}
またはSTLスタイルで、
template <class I>
bool is_unique(I first, I last) {
typedef typename iterator_traits<I>::value_type T;
…
もちろん、元のベクトルを並べ替えることができれば、
template <class T>
bool is_unique(vector<T> &x) {
sort( x.begin(), x.end() ); // O(N log N)
return adjacent_find( x.begin(), x.end() ) == x.end();
}
一意の要素しかないかどうかをすばやく判断する場合は、ベクトルを並べ替える必要があります。それ以外の場合は、O(n ^ 2)ランタイムまたはO(n log n)ランタイムとO(n)スペース)が最善です。入力を想定する関数を作成するのが最善だと思います。ソートされます。
template<class Fwd>
bool is_unique(In first, In last)
{
return adjacent_find(first, last) == last;
}
次に、クライアントにベクターを並べ替えさせるか、ベクターの並べ替えられたコピーを作成します。これにより、動的計画法への扉が開かれます。つまり、クライアントが過去にベクターをソートした場合、そのソートされたベクターを保持および参照するオプションがあり、O(n)ランタイムでこの操作を繰り返すことができます。
標準ライブラリにはstd::unique
がありますが、コンテナ全体のコピーを作成する必要があります(どちらの例でも、ベクトルを値で不必要に渡すため、ベクトル全体のコピーも作成することに注意してください) )。
template <typename T>
bool is_unique(std::vector<T> vec)
{
std::sort(vec.begin(), vec.end());
return std::unique(vec.begin(), vec.end()) == vec.end();
}
ご存知のように、これがstd::set
を使用するよりも速いかどうかは、:-)に依存します。
この「保証」を最初から提供するコンテナを使用することは不可能ですか?将来のある時点ではなく、挿入時に重複にフラグを立てることは有用でしょうか?私がこのようなことをしたかったとき、それは私が行った方向です。セットを「プライマリ」コンテナとして使用し、元の順序を維持する必要がある場合は並列ベクトルを構築するだけですが、もちろん、メモリとCPUの可用性についていくつかの仮定があります...
1つには、両方の利点を組み合わせることができます。重複をすでに発見している場合は、セットの作成を停止します。
template <class T>
bool is_unique(const std::vector<T>& vec)
{
std::set<T> test;
for (typename std::vector<T>::const_iterator it = vec.begin(); it != vec.end(); ++it) {
if (!test.insert(*it).second) {
return false;
}
}
return true;
}
ところで、 Potatoswatter は、一般的なケースではTのコピーを避けたい場合があり、その場合は代わりにstd::set<const T*, dereference_less>
を使用することをお勧めします。
もちろん、それが一般的でなければ、はるかにうまくいく可能性があります。たとえば、既知の範囲の整数のベクトルがある場合、要素が存在する場合は、配列(またはビットセット)でマークを付けることができます。
std::unique
を使用できますが、最初に範囲を並べ替える必要があります。
template <class T>
bool is_unique(vector<T> X) {
std::sort(X.begin(), X.end());
return std::unique(X.begin(), X.end()) == X.end();
}
std::unique
はシーケンスを変更し、イテレータを一意のセットの最後に返すため、それでもベクトルの最後である場合は、一意である必要があります。
これはnlog(n)で実行されます。セットの例と同じです。理論的には、std::unordered_set
の代わりにC++ 0x std::set
を使用すると、予想される線形時間で実行できるとは限りませんが、要素を次のようにハッシュ可能にする必要があります。 operator ==
を定義するだけでなく、それほど簡単ではないかもしれません。
また、例でベクトルを変更していない場合は、const参照で渡すことでパフォーマンスが向上するため、不要なコピーを作成する必要はありません。
私が自分の2セントを追加することができれば。
まず、@Potatoswatter
が述べたように、要素をコピーするのが安価でない限り(組み込み/小さなPOD)、コピーするのではなく、元の要素へのポインターを使用することをお勧めします。
第二に、利用可能な2つの戦略があります。
私は最初に傾くことを認めなければなりません。カプセル化、責任の明確な分離など。
とにかく、要件に応じていくつかの方法があります。最初の質問は次のとおりです。
vector
の要素を特定の順序にする必要がありますか、それともそれらを「混乱」させることができますか?それらをいじることができる場合は、vector
をソートしたままにしておくことをお勧めします。Loki::AssocVector
で開始できます。そうでない場合は、このプロパティを確保するために構造体のインデックスを保持する必要があります...ちょっと待ってください:Boost.MultiIndex
救助に?
第三に:あなたが自分自身に言ったように、単純な線形探索は2倍になり、O(N2)平均して複雑で、これは良くありません。
<
がすでに定義されている場合、O(N log N)の複雑さで、ソートは明白です。 std::tr1::hash_set
の方が時間がかかる可能性があるため、T
をハッシュ可能にすることも価値があるかもしれません(RandomAccessIteratorが必要ですが、T
がハッシュ可能であれば簡単です) T*
をハッシュ可能にする;))
しかし、結局のところ、ここでの本当の問題は、データが不足しているため、アドバイスが一般的である必要があるということです。
T
とは何ですか、アルゴリズムを一般的なものにするつもりですか?さて、あなたの最初のものはN log(N)
だけを取るべきなので、それは明らかにこのアプリケーションにとってより良い最悪のシナリオです。
ただし、セットに物事を追加するときにチェックすると、より良い最良のケースを得ることができるはずです。
_template <class T>
bool is_unique3(vector<T> X) {
set<T> Y;
typename vector<T>::const_iterator i;
for(i=X.begin(); i!=X.end(); ++i) {
if (Y.find(*i) != Y.end()) {
return false;
}
Y.insert(*i);
}
return true;
}
_
これには、O(1)
の最良の場合、O(N log(N))
の最悪の場合があり、平均的な場合は入力の分布に依存します。
ベクターに格納するタイプTが大きく、コピーにコストがかかる場合は、ベクター要素へのポインターまたはイテレーターのベクターを作成することを検討してください。指し示した要素に基づいて並べ替えてから、一意性を確認します。
そのためにstd :: setを使用することもできます。テンプレートは次のようになります
template <class Key,class Traits=less<Key>,class Allocator=allocator<Key> > class set
適切なTraitsパラメーターを指定し、速度を上げるために生のポインターを挿入するか、<演算子を使用してポインターの単純なラッパークラスを実装できると思います。
セットに挿入するためにコンストラクターを使用しないでください。挿入メソッドを使用します。メソッド(オーバーロードの1つ)には署名があります
pair <iterator, bool> insert(const value_type& _Val);
結果を確認することにより(2番目のメンバー)すべての要素を挿入した場合よりも、重複をはるかに迅速に検出できることがよくあります。
既知の、大きすぎない最大値Nで離散値をソートする(非常に)特殊なケース。
バケットソートを開始して、各バケットの値の数が2未満であることを確認するだけで済みます。
bool is_unique(const vector<int>& X, int N)
{
vector<int> buckets(N,0);
typename vector<int>::const_iterator i;
for(i = X.begin(); i != X.end(); ++i)
if(++buckets[*i] > 1)
return false;
return true;
}
これの複雑さはO(n)になります。
現在のC++標準コンテナーを使用すると、最初の例で優れたソリューションが得られます。ただし、ハッシュコンテナを使用できる場合は、標準セットのハッシュセットがn n O(log n)ではなくn ---(O(1)になるため、より適切に実行できる可能性があります。もちろん、すべてはnのサイズと特定のライブラリの実装に依存します。