Qual é a maneira mais eficiente de apagar duplicatas e classificar um vetor?

274

Eu preciso pegar um vetor C ++ com potencialmente muitos elementos, apagar duplicatas e classificá-lo.

Atualmente, tenho o código abaixo, mas ele não funciona.

vec.erase(
      std::unique(vec.begin(), vec.end()),
      vec.end());
std::sort(vec.begin(), vec.end());

Como posso fazer isso corretamente?

Além disso, é mais rápido apagar as duplicatas primeiro (semelhante ao codificado acima) ou executar a classificação primeiro? Se eu executar a classificação primeiro, é garantido que permaneça classificada após a std::uniqueexecução?

Ou existe outra maneira (talvez mais eficiente) de fazer tudo isso?

Kyle Ryan
fonte
3
Eu suponho que você não tem a opção de verificar antes da inserção para evitar enganar em primeiro lugar?
Joe
Corrigir. Isso seria o ideal.
Kyle Ryan
29
Sugiro corrigir o código acima ou realmente indicar que está errado. std :: unique assume que o intervalo já está classificado.
Matthieu M.

Respostas:

584

Eu concordo com R. Pate e Todd Gardner ; uma std::setpode ser uma boa ideia aqui. Mesmo se você estiver preso ao usar vetores, se você tiver duplicatas suficientes, é melhor criar um conjunto para fazer o trabalho sujo.

Vamos comparar três abordagens:

Apenas usando vetor, classificar + exclusivo

sort( vec.begin(), vec.end() );
vec.erase( unique( vec.begin(), vec.end() ), vec.end() );

Converter para definir (manualmente)

set<int> s;
unsigned size = vec.size();
for( unsigned i = 0; i < size; ++i ) s.insert( vec[i] );
vec.assign( s.begin(), s.end() );

Converter em conjunto (usando um construtor)

set<int> s( vec.begin(), vec.end() );
vec.assign( s.begin(), s.end() );

Aqui está como elas são executadas conforme o número de duplicatas é alterado:

comparação de abordagens de vetores e conjuntos

Resumo : quando o número de duplicatas é grande o suficiente, é realmente mais rápido converter em um conjunto e depois despejar os dados em um vetor .

E, por alguma razão, fazer a conversão do conjunto manualmente parece ser mais rápido do que usar o construtor do conjunto - pelo menos nos dados aleatórios de brinquedo que eu usei.

Nate Kohl
fonte
61
Estou chocado que a abordagem do construtor seja consistentemente mensurável pior que a manual. Você gostaria que, além de uma pequena sobrecarga constante, ela faria apenas a coisa manual. Alguém pode explicar isso?
Ari
17
Legal, obrigado pelo gráfico. Você poderia dar uma idéia de quais são as unidades para Number of Duplicates? (ou seja, em torno de quão grande é "grande o suficiente")?
26611 Kyle Ryan
5
@Kyle: É bem grande. Usei conjuntos de dados de 1.000.000 de números inteiros sorteados aleatoriamente entre 1 e 1000, 100 e 10 para este gráfico.
Nate Kohl
5
Eu acho que seus resultados estão errados. Nos meus testes, quanto mais elementos duplicados, o vetor mais rápido (comparativo), na verdade é o contrário. Você compilou com otimizações ativadas e verificações de tempo de execução? No meu lado, o vetor é sempre mais rápido, até 100x, dependendo do número de duplicatas. VS2013, cl / Boi-D_SECURE_SCL = 0.
Davidnr 9/07
39
A descrição do eixo x parece estar ausente.
BartoszKP
72

Refiz o perfil de Nate Kohl e obtive resultados diferentes. Para o meu caso de teste, classificar diretamente o vetor é sempre mais eficiente do que usar um conjunto. Eu adicionei um novo método mais eficiente, usando um unordered_set.

Lembre-se de que o unordered_setmétodo só funciona se você tiver uma boa função de hash para o tipo de unificação e classificação necessárias. Para ints, isso é fácil! (A biblioteca padrão fornece um hash padrão, que é simplesmente a função de identidade.) Além disso, não se esqueça de classificar no final, pois unordered_set é, bem, desordenado :)

Eu fiz alguma escavação dentro do sete unordered_setimplementação e descobriu que o construtor realmente construir um novo nó para cada elemento, antes de verificar o seu valor para determinar se ele realmente deve ser inserido (na implementação do Visual Studio, pelo menos).

Aqui estão os 5 métodos:

f1: Apenas usando vector, sort+unique

sort( vec.begin(), vec.end() );
vec.erase( unique( vec.begin(), vec.end() ), vec.end() );

f2: converta para set(usando um construtor)

set<int> s( vec.begin(), vec.end() );
vec.assign( s.begin(), s.end() );

f3: converter para set(manualmente)

set<int> s;
for (int i : vec)
    s.insert(i);
vec.assign( s.begin(), s.end() );

f4: converta para unordered_set(usando um construtor)

unordered_set<int> s( vec.begin(), vec.end() );
vec.assign( s.begin(), s.end() );
sort( vec.begin(), vec.end() );

f5: converter para unordered_set(manualmente)

unordered_set<int> s;
for (int i : vec)
    s.insert(i);
vec.assign( s.begin(), s.end() );
sort( vec.begin(), vec.end() );

Eu fiz o teste com um vetor de 100.000.000 de ints escolhido aleatoriamente nos intervalos [1,10], [1.100] e [1.100000]

Os resultados (em segundos, menor é melhor):

