¿Cuál es la forma más eficiente de borrar duplicados y ordenar un vector?

274

Necesito tomar un vector C ++ con potencialmente muchos elementos, borrar duplicados y ordenarlo.

Actualmente tengo el siguiente código, pero no funciona.

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

¿Cómo puedo hacer esto correctamente?

Además, ¿es más rápido borrar los duplicados primero (similar al codificado anteriormente) o realizar primero la clasificación? Si realizo la ordenación primero, ¿se garantiza que permanecerá ordenada después?std::unique se ejecute?

¿O hay otra forma (quizás más eficiente) de hacer todo esto?

Kyle Ryan
fuente
3
¿Supongo que no tiene la opción de verificar antes de insertar para evitar tener engaños en primer lugar?
Joe
Correcto. Eso sería lo ideal.
Kyle Ryan
29
Sugeriría corregir el código anterior, o realmente indicaría que ES INCORRECTO. std :: unique asume que el rango ya está ordenado.
Matthieu M.

Respuestas:

584

Estoy de acuerdo con R. Pate y Todd Gardner ; unastd::set podría ser una buena idea aquí. Incluso si está atascado usando vectores, si tiene suficientes duplicados, es mejor que cree un conjunto para hacer el trabajo sucio.

Comparemos tres enfoques:

Solo usando vector, sort + unique

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

Convertir a conjunto (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() );

Convertir a conjunto (usando un constructor)

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

Así es como funcionan estos a medida que se modifican los duplicados:

comparación de enfoques vectoriales y conjuntos

Resumen : cuando el número de duplicados es lo suficientemente grande, en realidad es más rápido convertirlo a un conjunto y luego volcar los datos nuevamente en un vector .

Y por alguna razón, hacer la conversión del conjunto manualmente parece ser más rápido que usar el constructor del conjunto, al menos en los datos aleatorios del juguete que utilicé.

Nate Kohl
fuente
61
Me sorprende que el enfoque de constructor sea consistentemente mucho peor que el manual. Lo haría, aparte de una pequeña sobrecarga constante, solo haría lo manual. ¿Alguien puede explicar esto?
Ari
17
Genial, gracias por el gráfico. ¿Podría darnos una idea de cuáles son las unidades para Número de duplicados? (es decir, alrededor de qué tan grande es "lo suficientemente grande")?
Kyle Ryan
55
@ Kyle: es bastante grande. Utilicé conjuntos de datos de 1,000,000 enteros dibujados al azar entre 1 y 1000, 100 y 10 para este gráfico.
Nate Kohl
55
Creo que tus resultados son incorrectos. En mis pruebas, cuanto más elementos duplicados es el vector más rápido (comparativo), en realidad se escala al revés. ¿Compiló con optimizaciones activadas y verificaciones de tiempo de ejecución? En mi lado, el vector siempre es más rápido, hasta 100x dependiendo de la cantidad de duplicados. VS2013, cl / Ox -D_SECURE_SCL = 0.
davidnr
39
Parece que falta la descripción del eje x.
BartoszKP
72

Rehice el perfil de Nate Kohl y obtuve resultados diferentes. Para mi caso de prueba, ordenar directamente el vector siempre es más eficiente que usar un conjunto. Agregué un nuevo método más eficiente, usando ununordered_set .

Tenga en cuenta que el unordered_setmétodo solo funciona si tiene una buena función hash para el tipo que necesita unificado y ordenado. ¡Para los int, esto es fácil! (La biblioteca estándar proporciona un hash predeterminado que es simplemente la función de identidad). Además, no olvide ordenar al final ya que unordered_set es, bueno, no ordenado :)

Hice algo de investigación dentro de la sete unordered_setimplementación y descubrí que el constructor en realidad la construcción de un nuevo nodo para cada elemento, antes de comprobar su valor para determinar si en realidad se debe insertar (en aplicación de Visual Studio, por lo menos).

Aquí están los 5 métodos:

f1: solo usando vector, sort+unique

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

f2: Convertir a set(usando un constructor)

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

f3: Convertir a set(manualmente)

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

f4: Convertir a unordered_set(usando un constructor)

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

f5: Convertir a 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() );

Hice la prueba con un vector de 100,000,000 ints elegidos al azar en rangos [1,10], [1,1000] y [1,100000]

