Как сохранить только дубликаты эффективно?

Учитывая вектор STL, вывод только дубликаты в отсортированном порядке, например,

INPUT : { 4, 4, 1, 2, 3, 2, 3 }
OUTPUT: { 2, 3, 4 }

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

Моя наивная реализация:

void not_unique(vector<int>* pv)
{
    if (!pv)
        return;

 // Sort (in-place) so we can find duplicates in linear time
 sort(pv->begin(), pv->end());

 vector<int>::iterator it_start = pv->begin();
 while (it_start != pv->end())
 {
  size_t nKeep = 0;

  // Find the next different element
  vector<int>::iterator it_stop = it_start + 1;
  while (it_stop != pv->end() && *it_start == *it_stop)
  {
   nKeep = 1; // This gets set redundantly
   ++it_stop;
  }

  // If the element is a duplicate, keep only the first one (nKeep=1).
  // Otherwise, the element is not duplicated so erase it (nKeep=0).
  it_start = pv->erase(it_start + nKeep, it_stop);
 }
}

Если можно сделать это более эффективным, изящным, или общим, сообщите мне. Например, алгоритм сортировки или элементы копии в 2-м цикле для устранения стирания () вызов.

12
задан 11 revs 2 April 2010 в 17:11
поделиться

10 ответов

Более короткий и более STL-шный, чем предыдущие записи. Предполагает сортированный ввод.

#include <algorithm>
#include <functional>

template< class I, class P >
I remove_unique( I first, I last, P pred = P() ) {
    I dest = first;
    while (
        ( first = std::adjacent_find( first, last, pred ) )
            != last ) {
        * dest = * first;
        ++ first;
        ++ dest;
        if ( ( first = std::adjacent_find( first, last, std::not2( pred ) ) )
            == last ) break;
        ++ first;
    }
    return dest;
}

template< class I >
I remove_unique( I first, I last ) {
    return remove_unique( first, last,
        std::equal_to< typename std::iterator_traits<I>::value_type >() );
}
2
ответ дан 2 December 2019 в 19:53
поделиться

Вы даже можете использовать несоответствие для дополнительных очков!
Кстати: хорошее упражнение.

template<class TIter>
/** Moves duplicates to front, returning end of duplicates range.
 *  Use a sorted range as input. */
TIter Duplicates(TIter begin, TIter end) {
    TIter dup = begin;
    for (TIter it = begin; it != end; ++it) {
        TIter next = it;
        ++next;
        TIter const miss = std::mismatch(next, end, it).second;
        if (miss != it) {
            *dup++ = *miss;
            it = miss;
        }
    }
    return dup;
}
3
ответ дан 2 December 2019 в 19:53
поделиться

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

Рассмотрим крайний пример. Если исходный массив состоял из 1000 записей только с одним дубликатом, то на выходе был бы вектор только с одним значением. Было бы немного эффективнее создать новый вектор с одной записью, чем удалять другие 999 записей из исходного вектора. Однако я подозреваю, что при тестировании в реальном мире экономию от этого изменения может быть трудно измерить.

Править Я как раз думал об этом с точки зрения вопроса "интервью". Другими словами, это не очень полезный ответ. Но можно было бы решить это за O (N) (линейное время), а не за O (N Log N). Используйте дисковое пространство вместо ЦП. Создайте два «битовых» массива с изначально очищенными. Прокрутите свой вектор целочисленных значений. Найдите каждое значение в первом битовом массиве.Если он не установлен, установите бит (установите его в 1). Если он установлен, то установите соответствующий бит во втором массиве (с указанием дубликата). После обработки всех векторных записей просканируйте второй массив и выведите повторяющиеся целые числа (обозначенные битами, установленными во втором битовом массиве). Причина использования битовых массивов просто для экономии места. При работе с 4-байтовыми целыми числами требуется необработанное пространство (2 * 2 ^ 32/8) . Но на самом деле это можно было бы превратить в полезный алгоритм, сделав его разреженным массивом. Сам псевдопсевдокод будет примерно таким:

bitarray1[infinite_size];
bitarray2[infinite_size];

clear/zero bitarrays