range         f1       f2       f3       f4      f5
[1,10]      1.6821   7.6804   2.8232   6.2634  0.7980
[1,1000]    5.0773  13.3658   8.2235   7.6884  1.9861
[1,100000]  8.7955  32.1148  26.5485  13.3278  3.9822
alexk7
fonte
4
Para números inteiros, você pode usar a classificação radix, que é muito mais rápida que std :: sort.
Changming Sun
2
Dica rápida, para utilização sortou uniquemétodos, você tem que#include <algorithm>
Davmrtl
3
@ChangmingSun Gostaria de saber por que o otimizador parecia falhar no f4? Os números são dramaticamente diferentes de f5. Não faz nenhum sentido para mim.
Sandthorn # 6/18
1
@sandthorn Como explicado na minha resposta, a implementação cria um nó (incluindo alocação dinâmica) para cada elemento da sequência de entrada, que é um desperdício para cada valor que acaba sendo uma duplicata. Não há como o otimizador saber que poderia pular isso.
alexk7
Ah, isso me lembra uma das conversas de Scott Meyer sobre o CWUK cenário que tem natureza de propablities para desacelerar o emplacetipo de construção.
sandthorn 11/09/19
58

std::unique somente remove elementos duplicados se forem vizinhos: você deve classificar o vetor primeiro antes que ele funcione conforme pretendido.

std::unique é definido como estável, portanto, o vetor ainda será classificado após a execução exclusiva nele.

jskinner
fonte
42

Não sei bem para que você está usando isso, então não posso dizer isso com 100% de certeza, mas normalmente quando penso em um contêiner "classificado e único", penso em um std :: set . Pode ser um ajuste melhor para o seu caso de usuário:

std::set<Foo> foos(vec.begin(), vec.end()); // both sorted & unique already

Caso contrário, a classificação anterior à chamada única (como as outras respostas apontam) é o caminho a percorrer.

Todd Gardner
fonte
Bem ao ponto! std :: set é especificado para ser um conjunto exclusivo classificado. A maioria das implementações usa uma árvore binária ordenada eficiente ou algo semelhante.
Notnoop
+1 Pensado em conjunto também. Não queria duplicar essa resposta
Tom
É garantido que std :: set seja classificado? Faz sentido que na prática seria, mas o padrão exige isso?
21133 MadCoder
1
Sim, consulte 23.1.4.9 "A propriedade fundamental dos iteradores de contêineres associativos é que eles iteram pelos contêineres na ordem não decrescente de chaves em que não decrescente é definido pela comparação usada para construí-los"
Todd Gardner
1
@ MadCoder: Não necessariamente "faz sentido" que um conjunto seja implementado de uma maneira que seja classificada. Também existem conjuntos implementados usando tabelas de hash, que não são classificadas. De fato, a maioria das pessoas prefere usar tabelas de hash quando disponíveis. Porém, a convenção de nomenclatura em C ++ simplesmente acontece que os contêineres associativos classificados são nomeados simplesmente "set" / "map" (análogo ao TreeSet / TreeMap em Java); e os contêineres associativos com hash, que foram deixados de fora do padrão, são chamados "hash_set" / "hash_map" (SGI STL) ou "unordered_set" / "unordered_set" (TR1) (análogo ao HashSet e HashMap em Java)
newacct
22

std::uniquefunciona apenas em execuções consecutivas de elementos duplicados, então é melhor classificar primeiro. No entanto, como é estável, seu vetor permanecerá classificado.

David Seiler
fonte
18

Aqui está um modelo para fazer isso por você:

template<typename T>
void removeDuplicates(std::vector<T>& vec)
{
    std::sort(vec.begin(), vec.end());
    vec.erase(std::unique(vec.begin(), vec.end()), vec.end());
}

chame assim:

removeDuplicates<int>(vectorname);
DShook
fonte
2
+1 Templatize away! - mas você pode simplesmente escrever removeDuplicates (vec), sem especificar explicitamente os argumentos do modelo
Faisal Vali
10
Ou, melhor ainda, basta ter iteradores modelados diretamente (início e fim) e você pode executá-lo em outras estruturas além de um vetor.
Kyle Ryan
Infernos sim, modelos! correção rápida para pequenas listas, estilo STL completo. 1 thx
QuantumKarl
@Kyle - apenas em outros contêineres que possuem um erase()método, caso contrário, você deve retornar o novo iterador final e o código de chamada truncará o contêiner.
Toby Speight
8

Eficiência é um conceito complicado. Há considerações de tempo versus espaço, bem como medições gerais (onde você só obtém respostas vagas, como O (n)) versus específicas (por exemplo, a classificação de bolhas pode ser muito mais rápida que a classificação rápida, dependendo das características da entrada).

Se você tiver relativamente poucas duplicatas, a classificação seguida por única e a exclusão parecerão o caminho a seguir. Se você tiver relativamente muitas duplicatas, criar um conjunto a partir do vetor e deixá-lo fazer o trabalho pesado pode vencê-lo facilmente.

Não se concentre apenas na eficiência do tempo. Ordenar + exclusivo + apagar opera no espaço O (1), enquanto a construção do conjunto opera no espaço O (n). E nenhum deles se presta diretamente a uma paralelização de redução de mapa (para conjuntos de dados realmente grandes ).


fonte
O que lhe daria um mapa / reduziria a capacidade? O único que consigo pensar é em uma classificação de mesclagem distribuída e você ainda pode usar apenas um segmento na mesclagem final.
Zan Lynx
1
Sim, você deve ter um nó / thread de controle. No entanto, você pode dividir o problema quantas vezes for necessário para colocar limites superiores no número de threads de trabalho / filho com os quais o thread de controle / pai lida e no tamanho do conjunto de dados que cada nó folha deve processar. Nem todos os problemas são facilmente solucionáveis ​​com a redução de mapa; eu simplesmente queria salientar que existem pessoas que lidam com problemas de otimização semelhantes (na superfície, de qualquer maneira), onde lidar com 10 terabytes de dados é chamado de "terça-feira".
7

Você precisa classificá-lo antes de ligar, uniqueporque uniqueapenas remove duplicatas próximas umas das outras.

editar: 38 segundos ...

David Johnstone
fonte
7

uniqueremove apenas elementos duplicados consecutivos (o que é necessário para ser executado em tempo linear), portanto, você deve executar a classificação primeiro. Ele permanecerá classificado após a chamada para unique.