Los resultados (en segundos, más pequeño es mejor):

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
fuente
44
Para enteros, puede usar la clasificación de radix, que es mucho más rápida que std :: sort.
Changming Sun
2
sortunique#include <algorithm>
Consejo
3
@ChangmingSun Me pregunto por qué el optimizador pareció fallar en f4. Los números son dramáticamente diferentes a f5. No tiene ningún sentido para mí.
Sandthorn
1
@sandthorn Como se explicó en mi respuesta, la implementación crea un nodo (incluida la asignación dinámica) para cada elemento de la secuencia de entrada, lo cual es un desperdicio para cada valor que termina siendo un duplicado. No hay forma de que el optimizador pueda saber que podría omitir eso.
alexk7
Ah, eso me recuerda a una de las charlas de Scott Meyer sobre el CWUK escenario que tiene una naturaleza de propaganda para frenar el emplacetipo de construcción.
Sandthorn
58

std::unique solo elimina elementos duplicados si son vecinos: primero debe ordenar el vector antes de que funcione como lo desea.

std::unique está definido para ser estable, por lo que el vector aún se ordenará después de ejecutarse en él.

jskinner
fuente
42

No estoy seguro de para qué estás usando esto, así que no puedo decir esto con 100% de certeza, pero normalmente cuando pienso en un contenedor "ordenado, único", pienso en un std :: set . Podría ser mejor para su caso de uso:

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

De lo contrario, la ordenación previa a la llamada única (como señalaron las otras respuestas) es el camino a seguir.

Todd Gardner
fuente
Bien al punto! std :: set se especifica como un conjunto único ordenado. La mayoría de las implementaciones usan un árbol binario ordenado eficiente o algo similar.
notnoop
+1 Pensamiento de conjunto también. No quería duplicar esta respuesta
Tom
¿Se garantiza que std :: set esté ordenado? Tiene sentido que en la práctica lo sea, pero ¿lo exige el estándar?
MadCoder
1
Sí, ver 23.1.4.9 "La propiedad fundamental de los iteradores de contenedores asociativos es que iteran a través de los contenedores en el orden no descendente de las claves donde no se define descendente por la comparación que se utilizó para construirlos"
Todd Gardner
1
@MadCoder: No necesariamente "tiene sentido" que un conjunto se implemente de una manera ordenada. También hay conjuntos implementados utilizando tablas hash, que no están ordenados. De hecho, la mayoría de las personas prefieren usar tablas hash cuando están disponibles. Pero la convención de nomenclatura en C ++ sucede que los contenedores asociativos ordenados se denominan simplemente "set" / "map" (análogo a TreeSet / TreeMap en Java); y los contenedores asociativos hash, que quedaron fuera del estándar, se denominan "hash_set" / "hash_map" (SGI STL) o "unordered_set" / "unordered_map" (TR1) (análogo a HashSet y HashMap en Java)
newacct
22

std::uniquesolo funciona en ejecuciones consecutivas de elementos duplicados, por lo que es mejor ordenar primero. Sin embargo, es estable, por lo que su vector permanecerá ordenado.

David Seiler
fuente
18

Aquí hay una plantilla para hacerlo por usted:

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

llámalo como:

removeDuplicates<int>(vectorname);
DShook
fuente
2
+1 ¡Templatiza! - pero puede escribir removeDuplicates (vec), sin especificar explícitamente los argumentos de la plantilla
Faisal Vali
10
O incluso mejor, simplemente haga que tome iteradores con plantilla directamente (comienzo y fin), y puede ejecutarlo en otras estructuras además de un vector.
Kyle Ryan
¡Sí, plantillas! Solución rápida para listas pequeñas, estilo STL completo. +1 thx
QuantumKarl
@Kyle: solo en otros contenedores que tienen un erase()método, de lo contrario, debe devolver el nuevo iterador final y hacer que el código de llamada trunca el contenedor.
Toby Speight el
8

La eficiencia es un concepto complicado. Hay consideraciones de tiempo frente a espacio, así como mediciones generales (donde solo se obtienen respuestas vagas como O (n)) frente a respuestas específicas (por ejemplo, la clasificación de burbujas puede ser mucho más rápida que la clasificación rápida, dependiendo de las características de entrada).

Si tiene relativamente pocos duplicados, entonces la ordenación seguida de única y borrar parece el camino a seguir. Si tuviera relativamente muchos duplicados, crear un conjunto a partir del vector y dejarlo hacer el trabajo pesado podría vencerlo fácilmente.

No se concentre solo en la eficiencia del tiempo tampoco. Sort + unique + erase opera en el espacio O (1), mientras que la construcción del set opera en el espacio O (n). Y ninguno se presta directamente a una paralelización de reducción de mapas (para conjuntos de datos realmente enormes ).


