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

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

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

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

  1. сортировка при вставке уже выполняет сравнения,
  2. следующий соседний вывод снова делает сравнение,
  3. these data structure prefer less-than operations to equal operations, they perform 2 less-than(a

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


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


Вывод

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

  1. my original naive method as explained above
  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() -> смежный_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 -> смежный_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);
}

person Community    schedule 26.08.2009    source источник
comment
Я думаю, вам нужно немного лучше описать цель вашего алгоритма. Учитывая, что цель состоит в том, чтобы закончить пустой список, не будет ли отбрасывание всего ввода самым быстрым и простым решением?   -  person CB Bailey    schedule 26.08.2009
comment
вход — это коллекция, содержащая возможные повторяющиеся элементы, а выходы — ноль или более коллекций, каждая из которых содержит дубликаты одного и того же типа. (Я предпочитаю не использовать список терминов, потому что это может подразумевать некоторую реализацию, например, std::list )   -  person t.g.    schedule 26.08.2009
comment
В таком случае, почему предварительно отсортированный контейнер (например, мультисет) не является решением? то есть пост-редактирование, почему вы подразумеваете, что вам нужно сделать смежный поиск. Конечно, как только контейнер заполнен, вы просто выводите его по порядку, никаких сравнений не требуется?   -  person CB Bailey    schedule 26.08.2009
comment
требуется смежное обнаружение, потому что может быть несколько повторяющихся групп. например, в предварительно отсортированном контейнере {E1, E2, E2, E4, E4, E5} мне нужно выделить {E2, E2} и {E4, E4}. без соседней находки, я не знаю, где начинаются и заканчиваются E2 и E4,   -  person t.g.    schedule 26.08.2009
comment
Какой тогда требуется выходной интерфейс? Вам нужно вывести структурированный объект коллекции или вы выводите значение для '{', ',' и '}'? Являются ли «E2» и «E2» идентичными или просто эквивалентными?   -  person CB Bailey    schedule 26.08.2009
comment
эквивалент. а проверка их эквивалентности — дорогостоящие операции, которые могут занять десятки секунд. выходной интерфейс не имеет значения, пока информация извлекается. В моей версии реализации std::multimap я просто сохранил std::multimap::iterators, представляющий диапазон. Потому что с std::multimap происходит сравнение при вставке и соседнем поиске. Я переключился на несортированный контейнер std::list в надежде сохранить сравнения. Но ни значительного улучшения, ни снижения. Мне нужен контейнер, ограниченный алгоритмом с наименьшим сравнением.   -  person t.g.    schedule 26.08.2009
comment
Формат вывода также не имеет значения. Я ставлю '{', ',' и '}' только для того, чтобы объяснить вещи таким образом, чтобы все легко поняли   -  person t.g.    schedule 26.08.2009
comment
Я не думаю, что формат вывода не имеет значения. Я только что описал выходной формат, который представляет собой плоскую итерацию по всей коллекции, которая сигнализирует об изменении в группе путем вывода элемента, который не == предыдущего. Кажется, это не соответствует тому, что вы хотите, поэтому вам нужно объяснить, что должно произойти с этими группами дальше. На данный момент я думаю, что либо мультимножество, либо сопоставление элементов со списком других совпадающих элементов будут соответствовать вашим требованиям и не будут нуждаться во втором шаге сравнения, но ваши требования недостаточно ясны, чтобы быть уверенными.   -  person CB Bailey    schedule 26.08.2009
comment
с мультимножеством вам нужно выполнить сравнение, чтобы отсортировать при построении мультимножества. это 1-й раунд сравнения. И до того, как набор будет полностью построен, то есть все элементы вставлены, вы не можете определить диапазоны для дубликатов. вам нужно подождать, пока все вставки не будут выполнены, теперь вы начинаете повторять набор (следовательно, второй раунд сравнения), чтобы идентифицировать диапазоны. со списком сравнение не требуется при вставке, поэтому сравнение выполняется только при итерации. Но я не уверен, является ли последний лучшим. и в этом суть моего вопроса.   -  person t.g.    schedule 26.08.2009
comment
Я думаю, что понимаю вашу озабоченность, поэтому я опубликовал ответ. Однако поиск диапазонов в мультимножестве будет включать только n-1 сравнений, которые (даже включая более дорогую вставку) по сравнению с удалением эквивалентных элементов по одной группе за раз с помощью линейного поиска по списку не могут быть более дорогими. для многих входов.   -  person CB Bailey    schedule 26.08.2009
comment
Хорошо, я все еще не понял вопроса. Пожалуйста, не могли бы вы обновить пример кода, который реализует тупой алгоритм, который производит нужный вам результат и выделяет, какие операции являются дорогостоящими. Мое текущее понимание состоит в том, что у вас есть более дешевый более слабый порядок и более дорогой более сильный порядок, что вы хотите создать группы, которые эквивалентны более слабому порядку, но с удаленными дубликатами более сильного порядка, но это следует из множества отдельных комментариев, и это было бы хорошо, если бы проблема была сформулирована более точно, и были бы некоторые подсчеты сравнения целей, по которым можно было бы стрелять.   -  person CB Bailey    schedule 27.08.2009
comment
Это не избавит вас от сравнений, но вы почти наверняка должны использовать вектор вместо списка. Промахи кэша болезненны, а идиома Erase/Remove эффективно справляется с удалением. И я не понимаю, зачем вам нужно много вставок после сортировки списка.   -  person jalf    schedule 27.08.2009
comment
@Чарльз Бейли, добавлен код. @jalf, векторы бесполезны, потому что я хочу сжимать контейнер по мере продвижения, чтобы у меня было все меньше и меньше сравнений. с векторами 1) удаление в середине неэффективно, 2) и что еще хуже, итераторы не являются недействительными после удаления в цикле   -  person t.g.    schedule 27.08.2009
comment
@ т.г. Почему вам нужно уменьшить вектор, чтобы избежать сравнений? Просто используйте пару итераторов для обозначения поддиапазона, который необходимо сравнить. Вместо того, чтобы удалять элементы, поменяйте их местами до конца и исключите из этого поддиапазона.   -  person jalf    schedule 27.08.2009
comment
@jalf, потому что элементы случайны. Если есть поддиапазон, то это [begin(),end()).   -  person t.g.    schedule 27.08.2009


