каковы быстрые алгоритмы поиска повторяющихся элементов в коллекции и их группировки?

скажем, у вас есть коллекция элементов, как вы можете выбрать те, с дубликатами и поместить их в каждую группу с наименьшим количеством сравнения? предпочтительно на C++, но алгоритм важнее языка. например учитывая {E1,E2,E3,E4,E4,E2,E6,E4, E3}, я хочу извлечь {E2, E2}, {E3, E3}, {E4,E4, E4}. какую структуру данных и алгоритм вы выберете? Пожалуйста, также включите стоимость настройки структуры данных, скажем, если она предварительно отсортирована, как std:: multimap

обновления

чтобы сделать вещи яснее, как было предложено. есть одно ограничение: элементы должны сравниваться сами по себе чтобы быть уверенным, что они дубликаты.

поэтому хэши не применяются, потому что фактически они смещают сравнение с тяжелых элементов (например, куски данных) на легкие элементы (целые числа) и уменьшают некоторое сравнение, но не избавляются от них, и в конце концов мы возвращаемся к нашей первоначальной проблеме, когда внутри одно ведро столкновения.

представьте, что у вас есть куча потенциалов, дублирующих файлы GBs каждый, они несут одно и то же значение хэша каждым хэш-алгоритмом, который знают люди. Теперь вы обнаружите настоящие дубликаты.

нет, это не может быть реальной проблемой(даже MD5 достаточно для создания уникального хэша для реальных файлов). Но только притворись, что мы можем!--9-->сосредоточиться на поиске структуры данных алгоритм+, который включает в себя наименьшее количество сравнение.


что я делаю это

  1. представляют в структуру данных STL std::list(в том, что 1) его элемент-удаление дешевле, чем, скажем, вектор 2) его вставка дешевле, не требуя сортировки.)

  2. выскочить один элемент и сравнить его с остальными, если найден дубликат, он вытащил из списка. по достижении конца списка обнаруживается одна группа дублирования, если любой.

  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
}; 


};

Спасибо за ответ ниже, однако они, похоже, введены в заблуждение моим примером, что речь идет о целых числах. На самом деле элементы типа agnostic(не обязательно целые числа, строки или любые другие стручки), и равные предикаты являются самоопределяющимися, то есть сравнение может быть очень тяжелым.

поэтому, возможно, мой вопрос должен быть: использование какой структуры данных + алгоритм включает в себя меньше сравнений.