Peter
fonte
7

Se você não deseja alterar a ordem dos elementos, tente esta solução:

template <class T>
void RemoveDuplicatesInVector(std::vector<T> & vec)
{
    set<T> values;
    vec.erase(std::remove_if(vec.begin(), vec.end(), [&](const T & value) { return !values.insert(value).second; }), vec.end());
}
yury
fonte
Talvez use unordered_set em vez de conjunto (e boost :: remove_erase_if se disponível)
gast128
4

Supondo que a seja um vetor, remova as duplicatas contíguas usando

a.erase(unique(a.begin(),a.end()),a.end());é executado em O (n) tempo.

KPMG
fonte
1
duplicados contíguos. ok, então ele precisa de um std::sortprimeiro.
v.oddou 23/04
2

Como já foi dito, uniquerequer um contêiner classificado. Além disso, uniquena verdade , não remove elementos do contêiner. Em vez disso, eles são copiados para o final, uniqueretorna um iterador apontando para o primeiro elemento duplicado e você deve chamar erasepara remover os elementos.

Max Lybbert
fonte
O unique requer um contêiner classificado ou simplesmente reorganiza a sequência de entrada para que não contenha duplicatas adjacentes? Eu pensei no último.
@ Patê, você está correto. Não requer um. Remove duplicatas adjacentes.
Bill Lynch
Se você possui um contêiner que pode ter duplicatas e deseja um contêiner que não tenha nenhum valor duplicado em qualquer lugar do contêiner, primeiro classifique o contêiner, depois passe-o para exclusivo e, em seguida, use erase para remover as duplicatas. . Se você simplesmente deseja remover duplicatas adjacentes, não precisará classificar o contêiner. Mas você terminará com valores duplicados: 1 2 2 3 2 4 2 5 2 será alterado para 1 2 3 2 4 2 5 2 se passado para exclusivo sem classificação, 1 2 3 4 5 se classificado, passado para exclusivo e apagado .
Max Lybbert
2

A abordagem padrão sugerida por Nate Kohl, apenas usando vetor, classifica + exclusivo:

sort( vec.begin(), vec.end() );
vec.erase( unique( vec.begin(), vec.end() ), vec.end() );

não funciona para um vetor de ponteiros.

Veja atentamente este exemplo em cplusplus.com .

No exemplo deles, as "chamadas duplicatas" movidas para o final são realmente mostradas como? (valores indefinidos), porque essas "chamadas duplicatas" são ALGUMAS VEZES "elementos extras" e ALGUMAS VEZES existem "elementos ausentes" que estavam no vetor original.

Ocorre um problema ao usar std::unique()um vetor de ponteiros para objetos (vazamentos de memória, leitura incorreta de dados do HEAP, liberações duplicadas, que causam falhas de segmentação etc.).

Aqui está a minha solução para o problema: substitua std::unique()por ptgi::unique().

Veja o arquivo ptgi_unique.hpp abaixo:

// ptgi::unique()
//
// Fix a problem in std::unique(), such that none of the original elts in the collection are lost or duplicate.
// ptgi::unique() has the same interface as std::unique()
//
// There is the 2 argument version which calls the default operator== to compare elements.
//
// There is the 3 argument version, which you can pass a user defined functor for specialized comparison.
//
// ptgi::unique() is an improved version of std::unique() which doesn't looose any of the original data
// in the collection, nor does it create duplicates.
//
// After ptgi::unique(), every old element in the original collection is still present in the re-ordered collection,
// except that duplicates have been moved to a contiguous range [dupPosition, last) at the end.
//
// Thus on output:
//  [begin, dupPosition) range are unique elements.
//  [dupPosition, last) range are duplicates which can be removed.
// where:
//  [] means inclusive, and
//  () means exclusive.
//
// In the original std::unique() non-duplicates at end are moved downward toward beginning.
// In the improved ptgi:unique(), non-duplicates at end are swapped with duplicates near beginning.
//
// In addition if you have a collection of ptrs to objects, the regular std::unique() will loose memory,
// and can possibly delete the same pointer multiple times (leading to SEGMENTATION VIOLATION on Linux machines)
// but ptgi::unique() won't.  Use valgrind(1) to find such memory leak problems!!!
//
// NOTE: IF you have a vector of pointers, that is, std::vector<Object*>, then upon return from ptgi::unique()
// you would normally do the following to get rid of the duplicate objects in the HEAP:
//
//  // delete objects from HEAP
//  std::vector<Object*> objects;
//  for (iter = dupPosition; iter != objects.end(); ++iter)
//  {
//      delete (*iter);
//  }
//
//  // shrink the vector. But Object * pointers are NOT followed for duplicate deletes, this shrinks the vector.size())
//  objects.erase(dupPosition, objects.end));
//
// NOTE: But if you have a vector of objects, that is: std::vector<Object>, then upon return from ptgi::unique(), it
// suffices to just call vector:erase(, as erase will automatically call delete on each object in the
// [dupPosition, end) range for you:
//
//  std::vector<Object> objects;
//  objects.erase(dupPosition, last);
//
//==========================================================================================================
// Example of differences between std::unique() vs ptgi::unique().
//
//  Given:
//      int data[] = {10, 11, 21};
//
//  Given this functor: ArrayOfIntegersEqualByTen:
//      A functor which compares two integers a[i] and a[j] in an int a[] array, after division by 10:
//  
//  // given an int data[] array, remove consecutive duplicates from it.
//  // functor used for std::unique (BUGGY) or ptgi::unique(IMPROVED)
//
//  // Two numbers equal if, when divided by 10 (integer division), the quotients are the same.
//  // Hence 50..59 are equal, 60..69 are equal, etc.
//  struct ArrayOfIntegersEqualByTen: public std::equal_to<int>
//  {
//      bool operator() (const int& arg1, const int& arg2) const
//      {
//          return ((arg1/10) == (arg2/10));
//      }
//  };
//  
//  Now, if we call (problematic) std::unique( data, data+3, ArrayOfIntegersEqualByTen() );
//  
//  TEST1: BEFORE UNIQ: 10,11,21
//  TEST1: AFTER UNIQ: 10,21,21
//  DUP_INX=2
//  
//      PROBLEM: 11 is lost, and extra 21 has been added.
//  
//  More complicated example:
//  
//  TEST2: BEFORE UNIQ: 10,20,21,22,30,31,23,24,11
//  TEST2: AFTER UNIQ: 10,20,30,23,11,31,23,24,11
//  DUP_INX=5
//  
//      Problem: 21 and 22 are deleted.
//      Problem: 11 and 23 are duplicated.
//  
//  
//  NOW if ptgi::unique is called instead of std::unique, both problems go away:
//  
//  DEBUG: TEST1: NEW_WAY=1
//  TEST1: BEFORE UNIQ: 10,11,21
//  TEST1: AFTER UNIQ: 10,21,11
//  DUP_INX=2
//  
//  DEBUG: TEST2: NEW_WAY=1
//  TEST2: BEFORE UNIQ: 10,20,21,22,30,31,23,24,11
//  TEST2: AFTER UNIQ: 10,20,30,23,11,31,22,24,21
//  DUP_INX=5
//
//  @SEE: look at the "case study" below to understand which the last "AFTER UNIQ" results with that order:
//  TEST2: AFTER UNIQ: 10,20,30,23,11,31,22,24,21
//
//==========================================================================================================
// Case Study: how ptgi::unique() works:
//  Remember we "remove adjacent duplicates".
//  In this example, the input is NOT fully sorted when ptgi:unique() is called.
//
//  I put | separatators, BEFORE UNIQ to illustrate this
//  10  | 20,21,22 |  30,31 |  23,24 | 11
//
//  In example above, 20, 21, 22 are "same" since dividing by 10 gives 2 quotient.
//  And 30,31 are "same", since /10 quotient is 3.
//  And 23, 24 are same, since /10 quotient is 2.
//  And 11 is "group of one" by itself.
//  So there are 5 groups, but the 4th group (23, 24) happens to be equal to group 2 (20, 21, 22)
//  So there are 5 groups, and the 5th group (11) is equal to group 1 (10)
//
//  R = result
//  F = first
//
//  10, 20, 21, 22, 30, 31, 23, 24, 11
//  R    F
//
//  10 is result, and first points to 20, and R != F (10 != 20) so bump R:
//       R
//       F
//
//  Now we hits the "optimized out swap logic".
//  (avoid swap because R == F)
//
//  // now bump F until R != F (integer division by 10)
//  10, 20, 21, 22, 30, 31, 23, 24, 11
//       R   F              // 20 == 21 in 10x
//       R       F              // 20 == 22 in 10x
//       R           F          // 20 != 30, so we do a swap of ++R and F
//  (Now first hits 21, 22, then finally 30, which is different than R, so we swap bump R to 21 and swap with  30)
//  10, 20, 30, 22, 21, 31, 23, 24, 11  // after R & F swap (21 and 30)
//           R       F 
//
//  10, 20, 30, 22, 21, 31, 23, 24, 11
//           R          F           // bump F to 31, but R and F are same (30 vs 31)
//           R               F      // bump F to 23, R != F, so swap ++R with F
//  10, 20, 30, 22, 21, 31, 23, 24, 11
//                  R           F       // bump R to 22
//  10, 20, 30, 23, 21, 31, 22, 24, 11  // after the R & F swap (22 & 23 swap)
//                  R            F      // will swap 22 and 23
//                  R                F      // bump F to 24, but R and F are same in 10x
//                  R                    F  // bump F, R != F, so swap ++R  with F
//                      R                F  // R and F are diff, so swap ++R  with F (21 and 11)
//  10, 20, 30, 23, 11, 31, 22, 24, 21
//                      R                F  // aftter swap of old 21 and 11
//                      R                  F    // F now at last(), so loop terminates
//                          R               F   // bump R by 1 to point to dupPostion (first duplicate in range)
//
//  return R which now points to 31
//==========================================================================================================
// NOTES:
// 1) the #ifdef IMPROVED_STD_UNIQUE_ALGORITHM documents how we have modified the original std::unique().
// 2) I've heavily unit tested this code, including using valgrind(1), and it is *believed* to be 100% defect-free.
//
//==========================================================================================================
// History:
//  130201  dpb [email protected] created
//==========================================================================================================

#ifndef PTGI_UNIQUE_HPP
#define PTGI_UNIQUE_HPP

// Created to solve memory leak problems when calling std::unique() on a vector<Route*>.
// Memory leaks discovered with valgrind and unitTesting.


#include <algorithm>        // std::swap

// instead of std::myUnique, call this instead, where arg3 is a function ptr
//
// like std::unique, it puts the dups at the end, but it uses swapping to preserve original
// vector contents, to avoid memory leaks and duplicate pointers in vector<Object*>.

#ifdef IMPROVED_STD_UNIQUE_ALGORITHM
#error the #ifdef for IMPROVED_STD_UNIQUE_ALGORITHM was defined previously.. Something is wrong.
#endif

#undef IMPROVED_STD_UNIQUE_ALGORITHM
#define IMPROVED_STD_UNIQUE_ALGORITHM

// similar to std::unique, except that this version swaps elements, to avoid
// memory leaks, when vector contains pointers.
//
// Normally the input is sorted.
// Normal std::unique:
// 10 20 20 20 30   30 20 20 10
// a  b  c  d  e    f  g  h  i
//
// 10 20 30 20 10 | 30 20 20 10
// a  b  e  g  i    f  g  h  i
//
// Now GONE: c, d.
// Now DUPS: g, i.
// This causes memory leaks and segmenation faults due to duplicate deletes of same pointer!