Ответы (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
        }
    }
}

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

1630674 comparisons, 443290 copies

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

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

1756208 comparisons, 100000 copies

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

1885879088 comparisons, 100000 copies

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

Изменить

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

1885879088 comparisons, 420939 copies

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

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

person CB Bailey    schedule 26.08.2009
comment
это почти мое решение до того, как я столкнулся со своей проблемой, за исключением того, что я использовал list вместо deque. Здесь возникает вопрос: теперь, когда у вас есть очередь элементов, предварительно обработанных вашим способом, следовательно, они не могут быть представлены никакими другими атрибутами, кроме самих себя (подумайте о файлах, которые уже имеют идентичные хэши), т.е. они могут быть равными или разными, но должны сравниваться сами по себе (что дорого), как вы можете сгруппировать одинаковые, используя минимальное количество сравнений - person t.g.; 26.08.2009
comment
Каждый deque содержит только эквивалентные элементы, вы говорите, что используете разные концепции для == и <? - person CB Bailey; 26.08.2009
comment
Я пропустил это, когда использовал list. Если бы я использовал deque, мне нужно было бы сделать эти дорогостоящие сравнения перед вставкой. Не хорошо. Поскольку сравнения настолько обширны, я изо всех сил старался отложить их (включая способ, который вы рекомендовали). Сейчас наступает судный день, и мне приходится сталкиваться с ним — поэтому я ищу способы сэкономить количество сравнений. - person t.g.; 26.08.2009
comment
Либо проблема более сложная, чем вы предположили в вопросе, либо вы неправильно поняли мой ответ. В моем ответе каждый deque содержит группу эквивалентных элементов. Согласно исходной постановке задачи их заказывать не нужно. Они уже содержат только эквивалентные элементы. Теперь вы, кажется, подразумеваете, что существует второй подпорядок, который различает элементы в каждой группе, по которой вы хотите упорядочить группы. Если да, можете ли вы обновить вопрос, чтобы сделать его более ясным? - person CB Bailey; 26.08.2009
comment
@Charles Bailey, спасибо за интенсивное обсуждение и помощь в прояснении моего вопроса. Я только что обновил вопрос. Что касается вашей конкретной реализации: стоимость сравнения происходит в s.insert(std::make_pair(t, std::deque‹Type›())). На самом деле вы полагаетесь на реализацию предварительной сортировки std::map, которая представляет собой B-дерево, как предложено ttvd на этой странице, что не обязательно требует наименьшего сравнения. Я использовал ваш способ для фильтрации этих очевидных различных элементов. Теперь я нахожусь в деке, чтобы найти настоящие. Ваш тест std::list не похож на мой, так как вы не уменьшаете его при сравнении. - person t.g.; 27.08.2009
comment
Да, я понимаю, где я беру сравнение это. Я беру его при вставке карты, потому что считаю, что это снижает общую стоимость. В моем ответе предполагался единый порядок сортировки (с == и <, полученными из этого порядка). Вам нужно немного яснее понять, что представляют собой два разных порядка сортировки и как они влияют на требуемый результат. - person CB Bailey; 27.08.2009
comment
@Charles Bailey, спасибо за обсуждение, я использовал ваш метод, но не исследовал его эффективность в деталях до обсуждения с вами. - person t.g.; 31.08.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. Предполагая, что это невозможно, если вы можете сделать вариант (например, другой модульный), вы можете сделать вложенный хэш.