используя предварительно отсортированный контейнер, такой как multiset, multimap не лучше в соответствии с моим тестом, так как

  1. сортировка при вставке уже делает сравнения,
  2. следующая смежная находка снова выполняет сравнение,
  3. эти структуры данных предпочитают операции меньше, чем равные операции, они выполняют 2 меньше, чем (a

Я не вижу, как он может сохранить сравнения.


еще одна вещь, которая игнорируется некоторыми ответами ниже, мне нужно отличать дубликаты групп друг от друга, а не просто держать их в контейнере.


вывод

после всего обсуждения, кажется, есть 3 способа

  1. мой оригинальный наивный метод, как описано выше
  2. начните с линейного контейнера, такого как std::vector , отсортировать его, а затем найдите равные диапазоны
  3. начните с связанного контейнера, такого как std::map<Type, vector<duplicates>>, выберите дубликаты во время установки связанного контейнера, как предложил Чарльз Бейли.

я закодировал образец для проверьте все методы, как опубликовано ниже.

количество дубликатов и когда они распределены могут повлиять на лучший выбор.

  • метод 1 лучше всего, когда они сильно падают спереди, и хуже всего, когда в конце. Сортировка не изменит распределение, но endian.
  • Метод 3 имеет самую среднюю производительность
  • Способ 2 не лучший выбор

спасибо всем, кто участвует в обсуждение.

один выход с 20 деталями образца от кода ниже.

тест с [ 20 10 6 5 4 3 2 2 2 2 1 1 1 1 1 1 1 1 1 1 ]

и [ 1 1 1 1 1 1 1 1 1 1 2 2 2 2 3 4 5 6 10 20 ] соответственно

использование std:: vector - > sort () - > adjacent_find ():

сравнения: [ '

сравнения: [ '

использование std:: список - > сортировка() -> уменьшаться список:

сравнения: [ '

сравнения: [ '

использование std:: list - > shrink list:

сравнения: [ '

сравнения: [ '

использование std:: vector - > std:: map>:

сравнения: [ '

сравнения: [ '

использование std:: vector - > std:: multiset - > adjacent_find ():

сравнения: [ '

сравнения: [ '

код

// 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("nnTest %1% sample elementsnusing 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);
}

11 ответов


вы можете использовать карту из репрезентативного элемента в список / вектор / Деку других элементов. Это требует относительно меньшего количества сравнений при вставке в контейнер и означает, что вы можете перебирать результирующие группы без необходимости выполнять какие-либо сравнения.

этот пример всегда вставляет первый репрезентативный элемент в сопоставленное хранилище deque, поскольку это делает последующую итерацию через группу логически простой, но если это дублирование доказывает проблемой тогда было бы легко выполнить только 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
        }
    }
}

я провел несколько экспериментов для сравнения и количества предметов. В тесте со 100000 объектами в случайном порядке, образующими 50000 групп (т. е. в среднем по 2 объекта на группу) вышеуказанный метод стоил следующего количества сравнений и копий:

1630674 comparisons, 443290 copies

(я попытался принести количество копий вниз, но только действительно удалось за счет сравнений, которые кажутся более высокой стоимостью операции в вашем сценарии.)

использование multimap и сохранение предыдущего элемента на последней итерации для обнаружения групповых переходов стоит этого:

1756208 comparisons, 100000 copies

использование одного списка и выскакивание переднего элемента и выполнение линейного поиска для других членов группы стоимость:

1885879088 comparisons, 100000 copies

Да ,это ~ 1.9 B сравнения по сравнению с ~1.6 m для моего лучшего метода. Чтобы получить метод list для выполнения где-либо рядом с оптимальным количеством сравнений, он должен быть отсортирован, и это будет стоить аналогичного количества сравнений, как создание изначально упорядоченного контейнера в первую очередь.

редактировать

я взял ваш опубликованный код и запустил подразумеваемый алгоритм (я должен был сделать некоторые предположения о коде, как там, как некоторые предполагаемые определения) над тем же набором тестовых данных, что и я использовал раньше и я считал:

1885879088 comparisons, 420939 copies

т. е. точно такое же количество сравнений, как и мой алгоритм тупого списка, но с большим количеством копий. Я думаю, что это означает, что мы используем по существу аналогичные алгоритмы для этого случая. Я не вижу никаких доказательств альтернативного порядка сортировки, но похоже, вам нужен список групп, которые содержат более одного эквивалентного элемента. Это может быть просто достигнуто в мой на if (i->size > 1) предложения.

я все еще не вижу доказательство того, что создание отсортированного контейнера, такого как эта карта deques, не является хорошей (даже если не оптимальной) стратегией.


Да, вы можете сделать намного лучше.

  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 * log (N) меньше, для любого разумно высокого значения N.

насколько велика N в вашем случае?

http://img188.imageshack.us/img188/7315/foou.png

что касается уменьшения хэш-коллизий, лучший способ-получить лучшую хэш-функцию : - D. предполагая, что это невозможно, если вы можете сделать вариант (например, другой modulous), вы можете сделать вложенный хэш.


1. Сортировка массива O (N log n) в худшем случае-сортировка дерева mergesort/heapsort/binary и т. д.

2. Сравните соседей и вытащите спички из O (n)


держите структуру на основе хэш-таблицы от значения до счета; если ваша реализация c++ не предлагает std::hash_map (пока не является частью стандарта C++!- ) используйте Boost или захватить версию из интернета. Один проход над коллекцией(т. е. O (N)) позволяет выполнить сопоставление value->count; еще один проход над хэш-таблицей ( 1 и излучать их соответствующим образом. Общий O (N), что не относится к вашему предложению.


вы пробовали сортировать? Например, используя алгоритм, такой как quick sort? Если производительность достаточно хороша для вас, то это будет простой подход.


если известно, что это список целых чисел, и если известно, что все они находятся между A и B (например, A=0, B=9), создайте массив элементов B-A и создайте контейнеры B-A.

в очень конкретном случае (список простых целых чисел) я предлагаю вам просто подсчитать их, так как вы все равно не можете различать разные целые числа:

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

если они are distinguishable, создайте массив списков и добавьте их в соответствующий список.

Если это не числа, используйте std::map или std:: hash_map, сопоставляя ключи со списками значений.


самый простой, вероятно, просто отсортировать список, а затем перебирать его в поисках dups.

Если вы что-то знаете о данных, возможны более эффективные алгоритмы.

например, если вы знали, что список большой и содержит только целые числа от 1..N где 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;

теперь многие[] содержат массив, значения которого были видны больше, чем однажды.


большинство людей, упоминающих хэш / неупорядоченные решения карты, предполагают O(1) вставку и время запроса, однако это может быть O (N) в худшем случае. Кроме того, вы аннулируете стоимость хэширования объектов.

лично я бы вставлял объекты в двоичное дерево (вставка O (logn) для каждого) и держал счетчик на каждом узле. Это даст O(nlogn) время построения и o (n) обход для идентификации всех дубликатов.


если я правильно понял вопрос, то это самое простое решение я могу думать:

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). У вас есть один сортировочный пропуск O(n lg n) и затем второй проход, где O(lg n) сравнения выполненного для каждой группы (так это делается максимум n раз, тоже приносят O(n lg n).

обратите внимание, что это зависит от входных векторов. Только итераторы произвольного доступа имеют логарифмические сложность во втором проходе. Двунаправленные итераторы будут линейными.

это не зависит от хэширования (по запросу), и он сохраняет все исходные элементы (а не просто возвращает один элемент для каждой группы вместе с подсчетом того, как часто это происходило).

конечно, возможно несколько меньших постоянных оптимизаций. output.reserve(input.size()) на выходном векторе было бы хорошей идеей, чтобы избежать перераспределения. input.end() берется гораздо чаще, чем это необходимо, и можно легко кэшировать.

в зависимости от того, насколько большими считаются группы,equal_range может быть не самый эффективный выбор. Я предполагаю, что он выполняет двоичный поиск, чтобы получить логарифмическую сложность, но если каждая группа состоит только из нескольких элементов, простое линейное сканирование было бы быстрее. В любом случае, первоначальная сортировка доминирует над стоимостью.


просто зарегистрировать, что у меня была такая же проблема во время нормализации тройного магазина, с которым я работаю. Я реализовал метод 3, обобщенный Чарльзом Бейли, в Common Lisp, используя функцию хэш-таблицы из Allegro Common Lisp.

функция " агент-равный?"используется для проверки, когда два агента в TS одинаковы. Функция "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)
           ...
           )))))

начиная с C++11, хэш-таблицы предоставляются STL с std:: unordered_map. Таким образом, решение O(N) заключается в том, чтобы поместить ваши значения в unordered_map< T, <vector<T> >.