namespace ptgi {

// Return the position of the first in range of duplicates moved to end of vector.
//
// uses operator==  of class for comparison
//
// @param [first, last) is a range to find duplicates within.
//
// @return the dupPosition position, such that [dupPosition, end) are contiguous
// duplicate elements.
// IF all items are unique, then it would return last.
//
template <class ForwardIterator>
ForwardIterator unique( ForwardIterator first, ForwardIterator last)
{
    // compare iterators, not values
    if (first == last)
        return last;

    // remember the current item that we are looking at for uniqueness
    ForwardIterator result = first;

    // result is slow ptr where to store next unique item
    // first is  fast ptr which is looking at all elts

    // the first iterator moves over all elements [begin+1, end).
    // while the current item (result) is the same as all elts
    // to the right, (first) keeps going, until you find a different
    // element pointed to by *first.  At that time, we swap them.

    while (++first != last)
    {
        if (!(*result == *first))
        {
#ifdef IMPROVED_STD_UNIQUE_ALGORITHM
            // inc result, then swap *result and *first

//          THIS IS WHAT WE WANT TO DO.
//          BUT THIS COULD SWAP AN ELEMENT WITH ITSELF, UNCECESSARILY!!!
//          std::swap( *first, *(++result));

            // BUT avoid swapping with itself when both iterators are the same
            ++result;
            if (result != first)
                std::swap( *first, *result);
#else
            // original code found in std::unique()
            // copies unique down
            *(++result) = *first;
#endif
        }
    }

    return ++result;
}

template <class ForwardIterator, class BinaryPredicate>
ForwardIterator unique( ForwardIterator first, ForwardIterator last, BinaryPredicate pred)
{
    if (first == last)
        return last;

    // remember the current item that we are looking at for uniqueness
    ForwardIterator result = first;

    while (++first != last)
    {
        if (!pred(*result,*first))
        {
#ifdef IMPROVED_STD_UNIQUE_ALGORITHM
            // inc result, then swap *result and *first

//          THIS COULD SWAP WITH ITSELF UNCECESSARILY
//          std::swap( *first, *(++result));
//
            // BUT avoid swapping with itself when both iterators are the same
            ++result;
            if (result != first)
                std::swap( *first, *result);

#else
            // original code found in std::unique()
            // copies unique down
            // causes memory leaks, and duplicate ptrs
            // and uncessarily moves in place!
            *(++result) = *first;
#endif
        }
    }

    return ++result;
}

// from now on, the #define is no longer needed, so get rid of it
#undef IMPROVED_STD_UNIQUE_ALGORITHM

} // end ptgi:: namespace

#endif

E aqui está o programa UNIT Test que eu usei para testá-lo:

// QUESTION: in test2, I had trouble getting one line to compile,which was caused  by the declaration of operator()
// in the equal_to Predicate.  I'm not sure how to correctly resolve that issue.
// Look for //OUT lines
//
// Make sure that NOTES in ptgi_unique.hpp are correct, in how we should "cleanup" duplicates
// from both a vector<Integer> (test1()) and vector<Integer*> (test2).
// Run this with valgrind(1).
//
// In test2(), IF we use the call to std::unique(), we get this problem:
//
//  [dbednar@ipeng8 TestSortRoutes]$ ./Main7
//  TEST2: ORIG nums before UNIQUE: 10, 20, 21, 22, 30, 31, 23, 24, 11
//  TEST2: modified nums AFTER UNIQUE: 10, 20, 30, 23, 11, 31, 23, 24, 11
//  INFO: dupInx=5
//  TEST2: uniq = 10
//  TEST2: uniq = 20
//  TEST2: uniq = 30
//  TEST2: uniq = 33427744
//  TEST2: uniq = 33427808
//  Segmentation fault (core dumped)
//
// And if we run valgrind we seen various error about "read errors", "mismatched free", "definitely lost", etc.
//
//  valgrind --leak-check=full ./Main7
//  ==359== Memcheck, a memory error detector
//  ==359== Command: ./Main7
//  ==359== Invalid read of size 4
//  ==359== Invalid free() / delete / delete[]
//  ==359== HEAP SUMMARY:
//  ==359==     in use at exit: 8 bytes in 2 blocks
//  ==359== LEAK SUMMARY:
//  ==359==    definitely lost: 8 bytes in 2 blocks
// But once we replace the call in test2() to use ptgi::unique(), all valgrind() error messages disappear.
//
// 130212   dpb [email protected] created
// =========================================================================================================

#include <iostream> // std::cout, std::cerr
#include <string>
#include <vector>   // std::vector
#include <sstream>  // std::ostringstream
#include <algorithm>    // std::unique()
#include <functional>   // std::equal_to(), std::binary_function()
#include <cassert>  // assert() MACRO

#include "ptgi_unique.hpp"  // ptgi::unique()



// Integer is small "wrapper class" around a primitive int.
// There is no SETTER, so Integer's are IMMUTABLE, just like in JAVA.

class Integer
{
private:
    int num;
public:

    // default CTOR: "Integer zero;"
    // COMPRENSIVE CTOR:  "Integer five(5);"
    Integer( int num = 0 ) :
        num(num)
    {
    }

    // COPY CTOR
    Integer( const Integer& rhs) :
        num(rhs.num)
    {
    }

    // assignment, operator=, needs nothing special... since all data members are primitives

    // GETTER for 'num' data member
    // GETTER' are *always* const
    int getNum() const
    {
        return num;
    }   

    // NO SETTER, because IMMUTABLE (similar to Java's Integer class)

    // @return "num"
    // NB: toString() should *always* be a const method
    //
    // NOTE: it is probably more efficient to call getNum() intead
    // of toString() when printing a number:
    //
    // BETTER to do this:
    //  Integer five(5);
    //  std::cout << five.getNum() << "\n"
    // than this:
    //  std::cout << five.toString() << "\n"

    std::string toString() const
    {
        std::ostringstream oss;
        oss << num;
        return oss.str();
    }
};