// NOTE - do not need to sort the input
foreach value in original vector {
    if ( bitarray1[value] ) 
        // duplicate
        bitarray2[value] = 1
    bitarray1[value] = 1
}

// At this point, bitarray2 contains a 1 for all duplicate values.
// Scan it and create the new vector with the answer
for i = 0 to maxvalue
    if ( bitarray2[i] )
        print/save/keep i
1
ответ дан 2 December 2019 в 19:53
поделиться

Я с треском провалил свою первую попытку, предполагая, что std :: unique перемещает все дубликаты в конец диапазона (это не так). Ой. Вот еще одна попытка:

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

Функция имеет линейную сложность и выполняет один проход по диапазону ( std :: unique имеет линейную сложность). Он должен соответствовать требованиям прямого итератора. Возвращается конец результирующего диапазона.

template <typename It>
It not_unique(It first, It last)
{
    if (first == last) { return last; }

    It new_last = first;
    for (It current = first, next = ++first; next != last; ++current, ++next)
    {
        if (*current == *next)
        {
            if (current == new_last)
            {
                ++new_last;
            }
            else
            {
                *new_last++ = *current;
                while (next != last && *current == *next)
                {
                    ++current;
                    ++next;
                }
                if (next == last)
                    return new_last;
            }
        }
    }
    return new_last;
}
5
ответ дан 2 December 2019 в 19:53
поделиться

Я предлагаю модифицированную сортировку вставкой, чтобы вы могли сортировать и фильтровать дубликаты одновременно.

2
ответ дан 2 December 2019 в 19:53
поделиться

Вызов "erase (it_start + keep, it_stop);" из цикла while приведет к многократному копированию оставшихся элементов.

Я бы посоветовал переставить все уникальные элементы в начало вектора, а затем стереть все оставшиеся элементы сразу.

int num_repeats(vector<int>::const_iterator curr, vector<int>::const_iterator end) {
  int same = *curr;
  int count = 0;
  while (curr != end && same == *curr) {
    ++curr;
    ++count;
  }
  return count;
}

void dups(vector<int> *v) {
  sort(v->begin(), v->end());
  vector<int>::iterator current = v->begin();
  vector<int>::iterator end_of_dups = v->begin();
  while (current != v->end()) {
    int n = num_repeats(current, v->end());
    if (n > 1) {
      swap(*end_of_dups, *current);
      end_of_dups++;
    }
    current += n;
  }
  v->erase(end_of_dups, v->end());
}
1
ответ дан 2 December 2019 в 19:53
поделиться

Еще один:

template <typename T>
void keep_duplicates(vector<T>& v) 
{
    set<T> 
        u(v.begin(), v.end()), // unique 
        d; // duplicates
    for (size_t i = 0; i < v.size(); i++)
        if (u.find(v[i]) != u.end())
            u.erase(v[i]);
        else
            d.insert(v[i]);

    v = vector<T>(d.begin(), d.end());
}
1
ответ дан 2 December 2019 в 19:53
поделиться

Что означает выражение «столь же эффективно, как std :: unique»? Эффективен с точки зрения времени выполнения, времени разработки, использования памяти или чего-то еще?

Как отмечали другие, std :: unique требует отсортированного ввода, который вы не предоставили, поэтому это не лучший тест для начала.

Лично я бы просто std :: map делал всю мою работу за меня. У него много свойств, которые мы можем использовать для максимальной элегантности / лаконичности. Он сохраняет свои элементы уже отсортированными, и operator [] вставит нулевое значение, если ключ еще не существует. Используя эти свойства, мы можем сделать это в двух или трех строках кода и при этом достичь разумной сложности во время выполнения.

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

#include <iostream>
#include <vector>
#include <map>

void
output_sorted_duplicates(std::vector<int>* v)
{
   std::map<int, int> m;  

   // count how many of each element there are, putting results into map
   // map keys are elements in the vector, 
   // map values are the frequency of that element
   for (std::vector<int>::iterator vb = v->begin(); vb != v->end(); ++vb)
      ++m[*vb];

   // output keys whose values are 2 or more
   // the keys are already sorted by the map
   for (std::map<int, int>::iterator mb = m.begin(); mb != m.end(); ++mb)
      if ( (*mb).second >= 2 ) 
         std::cout << (*mb).first << " "; 
   std::cout << std::endl;
}

