web-dev-qa-db-ja.com

コレクション内の重複要素を見つけてグループ化するための高速アルゴリズムは何ですか?

要素のコレクションがあるとしましょう。重複している要素を選択して、比較の量を最小限に抑えて各グループに入れるにはどうすればよいでしょうか。できればC++で。ただし、アルゴリズムは言語よりも重要です。たとえば、{E1、E2、E3、E4、E4、E2、E6、E4、E3}の場合、{E2、E2}、{E3、E3}、{E4、E4、E4}を抽出します。どのデータ構造とアルゴリズムを選択しますか?たとえば、std :: multimapのように事前に並べ替えられている場合は、データ構造の設定コストも含めてください。

更新

提案されたように物事をより明確にするため。 1つの制約があります:要素はそれ自体で比較する必要がありますそれらが重複していることを確認します。

したがって、ハッシュは適用されません。事実上、ハッシュは比較を重い要素(データのチャンクなど)から軽い要素(整数)にシフトし、一部の比較を減らしますが、それらをなくすことはありません。最終的には、 1つの衝突バケット内にある場合の元の問題。

それぞれGBのファイルを複製する可能性のあるものがたくさんあると仮定すると、それらは人間が知っているすべてのハッシュアルゴリズムによって同じハッシュ値を持ちます。次に、実際の重複を見つけます。

いいえ、実際の問題になることはありません(MD5でさえ、実際のファイルの一意のハッシュを生成するのに十分です)。しかし、私たちができるようにふりをしてくださいデータ構造を見つけることに焦点を当てる+比較の量が最も少ないアルゴリズム


私がしていることは

  1. sTL std :: listデータ構造に表現します(1)その要素の削除は、たとえばベクトルよりも安価です2)挿入は安価であり、並べ替えは必要ありません。)

  2. 1つの要素を取り出して残りの要素と比較します。重複が見つかった場合は、リストから削除されます。リストの最後に達すると、重複のグループが1つ見つかります(存在する場合)。

  3. リストが空になるまで、上記の2つの手順を繰り返します。

最良の場合はN-1が必要ですが、(N-1)!最悪の場合。

より良い選択肢は何ですか?


上で説明した方法を使用した私のコード:

// algorithm to consume the std::list container,
// supports: list<path_type>,list< pair<std::string, paths_type::const_iterater>>
template<class T>
struct consume_list
{
    groups_type operator()(list<T>& l)
    {
        // remove spurious identicals and group the rest
        // algorithm:  
        // 1. compare the first element with the remaining elements, 
        //    pick out all duplicated files including the first element itself.
        // 2. start over again with the shrinked list
        //     until the list contains one or zero elements.

        groups_type sub_groups;           
        group_type one_group; 
        one_group.reserve(1024);

        while(l.size() > 1)
        {
            T front(l.front());
            l.pop_front();

            item_predicate<T> ep(front);
            list<T>::iterator it     = l.begin(); 
            list<T>::iterator it_end = l.end();
            while(it != it_end)
            {
                if(ep.equals(*it))
                {
                    one_group.Push_back(ep.extract_path(*(it))); // single it out
                    it = l.erase(it);
                }
                else
                {
                    it++;
                }
            }

            // save results
            if(!one_group.empty())
            {
                // save
                one_group.Push_back(ep.extract_path(front));                    
                sub_groups.Push_back(one_group);

                // clear, memory allocation not freed
                one_group.clear(); 
            }            
        }
        return sub_groups;
    }        
}; 


// type for item-item comparison within a stl container, e.g.  std::list 
template <class T>
struct item_predicate{};

// specialization for type path_type      
template <>
struct item_predicate<path_type>
{
public:
    item_predicate(const path_type& base)/*init list*/            
    {}
public:
    bool equals(const path_type& comparee)
    {
        bool  result;
        /* time-consuming operations here*/
        return result;
    }

    const path_type& extract_path(const path_type& p)
    {
        return p;
    }
private:
    // class members
}; 


};

以下の回答に感謝しますが、私の例では整数に関するものであると誤解されているようです。実際要素は型に依存しません(必ずしも整数、文字列、またはその他のPODである必要はありません)、等しい述語は自己定義されます、つまり比較は非常に重い場合があります