// convenience typedef's for iterating over std::vector<Integer>
typedef std::vector<Integer>::iterator      IntegerVectorIterator;
typedef std::vector<Integer>::const_iterator    ConstIntegerVectorIterator;

// convenience typedef's for iterating over std::vector<Integer*>
typedef std::vector<Integer*>::iterator     IntegerStarVectorIterator;
typedef std::vector<Integer*>::const_iterator   ConstIntegerStarVectorIterator;

// functor used for std::unique or ptgi::unique() on a std::vector<Integer>
// Two numbers equal if, when divided by 10 (integer division), the quotients are the same.
// Hence 50..59 are equal, 60..69 are equal, etc.
struct IntegerEqualByTen: public std::equal_to<Integer>
{
    bool operator() (const Integer& arg1, const Integer& arg2) const
    {
        return ((arg1.getNum()/10) == (arg2.getNum()/10));
    }
};

// functor used for std::unique or ptgi::unique on a std::vector<Integer*>
// Two numbers equal if, when divided by 10 (integer division), the quotients are the same.
// Hence 50..59 are equal, 60..69 are equal, etc.
struct IntegerEqualByTenPointer: public std::equal_to<Integer*>
{
    // NB: the Integer*& looks funny to me!
    // TECHNICAL PROBLEM ELSEWHERE so had to remove the & from *&
//OUT   bool operator() (const Integer*& arg1, const Integer*& arg2) const
//
    bool operator() (const Integer* arg1, const Integer* arg2) const
    {
        return ((arg1->getNum()/10) == (arg2->getNum()/10));
    }
};

void test1();
void test2();
void printIntegerStarVector( const std::string& msg, const std::vector<Integer*>& nums );

int main()
{
    test1();
    test2();
    return 0;
}

// test1() uses a vector<Object> (namely vector<Integer>), so there is no problem with memory loss
void test1()
{
    int data[] = { 10, 20, 21, 22, 30, 31, 23, 24, 11};

    // turn C array into C++ vector
    std::vector<Integer> nums(data, data+9);

    // arg3 is a functor
    IntegerVectorIterator dupPosition = ptgi::unique( nums.begin(), nums.end(), IntegerEqualByTen() );

    nums.erase(dupPosition, nums.end());

    nums.erase(nums.begin(), dupPosition);
}

//==================================================================================
// test2() uses a vector<Integer*>, so after ptgi:unique(), we have to be careful in
// how we eliminate the duplicate Integer objects stored in the heap.
//==================================================================================
void test2()
{
    int data[] = { 10, 20, 21, 22, 30, 31, 23, 24, 11};

    // turn C array into C++ vector of Integer* pointers
    std::vector<Integer*> nums;

    // put data[] integers into equivalent Integer* objects in HEAP
    for (int inx = 0; inx < 9; ++inx)
    {
        nums.push_back( new Integer(data[inx]) );
    }

    // print the vector<Integer*> to stdout
    printIntegerStarVector( "TEST2: ORIG nums before UNIQUE", nums );

    // arg3 is a functor
#if 1
    // corrected version which fixes SEGMENTATION FAULT and all memory leaks reported by valgrind(1)
    // I THINK we want to use new C++11 cbegin() and cend(),since the equal_to predicate is passed "Integer *&"

//  DID NOT COMPILE
//OUT   IntegerStarVectorIterator dupPosition = ptgi::unique( const_cast<ConstIntegerStarVectorIterator>(nums.begin()), const_cast<ConstIntegerStarVectorIterator>(nums.end()), IntegerEqualByTenPointer() );

    // DID NOT COMPILE when equal_to predicate declared "Integer*& arg1, Integer*&  arg2"
//OUT   IntegerStarVectorIterator dupPosition = ptgi::unique( const_cast<nums::const_iterator>(nums.begin()), const_cast<nums::const_iterator>(nums.end()), IntegerEqualByTenPointer() );


    // okay when equal_to predicate declared "Integer* arg1, Integer*  arg2"
    IntegerStarVectorIterator dupPosition = ptgi::unique(nums.begin(), nums.end(), IntegerEqualByTenPointer() );
#else
    // BUGGY version that causes SEGMENTATION FAULT and valgrind(1) errors
    IntegerStarVectorIterator dupPosition = std::unique( nums.begin(), nums.end(), IntegerEqualByTenPointer() );
#endif

    printIntegerStarVector( "TEST2: modified nums AFTER UNIQUE", nums );
    int dupInx = dupPosition - nums.begin();
    std::cout << "INFO: dupInx=" << dupInx <<"\n";

    // delete the dup Integer* objects in the [dupPosition, end] range
    for (IntegerStarVectorIterator iter = dupPosition; iter != nums.end(); ++iter)
    {
        delete (*iter);
    }

    // shrink the vector
    // NB: the Integer* ptrs are NOT followed by vector::erase()
    nums.erase(dupPosition, nums.end());


    // print the uniques, by following the iter to the Integer* pointer
    for (IntegerStarVectorIterator iter = nums.begin(); iter != nums.end();  ++iter)
    {
        std::cout << "TEST2: uniq = " << (*iter)->getNum() << "\n";
    }

    // remove the unique objects from heap
    for (IntegerStarVectorIterator iter = nums.begin(); iter != nums.end();  ++iter)
    {
        delete (*iter);
    }

    // shrink the vector
    nums.erase(nums.begin(), nums.end());

    // the vector should now be completely empty
    assert( nums.size() == 0);
}

