Каковы быстрые алгоритмы, чтобы найти дубликаты элементов в коллекции и сгруппировать их? - PullRequest
22 голосов
/ 26 августа 2009

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

Обновление

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

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

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

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


Что я делаю, это

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


};

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

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

При использовании предварительно отсортированного контейнера, такого как мультимножество, multimap не лучше в соответствии с моим тестом, так как

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

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


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


Заключение

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

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

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

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

  • Метод 1 лучше всего подходит, когда они сильно падают спереди, и хуже, когда он в конце. Сортировка не изменит распределение, но будет иметь порядок байтов.
  • Метод 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 ():

сравнения: ['<' = 139, '==' = 23 ] </p>

сравнения: ['<' = 38, '==' = 23] </p>

используя std :: list -> sort () -> shrink список:

сравнений: ['<' = 50, '==' = 43] </p>

сравнений: ['<' = 52, '==' = 43] </p>

используя std :: list -> shrink list:

сравнений: ['<' = 0, '==' = 121] </p>

сравнений: ['<' = 0, '==' = 43] </p>

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

сравнения: ['<' = 79, '==' = 0] </p>

сравнения: ['<' = 53, '==' = 0] </p>

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

сравнений: ['<' = 79, '==' = 7] </p>

сравнений: ['<' = 53, '==' = 7] </p>

Код

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

Ответы [ 11 ]

13 голосов
/ 26 августа 2009

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

  1. Сортируйте их (O (n) для простых целых чисел, O (n * log n) в целом), тогда дубликаты гарантированно будут соседними, что делает их поиск быстрым O (n)

  2. Используйте хеш-таблицу, также O (n). Для каждого элемента: (а) проверьте, находится ли он уже в хэш-таблице; если так, это дубликат; если нет, поместите его в хеш-таблицу.

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

Метод, который вы используете, похоже, выполняет 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 в вашем случае?

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

7 голосов
/ 26 августа 2009

1. Сортировать массив O (n log n) в худшем случае - сортировка слиянием / сортировка по папкам / сортировка двоичного дерева и т. Д.

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

5 голосов
/ 26 августа 2009

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

3 голосов
/ 26 августа 2009

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

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

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

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

1756208 comparisons, 100000 copies

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

1885879088 comparisons, 100000 copies

Да, это ~ 1,9b сравнений по сравнению с ~ 1,6 м для моего лучшего метода. Чтобы заставить метод list выполнить любое количество сравнений, близкое к оптимальному, его нужно отсортировать, и это будет стоить того же количества сравнений, что и создание изначально упорядоченного контейнера.

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

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

1885879088 comparisons, 420939 copies

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

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

1 голос
/ 26 августа 2009

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

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

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

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

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

1 голос
/ 26 августа 2009

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

0 голосов
/ 27 мая 2016

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

0 голосов
/ 13 июня 2011

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

Функция "агент-равно?" используется для проверки, когда два агента в TS одинаковы. Функция «объединить узлы» объединяет узлы в каждом кластере. В приведенном ниже коде «...» был использован для удаления не столь важных частей.

(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 голосов
/ 27 августа 2009

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

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 может быть не самым эффективным выбором. Я предполагаю, что он выполняет двоичный поиск, чтобы получить логарифмическую сложность, но если бы каждая группа состояла всего из пары элементов, простое линейное сканирование было бы быстрее. В любом случае, первоначальная сортировка преобладает над стоимостью.

0 голосов
/ 26 августа 2009

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

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

...