person derobert    schedule 26.08.2009
comment
моя проблема в том, что хэши уже равны. Предположим, мы уже находимся в ведре коллизий, и мы собираемся сгруппировать эти элементы. как лучше всего это сделать, если элемент equal-predicate настолько дорог, что я хочу сохранить каждый из них? - person t.g.; 26.08.2009
comment
Спасибо за разработку. В худшем случае (когда нет двух одинаковых элементов), . (N^2)/2 - ожидается N/2. но в лучшем случае (когда все равны) нужен только N-1, потому что он выдергивается после сравнения. Природа сохранения итераторов действительными после стирания элементов во время цикла — еще один фактор, который я учитывал при выборе std::list вместо std::vector. - person t.g.; 27.08.2009

<сильный>1. Сортировка массива O(n log n) в худшем случае - сортировка слиянием/сортировка кучей/сортировка бинарным деревом и т. д.

<сильный>2. Сравните соседей и вытащите совпадения за O(n)

person Nathan    schedule 26.08.2009
comment
это то, что я сделал сначала. с использованием STL std::multimap и std::adjacent_find, хотя и не намного лучше. - person t.g.; 26.08.2009
comment
Мультикарта не даст вам желаемой производительности. Быстрее поместить все элементы в вектор и отсортировать их, когда закончите, чем хранить отсортированную мультикарту, которая должна выполнять балансировку дерева, чтобы все было отсортировано. - person fbrereto; 26.08.2009
comment
@fbrereton, спасибо за эту информацию, я проверю это. Вероятно, я был слишком заинтригован простым в использовании интерфейсом multimap::euqal_range(), прежде чем начал думать дальше. - person t.g.; 27.08.2009
comment
Существует алгоритм equal_range, который работает со всеми контейнерами STL: cplusplus.com/reference/algorithm/equal_range - person fbrereto; 27.08.2009

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

person Alex Martelli    schedule 26.08.2009
comment
Второй проход не нужен. Если он уже есть в хэше, это дубликат. - person derobert; 26.08.2009
comment
@derobert, да, но вы не знаете, сколько раз это будет дублироваться, а спецификации в вопросе требуют этого. Таким образом, второй проход (который не меняет характер решения O (N)) является самым простым подходом! - person Alex Martelli; 26.08.2009

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

person hhafez    schedule 26.08.2009

Самое простое, вероятно, просто отсортировать список, а затем перебрать его в поисках дубликатов.

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

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

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

person lavinio    schedule 26.08.2009

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

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

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

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

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

person Martin v. Löwis    schedule 26.08.2009

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

person Community    schedule 26.05.2016

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

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

person ttvd    schedule 26.08.2009
comment
Вы правы в отношении других ответов, что они игнорируют стоимость построения структуры данных и измеряют только выгоду алгоритма, связанную с конкретной структурой данных. Является ли ваш метод просто реализацией std::map? кажется, что вы с другими, что выбор структуры данных важнее, чем алгоритм пересечения. - person t.g.; 26.08.2009
comment
Верно. Извините, я не могу оставлять комментарии в исходном вопросе. Возможно ли создать пустую коллекцию (список) для каждого типа объектов в вашем списке (один для всех E2, один для всех E3 и т. д.)? Если это так, вы можете линейно пройти свой набор объектов и в зависимости от объекта найти соответствующий список хранения и вставить его туда. Это позволит избежать сравнений, и в зависимости от вашей реализации это может быть реализовано за O (N). - person ttvd; 26.08.2009
comment
Я думаю, вы предлагаете что-то похожее на Чарльза Бейли. Я действительно сделал это как предварительный шаг для группировки почти равных элементов. и тут у меня возникает проблема, я не могу делегировать сравнение никаким другим типам, кроме самих себя, а они конечно дорогие. Поэтому я ищу способ сохранить количество сравнений, так как избавиться от них невозможно. - person t.g.; 26.08.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 может оказаться не самым эффективным выбором. Я предполагаю, что он выполняет двоичный поиск, чтобы получить логарифмическую сложность, но если каждая группа состоит всего из пары элементов, простое линейное сканирование было бы быстрее. В любом случае, первоначальная сортировка доминирует в стоимости.