//@ print to stdout the string: "info_msg: num1, num2, .... numN\n"
void printIntegerStarVector( const std::string& msg, const std::vector<Integer*>& nums )
{
    std::cout << msg << ": ";
    int inx = 0;
    ConstIntegerStarVectorIterator  iter;

    // use const iterator and const range!
    // NB: cbegin() and cend() not supported until LATER (c++11)
    for (iter = nums.begin(), inx = 0; iter != nums.end(); ++iter, ++inx)
    {
        // output a comma seperator *AFTER* first
        if (inx > 0)
            std::cout << ", ";

        // call Integer::toString()
        std::cout << (*iter)->getNum();     // send int to stdout
//      std::cout << (*iter)->toString();   // also works, but is probably slower

    }

    // in conclusion, add newline
    std::cout << "\n";
}
Joe
fonte
Eu não entendo a lógica aqui. Portanto, se você possui um contêiner de ponteiros e deseja remover duplicatas, como isso afeta os objetos apontados pelos ponteiros? Nenhum vazamento de memória aconteceria porque há pelo menos um ponteiro (e exatamente um neste contêiner) que aponta para eles. Bem, acho que seu método pode ter algum mérito com alguns operadores sobrecarregados estranhos ou funções de comparação estranhas que exigem consideração especial.
kccqzy
Não tenho certeza se eu entendo o seu ponto. Tome um caso simples de um vetor <int *>, onde os 4 ponteiros apontam para números inteiros {1, 2. 2, 3}. É ordenada, mas depois que você chama std :: unique, os 4 ponteiros são ponteiros para números inteiros {1, 2, 3, 3}. Agora você tem dois ponteiros idênticos para 3, portanto, se você chamar delete, ele fará uma exclusão duplicada. RUIM! Segundo, observe que o segundo 2 está faltando, um vazamento de memória.
joe
kccqzy, aqui está o programa de exemplo para você entender melhor minha resposta:
joe
@ joe: Mesmo depois de std::uniquevocê ter [1, 2, 3, 2], você não pode chamar delete em 2, pois isso deixaria um ponteiro para 2! => Simplesmente não chame delete nos elementos entre newEnd = std::uniquee std::endcomo você ainda possui ponteiros para esses elementos [std::begin, newEnd)!
MFH
2
@ ArneVogel: Para valores triviais de "funciona bem", talvez. É inútil chamar uniquea vector<unique_ptr<T>>, pois o único valor duplicado que esse vetor pode conter é nullptr.
Ben Voigt
2

Com a biblioteca Ranges (disponível em C ++ 20), você pode simplesmente usar

action::unique(vec);

Observe que ele realmente remove os elementos duplicados, não apenas os move.

LF
fonte
1

Sobre os benchmarks alexK7. Eu tentei e obtive resultados semelhantes, mas quando o intervalo de valores é de 1 milhão, os casos usando std :: sort (f1) e std :: unordered_set (f5) produzem tempo semelhante. Quando o intervalo de valores é 10 milhões, f1 é mais rápido que f5.

Se o intervalo de valores for limitado e os valores não tiverem sinal int, é possível usar std :: vector, cujo tamanho corresponde ao intervalo especificado. Aqui está o código:

void DeleteDuplicates_vector_bool(std::vector<unsigned>& v, unsigned range_size)
{
    std::vector<bool> v1(range_size);
    for (auto& x: v)
    {
       v1[x] = true;    
    }
    v.clear();

    unsigned count = 0;
    for (auto& x: v1)
    {
        if (x)
        {
            v.push_back(count);
        }
        ++count;
    }
}
Mikhail Semenov
fonte
1

ordenar (v.begin (), v.end ()), v.erase (único (v.begin (), v, end ()), v.end ());

Yohanna
fonte
1

Se você estiver procurando desempenho e uso std::vector, recomendo o que este link de documentação fornece.

std::vector<int> myvector{10,20,20,20,30,30,20,20,10};             // 10 20 20 20 30 30 20 20 10
std::sort(myvector.begin(), myvector.end() );
const auto& it = std::unique (myvector.begin(), myvector.end());   // 10 20 30 ?  ?  ?  ?  ?  ?
                                                                   //          ^
myvector.resize( std::distance(myvector.begin(),it) ); // 10 20 30
Gines Hidalgo
fonte
cplusplus.com não é de forma alguma documentação oficial.
Ilya Popov
0
std::set<int> s;
std::for_each(v.cbegin(), v.cend(), [&s](int val){s.insert(val);});
v.clear();
std::copy(s.cbegin(), s.cend(), v.cbegin());
Wes
fonte
1
talvez redimensione o vetor depois de limpá-lo para que haja apenas 1 alocação de memória ao criar o vetor. Talvez prefira std :: move em vez de std :: copy para mover as entradas para o vetor em vez de copiá-las, pois o conjunto não será necessário posteriormente.
precisa
0

Se você não quiser modificar o vetor (apagar, classificar), poderá usar a biblioteca Newton . Na sub- biblioteca de algoritmos, há uma chamada de função, copy_single

template <class INPUT_ITERATOR, typename T>
    void copy_single( INPUT_ITERATOR first, INPUT_ITERATOR last, std::vector<T> &v )

então você pode:

std::vector<TYPE> copy; // empty vector
newton::copy_single(first, last, copy);

onde cópia é o vetor no qual você deseja enviar a cópia dos elementos exclusivos. mas lembre-se de empurrar os elementos para trás e não criar um novo vetor

de qualquer forma, isso é mais rápido porque você não apaga () os elementos (o que leva muito tempo, exceto quando você pop_back (), devido à reatribuição)

Eu faço alguns experimentos e é mais rápido.

Além disso, você pode usar:

std::vector<TYPE> copy; // empty vector
newton::copy_single(first, last, copy);
original = copy;

às vezes ainda é mais rápido.

Moises Rojo
fonte
1
Esta função está presente na biblioteca padrão como unique_copy.
LF
0

Código mais compreensível em: https://en.cppreference.com/w/cpp/algorithm/unique

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <cctype>

int main() 
{
    // remove duplicate elements
    std::vector<int> v{1,2,3,1,2,3,3,4,5,4,5,6,7};
    std::sort(v.begin(), v.end()); // 1 1 2 2 3 3 3 4 4 5 5 6 7 
    auto last = std::unique(v.begin(), v.end());
    // v now holds {1 2 3 4 5 6 7 x x x x x x}, where 'x' is indeterminate
    v.erase(last, v.end()); 
    for (int i : v)
      std::cout << i << " ";
    std::cout << "\n";
}

saída:

1 2 3 4 5 6 7
Jayhello
fonte
0
void removeDuplicates(std::vector<int>& arr) {
    for (int i = 0; i < arr.size(); i++)
    {
        for (int j = i + 1; j < arr.size(); j++)
        {
            if (arr[i] > arr[j])
            {
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
    }
    std::vector<int> y;
    int x = arr[0];
    int i = 0;
    while (i < arr.size())
    {
        if (x != arr[i])
        {
            y.push_back(x);
            x = arr[i];
        }
        i++;
        if (i == arr.size())
            y.push_back(arr[i - 1]);
    }
    arr = y;
}
robertlucian13
fonte
2
Bem-vindo ao StackOverflow! Por favor edite sua pergunta para adicionar uma explicação de como você código funciona, e por isso a sua equivalente ou melhor do que as outras respostas. Esta pergunta tem mais de dez anos e já tem muitas respostas boas e bem explicadas. Sem uma explicação sobre a sua, não é tão útil e tem uma boa chance de ser votado ou removido.
Das_Geek 31/10/19
-1

Aqui está o exemplo do problema de exclusão duplicada que ocorre com std :: unique (). Em uma máquina LINUX, o programa falha. Leia os comentários para obter detalhes.

// Main10.cpp
//
// Illustration of duplicate delete and memory leak in a vector<int*> after calling std::unique.
// On a LINUX machine, it crashes the progam because of the duplicate delete.
//
// INPUT : {1, 2, 2, 3}
// OUTPUT: {1, 2, 3, 3}
//
// The two 3's are actually pointers to the same 3 integer in the HEAP, which is BAD
// because if you delete both int* pointers, you are deleting the same memory
// location twice.
//
//
// Never mind the fact that we ignore the "dupPosition" returned by std::unique(),
// but in any sensible program that "cleans up after istelf" you want to call deletex
// on all int* poitners to avoid memory leaks.
//
//
// NOW IF you replace std::unique() with ptgi::unique(), all of the the problems disappear.
// Why? Because ptgi:unique merely reshuffles the data:
// OUTPUT: {1, 2, 3, 2}
// The ptgi:unique has swapped the last two elements, so all of the original elements in
// the INPUT are STILL in the OUTPUT.
//
// 130215   [email protected]
//============================================================================

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

#include "ptgi_unique.hpp"

// functor used by std::unique to remove adjacent elts from vector<int*>
struct EqualToVectorOfIntegerStar: public std::equal_to<int *>
{
    bool operator() (const int* arg1, const int* arg2) const
    {
        return (*arg1 == *arg2);
    }
};

void printVector( const std::string& msg, const std::vector<int*>& vnums);

int main()
{
    int inums [] = { 1, 2, 2, 3 };
    std::vector<int*> vnums;

    // convert C array into vector of pointers to integers
    for (size_t inx = 0; inx < 4; ++ inx)
        vnums.push_back( new int(inums[inx]) );

    printVector("BEFORE UNIQ", vnums);

    // INPUT : 1, 2A, 2B, 3
    std::unique( vnums.begin(), vnums.end(), EqualToVectorOfIntegerStar() );
    // OUTPUT: 1, 2A, 3, 3 }
    printVector("AFTER  UNIQ", vnums);

    // now we delete 3 twice, and we have a memory leak because 2B is not deleted.
    for (size_t inx = 0; inx < vnums.size(); ++inx)
    {
        delete(vnums[inx]);
    }
}

// print a line of the form "msg: 1,2,3,..,5,6,7\n", where 1..7 are the numbers in vnums vector
// PS: you may pass "hello world" (const char *) because of implicit (automatic) conversion
// from "const char *" to std::string conversion.

void printVector( const std::string& msg, const std::vector<int*>& vnums)
{
    std::cout << msg << ": ";

    for (size_t inx = 0; inx < vnums.size(); ++inx)
    {
        // insert comma separator before current elt, but ONLY after first elt
        if (inx > 0)
            std::cout << ",";
        std::cout << *vnums[inx];

    }
    std::cout << "\n";
}
Joe
fonte
PS: Também executei "valgrind ./Main10" e o valgrind não encontrou problemas. Eu recomendo todos os programadores de C ++ que usam o LINUX para usar essa ferramenta muito produtiva, especialmente se você estiver escrevendo aplicativos em tempo real que devem ser executados 24x7 e nunca vazar ou travar!
quer
O cerne do problema com std :: unique pode ser resumido por esta declaração "std :: unique retorna duplicatas em estado não especificado" !!!!! Por que o comitê de padrões fez isso, eu nunca vou saber. Membros do comitê .. algum comentário ???
joe
1
Sim, "std :: unique retorna duplicatas em estado não especificado". Portanto, simplesmente não confie em uma matriz que foi "única" para gerenciar manualmente a memória! A maneira mais simples de fazer isso é usar std :: unique_ptr em vez de ponteiros brutos.
alexk7
Isso parece ser uma resposta a uma resposta diferente; ele não responde à pergunta (na qual o vectorcontém números inteiros, não ponteiros e não especifica um comparador).
Toby Speight
-2
void EraseVectorRepeats(vector <int> & v){ 
TOP:for(int y=0; y<v.size();++y){
        for(int z=0; z<v.size();++z){
            if(y==z){ //This if statement makes sure the number that it is on is not erased-just skipped-in order to keep only one copy of a repeated number
                continue;}
            if(v[y]==v[z]){
                v.erase(v.begin()+z); //whenever a number is erased the function goes back to start of the first loop because the size of the vector changes
            goto TOP;}}}}

Esta é uma função que eu criei que você pode usar para excluir repetições. Os arquivos de cabeçalho necessários são justos <iostream>e <vector>.

GrabeS
fonte