fuente
¿Qué le daría un mapa / reducir la capacidad? Lo único que se me ocurre es un tipo de fusión distribuida y todavía puede usar solo un hilo en la fusión final.
Zan Lynx
1
Sí, debe tener un nodo / hilo de control. Sin embargo, puede dividir el problema tantas veces como sea necesario para establecer límites superiores en el número de subprocesos de trabajo / hijo con los que trata el subproceso de control / padre, y en el tamaño del conjunto de datos que debe procesar cada nodo hoja. No todos los problemas se resuelven fácilmente con map-reduce, simplemente quería señalar que hay personas que se ocupan de problemas de optimización similares (en la superficie, de todos modos), donde tratar con 10 terabytes de datos se llama "martes".
7

Debe ordenarlo antes de llamar uniqueporque uniquesolo elimina los duplicados que están uno al lado del otro.

editar: 38 segundos ...

David Johnstone
fuente
7

uniquesolo elimina elementos duplicados consecutivos (lo cual es necesario para que se ejecute en tiempo lineal), por lo que primero debe realizar la ordenación. Seguirá ordenado después de la llamada a unique.

Peter
fuente
7

Si no desea cambiar el orden de los elementos, puede probar esta solución:

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
fuente
Quizás use unordered_set en lugar de set (y boost :: remove_erase_if si está disponible)
gast128
4

Suponiendo que a es un vector, elimine los duplicados contiguos usando

a.erase(unique(a.begin(),a.end()),a.end());se ejecuta en tiempo O (n) .

KPMG
fuente
1
duplicados contiguos. ok, entonces necesita un std::sortprimero.
v.oddou
2

Como ya se indicó, uniquerequiere un contenedor ordenado. Además, en uniquerealidad no elimina elementos del contenedor. En cambio, se copian hasta el final, uniquedevuelve un iterador que apunta al primer elemento duplicado y se espera que llame erasepara eliminar realmente los elementos.

Max Lybbert
fuente
¿Unique requiere un contenedor ordenado o simplemente reorganiza la secuencia de entrada para que no contenga duplicados adyacentes? Pensé lo último.
@Pate, tienes razón. No requiere uno. Elimina los duplicados adyacentes.
Bill Lynch
Si tiene un contenedor que puede tener duplicados, y desea un contenedor que no tenga ningún valor duplicado en ningún lugar del contenedor, primero debe ordenar el contenedor, luego pasarlo a único y luego usar borrar para eliminar realmente los duplicados . Si simplemente desea eliminar los duplicados adyacentes, no tendrá que ordenar el contenedor. Pero terminará con valores duplicados: 1 2 2 3 2 4 2 5 2 se cambiará a 1 2 3 2 4 2 5 2 si se pasa a único sin ordenar, 1 2 3 4 5 si se ordena, pasa a único y borre .
Max Lybbert
2

El enfoque estándar sugerido por Nate Kohl, simplemente usando vector, sort + unique:

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

no funciona para un vector de punteros.

Mire cuidadosamente este ejemplo en cplusplus.com .

En su ejemplo, los "llamados duplicados" movidos al final se muestran realmente como? (valores indefinidos), porque esos "llamados duplicados" son A VECES "elementos extra" y A VECES hay "elementos faltantes" que estaban en el vector original.

Se produce un problema cuando se usa std::unique()en un vector de punteros a objetos (pérdidas de memoria, mala lectura de datos de HEAP, liberaciones duplicadas, que causan fallas de segmentación, etc.).

Aquí está mi solución al problema: reemplazar std::unique()con ptgi::unique().

Vea el archivo ptgi_unique.hpp a continuación:

// 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

Y aquí está el programa UNIT Test que utilicé para probarlo:

// 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
fuente
No entiendo la razón aquí. Entonces, si tiene un contenedor de punteros y desea eliminar duplicados, ¿cómo afecta eso a los objetos señalados por los punteros? No ocurrirán pérdidas de memoria porque hay al menos un puntero (y exactamente uno en este contenedor) que apunta a ellos. Bueno, bueno, supongo que su método podría tener algún mérito con algunos operadores extraños sobrecargados o funciones de comparación extrañas que requieren una consideración especial.
kccqzy
No estoy seguro si entiendo tu punto. Tomemos un caso simple de un vector <int *>, donde los 4 punteros apuntan a enteros {1, 2. 2, 3}. Está ordenado, pero después de llamar a std :: unique, los 4 punteros son punteros a enteros {1, 2, 3, 3}. Ahora tiene dos punteros idénticos a 3, por lo que si llama a eliminar, se elimina por duplicado. ¡MALO! Segundo, observe que el 2do 2 está fallando, una pérdida de memoria.
Joe
kccqzy, aquí está el programa de ejemplo para que entiendas mejor mi respuesta:
Joe
@joe: ¡Incluso si después de std::uniquetener [1, 2, 3, 2] no puede llamar a eliminar en 2 ya que eso dejaría un puntero colgante a 2! => ¡Simplemente no llame a eliminar en los elementos entre newEnd = std::uniquey std::endya que todavía tiene punteros a estos elementos [std::begin, newEnd)!
MFH
2
@ArneVogel: Para valores triviales de "funciona bien", tal vez. Es bastante inútil recurrir uniquea un vector<unique_ptr<T>>, ya que el único valor duplicado que puede contener ese vector es nullptr.
Ben Voigt
2

Con la biblioteca Ranges (que viene en C ++ 20) simplemente puede usar

action::unique(vec);

Tenga en cuenta que en realidad elimina los elementos duplicados, no solo los mueve.

LF
fuente
1

Sobre los puntos de referencia alexK7. Los probé y obtuve resultados similares, pero cuando el rango de valores es de 1 millón, los casos que usan std :: sort (f1) y std :: unordered_set (f5) producen un tiempo similar. Cuando el rango de valores es 10 millones, f1 es más rápido que f5.

Si el rango de valores es limitado y los valores no están firmados int, es posible usar std :: vector, cuyo tamaño corresponde al rango dado. Aquí está el 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
fuente
1

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

Yohanna
fuente
1

Si está buscando rendimiento y uso std::vector, le recomiendo el que proporciona este enlace de documentación .

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
fuente
cplusplus.com no es de ninguna manera documentación 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
fuente
1
quizás cambiar el tamaño del vector después de borrarlo para que solo haya 1 asignación de memoria al construir el vector. Tal vez prefiera std :: move en lugar de std :: copy para mover las entradas al vector en lugar de copiarlas, ya que el conjunto no será necesario más adelante.
YoungJohn
0

Si no desea modificar el vector (borrar, ordenar), puede usar la biblioteca Newton . En la sublibrary del algoritmo hay una llamada a la función, copy_single

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

así que puedes:

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

donde copy es el vector en el que desea hacer retroceder la copia de los elementos únicos. pero recuerda que empujas los elementos hacia atrás y no creas un nuevo vector

de todos modos, esto es más rápido porque no borras () los elementos (lo que lleva mucho tiempo, excepto cuando pop_back (), debido a la reasignación)

Hago algunos experimentos y es más rápido.

Además, puedes usar:

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

A veces es aún más rápido.

Moisés rojo
fuente
1
Esta función está presente en la biblioteca estándar como unique_copy.
LF
0

Código más comprensible de: 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";
}

salida:

1 2 3 4 5 6 7
Jayhello
fuente
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
fuente
2
¡Bienvenido a StackOverflow! Por favor, editar su pregunta para añadir una explicación de la forma en que funciona el código, y por eso él es equivalente o mejor que las otras respuestas. Esta pregunta tiene más de diez años y ya tiene muchas respuestas buenas y bien explicadas. Sin una explicación suya, no es tan útil y tiene buenas posibilidades de ser rechazado o eliminado.
Das_Geek
-1

Este es el ejemplo del problema de eliminación de duplicados que ocurre con std :: unique (). En una máquina LINUX, el programa se bloquea. Lea los comentarios para más detalles.

// 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
fuente
PD: También ejecuté "valgrind ./Main10", y valgrind no encontró problemas. Recomiendo encarecidamente a todos los programadores de C ++ que usen LINUX que utilicen esta herramienta muy productiva, especialmente si está escribiendo aplicaciones en tiempo real que deben ejecutarse 24x7, ¡y nunca tienen fugas ni fallas!
Joe
¡El núcleo del problema con std :: unique puede resumirse en esta declaración "std :: unique devuelve duplicados en estado no especificado" !!!!! Por qué el comité de estándares hizo esto, nunca lo sabré. Comprometer a los miembros ... ¿algún comentario?
Joe
1
Sí, "std :: unique devuelve duplicados en estado no especificado". Por lo tanto, ¡simplemente no confíe en una matriz que ha sido "única" para administrar manualmente la memoria! La forma más sencilla de hacer esto es usar std :: unique_ptr en lugar de punteros sin formato.
alexk7
Esto parece ser una respuesta a una respuesta diferente; no responde la pregunta (en la que vectorcontiene enteros, no punteros, y no especifica un comparador).
Toby Speight el
-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 es una función que creé que puedes usar para eliminar repeticiones. Los archivos de encabezado necesarios son just <iostream>y <vector>.

GrabeS
fuente