person jalf    schedule 27.08.2009
comment
После прочтения документа STL я подумал, что ни один контейнер предварительной сортировки не подходит для ситуации, потому что все они, похоже, полагаются на 2 операции сравнения меньше, чем для определения равенства, в то время как одна операция (==) в моем случае в порядке и сравнение - единственное, что я хочу сохранить. - person t.g.; 27.08.2009
comment
Какая предварительная сортировка контейнера? Я использую вектор и сортирую его вручную. - person jalf; 27.08.2009
comment
кажется, что sort() относится к той категории, которая полагается на операции less_than, отсюда и мои комментарии. извините, что не ясно выразился. - person t.g.; 27.08.2009
comment
Да, он полагается на меньше чем, но не использует эту операцию для эмуляции тестов на равенство. Он использует меньше, чем потому что ему нужно выполнять меньше операций. Вам нужна операция меньше, чем для сортировки элементов. Опять же, он не используется дважды для имитации равенства. На самом деле, я бы хотел, чтобы вы показали мне алгоритм сортировки, любой алгоритм сортировки, основанный на ==. ;) - person jalf; 27.08.2009
comment
вполне разумно полагаться на меньшее. И именно по этой причине сортировка в моем случае излишня :-) - person t.g.; 27.08.2009
comment
Перебор? Это меньше сравнений, чем вам нужно для работы с несортированным диапазоном. Как бы вы нашли повторяющиеся элементы без сортировки? - person jalf; 27.08.2009
comment
если вы хотите только сказать, что два элемента (a, b) равны или нет, лучше предоставить оператор ==(), чем оператор‹(), потому что вы можете проверить if(a==b), а не if (! (а‹б) && !(б‹а)). В этом смысле «меньше чем» — это излишество. - person t.g.; 27.08.2009
comment
Как бы вы нашли повторяющиеся элементы без сортировки? -- Я использую семантику равного вместо меньшего или большего чем. - person t.g.; 27.08.2009
comment
Возможно, вам следует прочитать об алгоритмах сортировки. Дело в том, что если вы сортируете список, вам никогда не нужно проверять равенство. Элементы группируются автоматически, используя только семантику меньше, чем. Если вы попытаетесь использовать == для поиска дубликатов, вам придется сравнить каждую пару объектов. Это O(N^2), само по себе намного больше, чем сортировка. Попробуйте запустить оба на бумаге. - person jalf; 27.08.2009
comment
у нас обоих одинаковое понимание того, что вам никогда не нужно проверять на равенство, и на самом деле невозможно отсортировать только семантическое равенство. И моя ситуация заключается в том, чтобы найти ‹I›равенство‹/I›, поэтому использование метода, который должен вывести/имитировать равенство с его атомарной операцией (меньше, чем в случае sort()), по крайней мере, не оптимально. Сортировка просто не является «i» оптимальной / i › в поиске «i» равенства / i › не то, чтобы она не нуждалась в меньшем чем. - person t.g.; 28.08.2009
comment
Сортировка позволяет найти дубликаты в гораздо меньшем количестве сравнений, чем простая проверка на равенство. Поиск дубликатов в несортированном диапазоне может быть выполнен только одним способом: путем сравнения каждого элемента с любым другим элементом. Это (N^2)/2 проверки на равенство. Однако вы можете выполнить сортировку в N lg(N) сравнений «меньше чем», и после того, как диапазон отсортирован, вы можете определить дубликаты в простом линейном сканировании, то есть ровно N сравнений на равенство. Если ваше N не крошечное, N lg(N)+N даст меньше сравнений, чем (N^2)/2. Следовательно, сортировка не такая уж плохая идея. - person jalf; 28.08.2009
comment
Ваш анализ верен в большинстве случаев. Но в этом случае вы можете удалить один элемент из контейнера, как только он будет идентифицирован как дубликат. Так что по мере продвижения список может уменьшаться, следовательно, меньше сравнений в каждом раунде. В лучшем случае, когда все элементы равны, вам нужно только (N-1) сравнений; Но в худшем случае, вы все сказали. Спасибо за обсуждение. Я узнал больше о сортировке. - person t.g.; 28.08.2009

Просто чтобы отметить, что у меня была такая же проблема во время нормализации тройного хранилища, с которым я работаю. Я реализовал метод 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)
           ...
           )))))
person Community    schedule 13.06.2011