つまり、私の質問は次のようになります。どのデータ構造+アルゴリズムを使用すると、比較が少なくなります。

マルチセットのような事前にソートされたコンテナを使用すると、私のテストによると、マルチマップは良くありません。

  1. 挿入中の並べ替えはすでに比較を行っていますが、
  2. 次の隣接する調査結果は、再度比較を行います。
  3. これらのデータ構造は、等しい操作よりも小さい操作を優先し、2より小さい(a

比較を保存する方法がわかりません。


以下のいくつかの回答で無視されているもう1つのことは、重複するグループをコンテナに保持するだけでなく、互いに区別する必要があるということです。


結論

すべての議論の後、3つの方法があるようです

  1. 上で説明した私の元の素朴な方法
  2. std::vectorのような線形コンテナから始めて、それをソートしてから、等しい範囲を見つけます
  3. std::map<Type, vector<duplicates>>のような関連コンテナから始め、Charles Baileyによって提案されたように、関連コンテナのセットアップ中に重複を選択します。

以下に示すように、すべてのメソッドをテストするためのサンプルをコーディングしました。

重複の数とそれらがいつ配布されるかが、最良の選択に影響を与える可能性があります。

  • 方法1は、前に大きく倒れたときに最適で、最後に倒れたときに最悪です。並べ替えは分布を変更しませんが、エンディアンを変更します。
  • 方法3のパフォーマンスは最も平均的です
  • 方法2は決して最良の選択ではありません

ディスカッションに参加してくれたすべての人に感謝します。

以下のコードからの20のサンプルアイテムを含む1つの出力。

[20 10 6 5 4 3 2 2 2 2 1 1 1 1 1 1 1 1 11]でテストします

および[11 1 1 1 1 1 1 1 1 2 2 2 2 3 4 5 6 1020]それぞれ

std :: vector-> sort()-> neighbor_find()の使用:

比較:['<' = 139、 '==' = 23]

比較:['<' = 38、 '==' = 23]

std :: list-> sort()->縮小リストの使用:

比較:['<' = 50、 '==' = 43]

比較:['<' = 52、 '==' = 43]

std :: list->縮小リストの使用:

比較:['<' = 0、 '==' = 121]

比較:['<' = 0、 '==' = 43]

std :: vector-> std :: map>:を使用する

比較:['<' = 79、 '==' = 0]

比較:['<' = 53、 '==' = 0]

std :: vector-> std :: multiset-> neighbor_find()の使用:

比較:['<' = 79、 '==' = 7]

比較:['<' = 53、 '==' = 7]

コード

// compile with VC++10: cl.exe /EHsc

#include <vector>
#include <deque>
#include <list>
#include <map>
#include <set>
#include <algorithm>
#include <iostream>
#include <sstream>

#include <boost/foreach.hpp>
#include <boost/Tuple/tuple.hpp>
#include <boost/format.hpp>

using namespace std;

struct Type
{
    Type(int i) : m_i(i){}

    bool operator<(const Type& t) const
    {
        ++number_less_than_comparison;
        return m_i < t.m_i;
    }

    bool operator==(const Type& t) const
    {
        ++number_equal_comparison;    
        return m_i == t.m_i;
    }
public:
    static void log(const string& operation)
    {
        cout 
        << "comparison during " <<operation << ": [ "
        << "'<'  = " << number_less_than_comparison
        << ", "
        << "'==' = " << number_equal_comparison
        << " ]\n";

        reset();
    }

    int to_int() const
    {
        return m_i;
    }
private:
    static void reset()
    {
        number_less_than_comparison = 0;
        number_equal_comparison = 0;      
    }

public:
    static size_t number_less_than_comparison;
    static size_t number_equal_comparison;    
private:
    int m_i;
};

size_t Type::number_less_than_comparison = 0;
size_t Type::number_equal_comparison = 0;  

ostream& operator<<(ostream& os, const Type& t) 
{
    os << t.to_int();
    return os;
}

template< class Container >
struct Test
{    
    void recursive_run(size_t n)
    { 
        bool reserve_order = false;

        for(size_t i = 48; i < n; ++i)
        {
            run(i);
        }    
    }

    void run(size_t i)
    {
        cout << 
        boost::format("\n\nTest %1% sample elements\nusing method%2%:\n") 
        % i 
        % Description();

        generate_sample(i);
        sort();
        locate();   

        generate_reverse_sample(i);
        sort();
        locate(); 
    }

private:    
    void print_me(const string& when)
    {
        std::stringstream ss;
        ss << when <<" = [ ";
        BOOST_FOREACH(const Container::value_type& v, m_container)
        {
            ss << v << " ";
        }
        ss << "]\n";    
        cout << ss.str();
    }

    void generate_sample(size_t n)
    {
        m_container.clear();
        for(size_t i = 1; i <= n; ++i)
        {
            m_container.Push_back(Type(n/i));    
        }
        print_me("init value");
        Type::log("setup");
    }

    void generate_reverse_sample(size_t n)
    {
        m_container.clear();
        for(size_t i = 0; i < n; ++i)
        {
            m_container.Push_back(Type(n/(n-i)));     
        }
        print_me("init value(reverse order)");
        Type::log("setup");
    }    

    void sort()
    {    
        sort_it();

        Type::log("sort");
        print_me("after sort");

    }

    void locate()
    {
        locate_duplicates();

        Type::log("locate duplicate");
    }
protected:
    virtual string Description() = 0;
    virtual void sort_it() = 0;
    virtual void locate_duplicates() = 0;
protected:
    Container m_container;    
};

struct Vector : Test<vector<Type> >
{    
    string Description()
    {
        return "std::vector<Type> -> sort() -> adjacent_find()";
    } 

private:           
    void sort_it()
    {    
        std::sort(m_container.begin(), m_container.end()); 
    }

    void locate_duplicates()
    {
        using std::adjacent_find;
        typedef vector<Type>::iterator ITR;
        typedef vector<Type>::value_type  VALUE;

        typedef boost::Tuple<VALUE, ITR, ITR> Tuple;
        typedef vector<Tuple> V_Tuple;

        V_Tuple results;

        ITR itr_begin(m_container.begin());
        ITR itr_end(m_container.end());       
        ITR itr(m_container.begin()); 
        ITR itr_range_begin(m_container.begin());  

        while(itr_begin != itr_end)
        {     
            // find  the start of one equal reange
            itr = adjacent_find(
            itr_begin, 
            itr_end, 
                []  (VALUE& v1, VALUE& v2)
                {
                    return v1 == v2;
                }
            );
            if(itr_end == itr) break; // end of container

            // find  the end of one equal reange
            VALUE start = *itr; 
            while(itr != itr_end)
            {
                if(!(*itr == start)) break;                
                itr++;
            }

            results.Push_back(Tuple(start, itr_range_begin, itr));

            // prepare for next iteration
            itr_begin = itr;
        }  
    }
};

struct List : Test<list<Type> >
{
    List(bool sorted) : m_sorted(sorted){}

    string Description()
    {
        return m_sorted ? "std::list -> sort() -> shrink list" : "std::list -> shrink list";
    }
private:    
    void sort_it()
    {
        if(m_sorted) m_container.sort();////std::sort(m_container.begin(), m_container.end()); 
    }

    void locate_duplicates()
    {       
        typedef list<Type>::value_type VALUE;
        typedef list<Type>::iterator ITR;

        typedef vector<VALUE>  GROUP;
        typedef vector<GROUP>  GROUPS;

        GROUPS sub_groups;
        GROUP one_group; 

        while(m_container.size() > 1)
        {
            VALUE front(m_container.front());
            m_container.pop_front();

            ITR it     = m_container.begin(); 
            ITR it_end = m_container.end();
            while(it != it_end)
            {
                if(front == (*it))
                {
                    one_group.Push_back(*it); // single it out
                    it = m_container.erase(it); // shrink list by one
                }
                else
                {
                    it++;
                }
            }

            // save results
            if(!one_group.empty())
            {
                // save
                one_group.Push_back(front);                    
                sub_groups.Push_back(one_group);

                // clear, memory allocation not freed
                one_group.clear(); 
            }            
        }
    }        

private:
    bool m_sorted;
};

struct Map : Test<vector<Type>>
{    
    string Description()
    {
        return "std::vector -> std::map<Type, vector<Type>>" ;
    }
private:    
    void sort_it() {}

    void locate_duplicates()
    {
        typedef map<Type, vector<Type> > MAP;
        typedef MAP::iterator ITR;

        MAP local_map;

        BOOST_FOREACH(const vector<Type>::value_type& v, m_container)
        {
            pair<ITR, bool> mit; 
            mit = local_map.insert(make_pair(v, vector<Type>(1, v)));   
            if(!mit.second) (mit.first->second).Push_back(v); 
         }

        ITR itr(local_map.begin());
        while(itr != local_map.end())
        {
            if(itr->second.empty()) local_map.erase(itr);

            itr++;
        }
    }        
};

struct Multiset :  Test<vector<Type>>
{
    string Description()
    {
        return "std::vector -> std::multiset<Type> -> adjacent_find()" ;
    }
private:
    void sort_it() {}

    void locate_duplicates()
    {   
        using std::adjacent_find;

        typedef set<Type> SET;
        typedef SET::iterator ITR;
        typedef SET::value_type  VALUE;

        typedef boost::Tuple<VALUE, ITR, ITR> Tuple;
        typedef vector<Tuple> V_Tuple;

        V_Tuple results;

        SET local_set;
        BOOST_FOREACH(const vector<Type>::value_type& v, m_container)
        {
            local_set.insert(v);
        }

        ITR itr_begin(local_set.begin());
        ITR itr_end(local_set.end());       
        ITR itr(local_set.begin()); 
        ITR itr_range_begin(local_set.begin());  

        while(itr_begin != itr_end)
        {     
            // find  the start of one equal reange
            itr = adjacent_find(
            itr_begin, 
            itr_end, 
            []  (VALUE& v1, VALUE& v2)
                {
                    return v1 == v2;
                }
            );
            if(itr_end == itr) break; // end of container

            // find  the end of one equal reange
            VALUE start = *itr; 
            while(itr != itr_end)
            {
                if(!(*itr == start)) break;                
                itr++;
            }

            results.Push_back(Tuple(start, itr_range_begin, itr));

            // prepare for next iteration
            itr_begin = itr;
        }  
    } 
};

int main()
{
    size_t N = 20;

    Vector().run(20);
    List(true).run(20);
    List(false).run(20);
    Map().run(20);
    Multiset().run(20);
}
22
t.g.

代表的な要素から他の要素のリスト/ベクトル/両端キューへのマップを使用できます。これにより、コンテナへの挿入に必要な比較が比較的少なくなり、比較を実行しなくても、結果のグループを反復処理できるようになります。

この例では、グループ全体の後続の反復が論理的に単純になるため、常に最初の代表的な要素をマップされた両端キューストレージに挿入しますが、この重複が問題であることが判明した場合は、_Push_back_のみif (!ins_pair.second)

_typedef std::map<Type, std::deque<Type> > Storage;

void Insert(Storage& s, const Type& t)
{
    std::pair<Storage::iterator, bool> ins_pair( s.insert(std::make_pair(t, std::deque<Type>())) );
    ins_pair.first->second.Push_back(t);
}
_

グループを反復処理することは、(比較的)簡単で安価です。

_void Iterate(const Storage& s)
{
    for (Storage::const_iterator i = s.begin(); i != s.end(); ++i)
    {
        for (std::deque<Type>::const_iterator j = i->second.begin(); j != i->second.end(); ++j)
        {
            // do something with *j
        }
    }
}
_

比較とオブジェクト数のためにいくつかの実験を行いました。 50000グループを形成するランダムな順序で100000個のオブジェクトを使用したテスト(つまり、グループごとに平均2個のオブジェクト)では、上記の方法で次の数の比較とコピーが必要になります。

_1630674 comparisons, 443290 copies
_

(私はコピーの数を減らしてみましたが、実際には比較を犠牲にして成功しました。これは、シナリオではよりコストのかかる操作のようです。)

マルチマップを使用し、最後の反復で前の要素を保持してグループ遷移を検出すると、次のコストがかかります。

_1756208 comparisons, 100000 copies
_

単一のリストを使用してフロント要素をポップし、他のグループメンバーの線形検索を実行するには、次のコストがかかります。

_1885879088 comparisons, 100000 copies
_

はい、それは私の最良の方法の約1.6mと比較して、約1.9bの比較です。 listメソッドを最適な数の比較に近い場所で実行するには、並べ替える必要があります。これには、最初に本質的に順序付けられたコンテナーを構築する場合と同様の数の比較が必要になります。

編集

私はあなたの投稿されたコードを取り、以前に使用したのと同じテストデータセットに対して暗黙のアルゴリズムを実行しました(いくつかの仮定された定義があるので、コードについていくつかの仮定をしなければなりませんでした)。

_1885879088 comparisons, 420939 copies
_

つまり、ダムリストアルゴリズムとまったく同じ数の比較ですが、コピー数が多くなります。これは、この場合、本質的に同様のアルゴリズムを使用することを意味すると思います。別の並べ替え順序の証拠は見当たりませんが、複数の同等の要素を含むグループのリストが必要なようです。これは、if (i->size > 1)句を追加することで、私のIterate関数で簡単に実現できます。

この両端キューのマップのようなソートされたコンテナを構築することが(最適ではないにしても)良い戦略ではないという証拠はまだ見当たりません。

3
CB Bailey

はい、あなたははるかに良くすることができます。

  1. それらをソートし(単純な整数の場合はO(n)、一般にO(n * log n))、重複が隣接していることが保証されるため、すばやく見つけることができますO(n)

  2. ハッシュテーブルとO(n)を使用します。各項目について、(a)それがすでにハッシュテーブルにあるかどうかを確認します。もしそうなら、それは重複しています。そうでない場合は、ハッシュテーブルに入れます。

編集

使用しているメソッドは、O(N ^ 2)の比較を行っているようです。

for i = 0; i < length; ++i           // will do length times
    for j = i+1; j < length; ++j     // will do length-i times
        compare

したがって、長さ5の場合、4 + 3 + 2 + 1 = 10の比較を行います。 6の場合、15などを実行します。(N ^ 2)/ 2-N/2正確には。 Nの値が適度に高い場合、N * log(N)は小さくなります。

あなたの場合、Nはどのくらいの大きさですか?

ハッシュの衝突を減らす限り、最良の方法はより良いハッシュ関数を取得することです:-D。それが不可能であると仮定すると、バリアント(たとえば、異なるmodulous)を作成できる場合は、ネストされたハッシュを実行できる可能性があります。

13
derobert

1。最悪の場合、配列O(n log n)をソートします-マージソート/ヒープソート/バイナリツリーソートなど

2。ネイバーを比較し、一致するものを引き出しますO(n)

7
Nathan

値からカウントまでハッシュテーブルベースの構造を維持します。 C++実装がstd::hash_mapを提供しない場合(これまでのところ、実際にはC++標準の一部ではありません!-)Boostを使用するか、Webからバージョンを入手してください。コレクションを1回パスすると(つまり、O(N))は、値->カウントのマッピングを実行できます。ハッシュテーブルをもう1回パスすると(<= O(N)、明らかに)カウントが1より大きい値を特定し、適切に出力します。全体的なO(N)。これはあなたの提案には当てはまりません。

5
Alex Martelli

並べ替えてみましたか?たとえば、クイックソートのようなアルゴリズムを使用していますか?パフォーマンスが十分に良い場合、それは簡単なアプローチです。

1
hhafez

整数のリストであることがわかっていて、それらがすべてAとBの間にあることがわかっている場合(たとえば、A = 0、B = 9)、B-A要素の配列を作成し、B-Aコンテナーを作成します。

非常に特殊なケース(単純な整数のリスト)では、とにかく異なる整数を区別できないため、単にそれらを数えることをお勧めします。

for(int i = 0; i < countOfNumbers; i++)
    counter[number[i]]++;

それらがare区別できる場合は、リストの配列を作成し、それぞれのリストに追加します。

数値でない場合は、std :: mapまたはstd :: hash_mapを使用して、キーを値のリストにマッピングします。

1

最も簡単なのは、おそらくリストを並べ替えてから、リストを繰り返して重複を探すことです。

データについて何か知っている場合は、より効率的なアルゴリズムが可能です。

たとえば、リストが大きく、nがかなり小さい1..nの整数のみが含まれていることがわかっている場合は、ブール配列(またはビットマップ)のペアを使用して、次のようにすることができます。

bool[] once = new bool[MAXIMUM_VALUE];
bool[] many = new bool[MAXIMUM_VALUE];
for (int i = 0; i < list.Length; i++)
    if (once[ value[i] ])
        many[ value[i] ] = true;
    else
        once[ value[i] ] = true;

現在、many []には、値が複数回表示された配列が含まれています。

0
lavinio

私が質問を正しく理解していれば、これは私が考えることができる最も簡単な解決策です:

_std::vector<T> input;
typedef std::vector<T>::iterator iter;

std::vector<std::pair<iter, iter> > output;

sort(input.begin(), input.end()); // O(n lg n) comparisons

for (iter cur = input.begin(); cur != input.end();){
  std::pair<iter, iter> range = equal_range(cur, input.end(), *cur); // find all elements equal to the current one O(lg n) comparisons
  cur = range.second;
  output.Push_back(range);
}
_

合計実行時間:O(n log n)。 1つの並べ替えパスO(n lg n)があり、次にO(lg n)比較が実行される2番目のパスがありますグループごとに(これは実行されます最大でn回、またO(n lg n)を生成します。

これは、入力がベクトルであることに依存することに注意してください。ランダムアクセスイテレータのみが、2番目のパスで対数的に複雑になります。双方向イテレータは線形になります。

これは(要求に応じて)ハッシュに依存せず、(グループごとに1つの要素を、発生頻度とともに返すのではなく)すべての元の要素を保持します。

もちろん、いくつかの小さな定数の最適化が可能です。再割り当てを避けるために、出力ベクトルのoutput.reserve(input.size())は良い考えです。 input.end()も必要以上に頻繁に使用され、簡単にキャッシュできます。

グループの大きさによっては、_equal_range_が最も効率的な選択ではない場合があります。対数の複雑さを取得するために二分探索を行うと思いますが、各グループが2、3の要素しかない場合は、単純な線形スキャンの方が高速でした。いずれにせよ、最初のソートがコストを支配します。

0
jalf

ハッシュ/順序付けされていないマップソリューションに言及するほとんどの人は、O(1)挿入とクエリの時間を想定していますが、O(N)最悪の場合です。さらに、オブジェクトハッシュのコストを無効にします。

個人的には、オブジェクトを二分木に挿入し(それぞれにO(logn)挿入)、各ノードにカウンターを保持します。これにより、O(nlogn)構築時間、およびO(n)トラバーサルが行われ、すべての重複が識別されます。

0
ttvd

C++ 11以降、ハッシュテーブルはSTLによって std :: unordered_map で提供されます。したがって、O(N)の解決策は、値をunordered_map< T, <vector<T> >に入れることです。

0
Ismael EL ATIFI

作業中のトリプルストアの正規化中に同じ問題が発生したことを登録するだけです。 Allegro Common LISPのハッシュテーブル機能を使用して、CharlesBaileyによって要約されたメソッド3をCommonLISPに実装しました。

関数「agent-equal?」 TS内の2つのエージェントが同じであるかどうかをテストするために使用されます。関数「merge-nodes」は、各クラスターのノードをマージします。以下のコードでは、「...」を使用して、それほど重要ではない部分を削除しています。

(defun agent-equal? (a b)
 (let ((aprops (car (get-triples-list :s a :p !foaf:name)))
       (bprops (car (get-triples-list :s b :p !foaf:name))))
   (upi= (object aprops) (object bprops))))

(defun process-rdf (out-path filename)
 (let* (...
        (table (make-hash-table :test 'agent-equal?)))
  (progn 
   ...
   (let ((agents (mapcar #'subject 
          (get-triples-list :o !foaf:Agent :limit nil))))
     (progn 
       (dolist (a agents)
          (if (gethash a table)
            (Push a (gethash a table))
            (setf (gethash a table) (list a))))
       (maphash #'merge-nodes table)
           ...
           )))))
0