int main(void) 
{ 
   int initializer[] = { 4, 4, 1, 2, 3, 2, 3 };
   std::vector<int> data(&initializer[0], &initializer[0] + 7);
   output_sorted_duplicates(&data);
}

janks@phoenix:/tmp$ g++ test.cc && ./a.out
2 3 4

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

Сделать мою функцию шаблоном и заставить ее работать с диапазонами в стиле STL, а не просто с векторами целых чисел, остается в качестве упражнения.

0
ответ дан 2 December 2019 в 19:53
поделиться

Это в стиле стандартной библиотеки. Заслуга за алгоритм принадлежит Джеймсу! (Если вы ставите +1 мне, то лучше поставьте +1 ему, или иначе). Все, что я сделал, это привел его в стиль стандартной библиотеки:

#include <algorithm>
#include <functional>
#include <iostream>
#include <iterator>
#include <vector>

// other stuff (not for you)
template <typename T>
void print(const char* pMsg, const T& pContainer)
{
    std::cout << pMsg << "\n    ";
    std::copy(pContainer.begin(), pContainer.end(),
        std::ostream_iterator<typename T::value_type>(std::cout, " "));
    std::cout << std::endl;
}

template <typename T, size_t N>
T* endof(T (&pArray)[N])
{
    return &pArray[0] + N;
}

// not_unique functions (for you)
template <typename ForwardIterator, typename BinaryPredicate>
ForwardIterator not_unique(ForwardIterator pFirst, ForwardIterator pLast,
                           BinaryPredicate pPred)
{
    // correctly handle case where an empty range was given:
    if (pFirst == pLast) 
    { 
        return pLast; 
    }

    ForwardIterator result = pFirst;
    ForwardIterator previous = pFirst;

    for (++pFirst; pFirst != pLast; ++pFirst, ++previous)
    {
        // if equal to previous
        if (pPred(*pFirst, *previous))
        {
            if (previous == result)
            {
                // if we just bumped bump again
                ++result;
            }
            else if (!pPred(*previous, *result))
            {
                // if it needs to be copied, copy it
                *result = *previous;

                // bump
                ++result;
            }
        }
    }

    return result;
}

template <typename ForwardIterator>
ForwardIterator not_unique(ForwardIterator pFirst, ForwardIterator pLast)
{
    return not_unique(pFirst, pLast,
                std::equal_to<typename ForwardIterator::value_type>());
}


//test
int main()
{
    typedef std::vector<int> vec;

    int data[] = {1, 4, 7, 7, 2, 2, 2, 3, 9, 9, 5, 4, 2, 8};
    vec v(data, endof(data));

    // precondition
    std::sort(v.begin(), v.end());
    print("before", v);

    // duplicatify (it's a word now)
    vec::iterator iter = not_unique(v.begin(), v.end());
    print("after", v);

    // remove extra
    v.erase(iter, v.end());
    print("erased", v);
}
2
ответ дан 2 December 2019 в 19:53
поделиться

Это исправляет ошибки в исходной версии Джеймса Макнеллиса . Я также предоставляю версии на месте и вне места.

// In-place version.  Uses less memory and works for more container
// types but is slower.
template <typename It>
It not_unique_inplace(It first, It last)
{
    if (first == last)
        return last;

    It new_last = first;
    for (It current = first, next = first + 1; next != last; ++current, ++next)
    {
        if (*current == *next && 
            (new_last == first || *current != *(new_last-1)))
            *new_last++ = *current;
    }
    return new_last;
}

// Out-of-place version. Fastest.
template <typename It, typename Container>
void not_unique(It first, It last, Container pout)
{
    if (first == last || !pout)
        return;

    for (It current = first, next = first + 1; next != last; ++current, ++next)
    {
        if (*current == *next && 
            (pout->empty() || *current != pout->back()))
            pout->push_back(*current);
    }
}
0
ответ дан 2 December 2019 в 19:53
поделиться
Другие вопросы по тегам:

Похожие вопросы: