Quelle est la façon la plus efficace d'effacer les doublons et de trier un vecteur?

274

J'ai besoin de prendre un vecteur C ++ avec potentiellement beaucoup d'éléments, d'effacer les doublons et de le trier.

J'ai actuellement le code ci-dessous, mais cela ne fonctionne pas.

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

Comment puis-je le faire correctement?

De plus, est-il plus rapide d'effacer les doublons en premier (similaire au code ci-dessus) ou d'effectuer le tri en premier? Si j'effectue d'abord le tri, est-il garanti de rester trié après std::uniqueson exécution?

Ou existe-t-il une autre façon (peut-être plus efficace) de faire tout cela?

Kyle Ryan
la source
3
Je suppose que vous n'avez pas la possibilité de vérifier avant l'insertion pour éviter d'avoir des dupes en premier lieu?
Joe
Correct. Ce serait idéal.
Kyle Ryan
29
Je suggérerais de corriger le code ci-dessus, ou indiquerais vraiment qu'il est FAUX. std :: unique suppose que la plage est déjà triée.
Matthieu M.

Réponses:

584

Je suis d'accord avec R. Pate et Todd Gardner ; un std::setpourrait être une bonne idée ici. Même si vous êtes bloqué à l'aide de vecteurs, si vous avez suffisamment de doublons, vous feriez mieux de créer un ensemble pour faire le sale boulot.

Comparons trois approches:

En utilisant simplement le vecteur, trier + unique

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

Convertir en ensemble (manuellement)

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 en ensemble (à l'aide d'un constructeur)

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

Voici comment ceux-ci fonctionnent lorsque le nombre de doublons change:

comparaison des approches vectorielles et définies

Résumé : lorsque le nombre de doublons est suffisamment important, il est en fait plus rapide de convertir en un ensemble, puis de sauvegarder les données dans un vecteur .

Et pour une raison quelconque, faire la conversion d'ensemble manuellement semble être plus rapide que d'utiliser le constructeur d'ensemble - au moins sur les données aléatoires de jouets que j'ai utilisées.

Nate Kohl
la source
61
Je suis choqué que l'approche constructeur soit toujours pire que celle manuelle. Vous feriez cela à part quelques minuscules frais généraux constants, cela ferait juste la chose manuelle. Quelqu'un peut-il expliquer cela?
Ari
17
Cool, merci pour le graphique. Pourriez-vous donner une idée de ce que sont les unités pour le nombre de doublons? (c.-à-d. autour de quelle taille est «assez grand»)?
Kyle Ryan
5
@Kyle: C'est assez grand. J'ai utilisé des ensembles de données de 1 000 000 d'entiers tirés au hasard entre 1 et 1 000, 100 et 10 pour ce graphique.
Nate Kohl le
5
Je pense que vos résultats sont faux. Dans mes tests, plus les éléments sont dupliqués, plus le vecteur (comparatif) est rapide, en fait, il évolue dans l'autre sens. Avez-vous compilé avec des optimisations activées et des contrôles d'exécution désactivés?. De mon côté, le vecteur est toujours plus rapide, jusqu'à 100x selon le nombre de doublons. VS2013, cl / Ox -D_SECURE_SCL = 0.
davidnr
39
La description de l'axe des x semble manquer.
BartoszKP
72

J'ai refait le profilage de Nate Kohl et j'ai obtenu des résultats différents. Pour mon cas de test, trier directement le vecteur est toujours plus efficace que d'utiliser un ensemble. J'ai ajouté une nouvelle méthode plus efficace, en utilisant un unordered_set.

Gardez à l'esprit que la unordered_setméthode ne fonctionne que si vous avez une bonne fonction de hachage pour le type dont vous avez besoin unique et trié. Pour les pouces, c'est facile! (La bibliothèque standard fournit un hachage par défaut qui est simplement la fonction d'identité.) N'oubliez pas non plus de trier à la fin car unordered_set est, eh bien, non ordonné :)

Je l' ai fait quelques recherches dans la setet la unordered_setmise en œuvre et a découvert que le constructeur construction en fait un nouveau noeud pour chaque élément, avant de vérifier sa valeur pour déterminer si elle doit effectivement être inséré (dans la mise en œuvre Visual Studio, au moins).

Voici les 5 méthodes:

f1: Il suffit d'utiliser vector, sort+unique

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

f2: Convertir en set(à l'aide d'un constructeur)

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

f3: Convertir en set(manuellement)

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

f4: convertir en unordered_set(à l'aide d'un constructeur)

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

f5: Convertir en unordered_set(manuellement)

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

J'ai fait le test avec un vecteur de 100 000 000 d'entités choisies au hasard dans les plages [1,10], [1,1000] et [1,100000]

Les résultats (en quelques secondes, plus petit est meilleur):

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
la source
4
Pour les entiers, vous pouvez utiliser le tri radix, qui est beaucoup plus rapide que std :: sort.
Changming Sun
2
sortunique#include <algorithm>
Astuce
3
@ChangmingSun Je me demande pourquoi l'optimiseur a semblé échouer sur f4? Les chiffres sont radicalement différents de f5. Cela n'a aucun sens pour moi.
Sandthorn
1
@sandthorn Comme expliqué dans ma réponse, l'implémentation crée un nœud (y compris l'allocation dynamique) pour chaque élément de la séquence d'entrée, ce qui est inutile pour chaque valeur qui finit par être un doublon. L'optimiseur n'a aucun moyen de savoir qu'il peut ignorer cela.
alexk7
Ah, cela me rappelle l'une des discussions de Scott Meyer sur le CWUK scénario qui a une nature de propriétés pour ralentir le emplacetype de construction.
sandthorn
58

std::unique ne supprime les éléments en double que s'ils sont voisins: vous devez d'abord trier le vecteur avant qu'il ne fonctionne comme vous le souhaitez.

std::unique est défini comme étant stable, donc le vecteur sera toujours trié après avoir été exécuté sur lui.

jskinner
la source
42

Je ne sais pas à quoi vous l'utilisez, donc je ne peux pas le dire avec 100% de certitude, mais normalement quand je pense à un conteneur "trié, unique", je pense à un std :: set . Cela pourrait être mieux adapté à votre cas d'utilisation:

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

Sinon, le tri avant d'appeler unique (comme l'ont souligné les autres réponses) est la voie à suivre.

Todd Gardner
la source
Eh bien au point! std :: set est spécifié pour être un ensemble unique trié. La plupart des implémentations utilisent un arbre binaire ordonné efficace ou quelque chose de similaire.
notnoop
+1 Pensée à l'ensemble également. Je ne voulais pas dupliquer cette réponse
Tom
Le tri de std :: set est-il garanti? Il est logique que ce soit le cas en pratique, mais la norme l'exige-t-elle?
MadCoder
1
Oui, voir 23.1.4.9 "La propriété fondamentale des itérateurs de conteneurs associatifs est qu'ils parcourent les conteneurs dans l'ordre non décroissant des clés où non descendant est défini par la comparaison qui a été utilisée pour les construire"
Todd Gardner
1
@MadCoder: Il n'est pas nécessairement "logique" qu'un ensemble soit implémenté de manière triée. Il existe également des ensembles implémentés à l'aide de tables de hachage, qui ne sont pas triés. En fait, la plupart des gens préfèrent utiliser des tables de hachage lorsqu'elles sont disponibles. Mais la convention de dénomination en C ++ arrive juste que les conteneurs associatifs triés soient nommés simplement "set" / "map" (analogue à TreeSet / TreeMap en Java); et les conteneurs associatifs hachés, qui étaient exclus de la norme, sont appelés "hash_set" / "hash_map" (SGI STL) ou "unordered_set" / "unordered_map" (TR1) (analogue à HashSet et HashMap en Java)
newacct
22

std::uniquene fonctionne que sur des exécutions consécutives d'éléments en double, vous devriez donc d'abord trier. Cependant, il est stable, donc votre vecteur restera trié.

David Seiler
la source
18

Voici un modèle pour le faire pour vous:

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

appelez-le comme:

removeDuplicates<int>(vectorname);
DShook
la source
2
+1 Templatize loin! - mais vous pouvez simplement écrire removeDuplicates (vec), sans spécifier explicitement les arguments du modèle
Faisal Vali
10
Ou encore mieux, faites-le simplement prendre des itérateurs modèles directement (début et fin), et vous pouvez l'exécuter sur d'autres structures en plus d'un vecteur.
Kyle Ryan
Hells ouais, les modèles! solution rapide pour les petites listes, style STL complet. +1 thx
QuantumKarl
@Kyle - uniquement sur les autres conteneurs qui ont une erase()méthode, sinon vous devez renvoyer le nouvel itérateur de fin et demander au code appelant de tronquer le conteneur.
Toby Speight
8

L'efficacité est un concept compliqué. Il y a des considérations de temps par rapport à l'espace, ainsi que des mesures générales (où vous n'obtenez que des réponses vagues telles que O (n)) et des réponses spécifiques (par exemple, le tri à bulles peut être beaucoup plus rapide que le tri rapide, selon les caractéristiques d'entrée).

Si vous avez relativement peu de doublons, le tri suivi de l'unique et l'effacement semblent être la solution. Si vous aviez relativement plusieurs doublons, créer un ensemble à partir du vecteur et le laisser faire le gros du travail pourrait facilement le battre.

Ne vous concentrez pas seulement sur l'efficacité du temps. Tri + unique + effacement fonctionne dans l'espace O (1), tandis que la construction d'ensemble fonctionne dans l'espace O (n). Et ni l'un ni l'autre ne se prête directement à une parallélisation de réduction de carte (pour des ensembles de données vraiment énormes ).


la source
Qu'est-ce qui vous donnerait une carte / réduirait la capacité? Le seul auquel je pense est un tri par fusion distribué et vous ne pouvez toujours utiliser qu'un seul thread dans la fusion finale.
Zan Lynx
1
Oui, vous devez avoir un nœud / thread de contrôle. Cependant, vous pouvez diviser le problème autant de fois que nécessaire pour placer des limites supérieures sur le nombre de threads de travail / enfant traités par le thread de contrôle / parent et sur la taille de l'ensemble de données que chaque nœud terminal doit traiter. Tous les problèmes ne sont pas faciles à résoudre avec la réduction de carte, je voulais simplement souligner qu'il y a des gens qui font face à des problèmes d'optimisation similaires (en surface, de toute façon), où le traitement de 10 téraoctets de données est appelé "mardi".
7

Vous devez le trier avant d'appeler, uniquecar uniqueil supprime uniquement les doublons les uns à côté des autres.

montage: 38 secondes ...

David Johnstone
la source
7

uniquesupprime uniquement les éléments en double consécutifs (ce qui est nécessaire pour qu'il s'exécute en temps linéaire), vous devez donc effectuer le tri en premier. Il restera trié après l'appel à unique.

Peter
la source
7

Si vous ne souhaitez pas modifier l'ordre des éléments, vous pouvez essayer cette solution:

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
la source
Peut-être utiliser unordered_set au lieu de set (et boost :: remove_erase_if si disponible)
gast128
4

En supposant que a est un vecteur, supprimez les doublons contigus à l'aide de

a.erase(unique(a.begin(),a.end()),a.end());s'exécute en temps O (n) .

KPMG
la source
1
doublons contigus. ok, il faut donc une std::sortpremière.
v.oddou
2

Comme déjà indiqué, uniquenécessite un conteneur trié. De plus, uniquene supprime pas réellement les éléments du conteneur. Au lieu de cela, ils sont copiés à la fin, uniquerenvoie un itérateur pointant vers le premier élément en double de ce type, et vous êtes censé appeler erasepour supprimer réellement les éléments.

Max Lybbert
la source
Unique nécessite-t-il un conteneur trié ou réorganise-t-il simplement la séquence d'entrée afin qu'elle ne contienne pas de doublons adjacents? J'ai pensé à ce dernier.
@Pate, tu as raison. Il n'en a pas besoin. Il supprime les doublons adjacents.
Bill Lynch
Si vous avez un conteneur qui peut avoir des doublons et que vous voulez un conteneur qui n'a pas de valeurs dupliquées n'importe où dans le conteneur, vous devez d'abord trier le conteneur, puis le passer à unique, puis utiliser effacer pour supprimer les doublons. . Si vous souhaitez simplement supprimer les doublons adjacents, vous n'aurez pas à trier le conteneur. Mais vous vous retrouverez avec des valeurs dupliquées: 1 2 2 3 2 4 2 5 2 sera changé en 1 2 3 2 4 2 5 2 s'il est passé à unique sans tri, 1 2 3 4 5 s'il est trié, passé à unique et efface .
Max Lybbert
2

L'approche standard suggérée par Nate Kohl, en utilisant simplement vector, sort + unique:

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

ne fonctionne pas pour un vecteur de pointeurs.

Regardez attentivement cet exemple sur cplusplus.com .

Dans leur exemple, les «soi-disant doublons» déplacés à la fin sont en fait représentés par? (valeurs indéfinies), car ces "soi-disant doublons" sont PARFOIS "éléments supplémentaires" et PARFOIS il y a des "éléments manquants" qui étaient dans le vecteur d'origine.

Un problème survient lors de l'utilisation std::unique()sur un vecteur de pointeurs vers des objets (fuites de mémoire, mauvaise lecture des données de HEAP, libérations de doublons, qui provoquent des défauts de segmentation, etc.).

Voici ma solution au problème: remplacer std::unique()par ptgi::unique().

Voir le fichier ptgi_unique.hpp ci-dessous:

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

Et voici le programme UNIT Test que j'ai utilisé pour le tester:

// 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
la source
Je ne comprends pas la justification ici. Donc, si vous avez un conteneur de pointeurs et que vous souhaitez supprimer les doublons, comment cela affecte-t-il les objets pointés par les pointeurs? Aucune fuite de mémoire ne se produirait car il existe au moins un pointeur (et exactement un dans ce conteneur) qui pointe vers eux. Eh bien, eh bien, je suppose que votre méthode pourrait avoir un certain mérite avec certains opérateurs surchargés étranges ou des fonctions de comparaison étranges qui nécessitent une attention particulière.
kccqzy
Je ne sais pas si je comprends votre point. Prenons un cas simple d'un vecteur <int *>, où les 4 pointeurs pointent vers des entiers {1, 2. 2, 3}. C'est trié, mais après avoir appelé std :: unique, les 4 pointeurs sont des pointeurs vers des entiers {1, 2, 3, 3}. Maintenant, vous avez deux pointeurs identiques à 3, donc si vous appelez delete, il fait une suppression en double. MAUVAIS! Deuxièmement, notez que le 2ème 2 est en train de manquer, une fuite de mémoire.
joe
kccqzy, voici le programme d'exemple pour que vous compreniez mieux ma réponse:
joe
@joe: Même si après avoir std::uniqueeu [1, 2, 3, 2], vous ne pouvez pas appeler la suppression sur 2 car cela laisserait un pointeur suspendu à 2! => N'appelez simplement pas delete sur les éléments entre newEnd = std::uniqueet std::endcar vous avez toujours des pointeurs vers ces éléments dans [std::begin, newEnd)!
MFH
2
@ArneVogel: Pour des valeurs triviales de "fonctionne bien", peut-être. Il est plutôt inutile d'appeler uniquea vector<unique_ptr<T>>, car la seule valeur dupliquée qu'un tel vecteur puisse contenir est nullptr.
Ben Voigt
2

Avec la bibliothèque Ranges (disponible en C ++ 20), vous pouvez simplement utiliser

action::unique(vec);

Notez qu'il supprime en fait les éléments en double, pas seulement les déplacer.

LF
la source
1

À propos des benchmarks alexK7. Je les ai essayés et j'ai obtenu des résultats similaires, mais lorsque la plage de valeurs est de 1 million, les cas utilisant std :: sort (f1) et utilisant std :: unordered_set (f5) produisent un temps similaire. Lorsque la plage de valeurs est de 10 millions, f1 est plus rapide que f5.

Si la plage de valeurs est limitée et que les valeurs ne sont pas signées int, il est possible d'utiliser std :: vector, dont la taille correspond à la plage donnée. Voici le code:

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
la source
1

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

Yohanna
la source
1

Si vous recherchez des performances et une utilisation std::vector, je recommande celle fournie par ce lien de documentation .

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
la source
cplusplus.com n'est en aucun cas une documentation officielle.
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());
Nous s
la source
1
peut-être redimensionner le vecteur après l'avoir effacé afin qu'il n'y ait qu'une seule allocation de mémoire lors de la construction du vecteur. Peut-être préférez-vous std :: move au lieu de std :: copy pour déplacer les entrées dans le vecteur au lieu de les copier car l'ensemble ne sera pas nécessaire plus tard.
YoungJohn
0

Si vous ne voulez pas modifier le vecteur (effacer, trier), vous pouvez utiliser la bibliothèque Newton , dans la sous- bibliothèque d' algorithmes il y a un appel de fonction, copy_single

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

afin que vous puissiez:

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

copie est le vecteur dans lequel vous souhaitez repousser la copie des éléments uniques. mais rappelez-vous que vous repoussez les éléments et que vous ne créez pas de nouveau vecteur

de toute façon, c'est plus rapide car vous n'effacez pas () les éléments (ce qui prend beaucoup de temps, sauf lorsque vous pop_back (), à cause de la réaffectation)

Je fais des expériences et c'est plus rapide.

Vous pouvez également utiliser:

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

est parfois encore plus rapide.

Moises Rojo
la source
1
Cette fonction est présente dans la bibliothèque standard en tant que unique_copy.
LF
0

Code plus compréhensible depuis: 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";
}

sortie:

1 2 3 4 5 6 7
Jayhello
la source
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
la source
2
Bienvenue sur StackOverflow! Veuillez modifier votre question pour ajouter une explication sur le fonctionnement de votre code et pourquoi il est équivalent ou meilleur que les autres réponses. Cette question a plus de dix ans et contient déjà de nombreuses bonnes réponses bien expliquées. Sans explication sur la vôtre, ce n'est pas aussi utile et a de bonnes chances d'être déclassé ou supprimé.
Das_Geek
-1

Voici l'exemple du problème de suppression en double qui se produit avec std :: unique (). Sur une machine LINUX, le programme plante. Lisez les commentaires pour plus de détails.

// 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
la source
PS: J'ai également exécuté "valgrind ./Main10", et valgrind n'a trouvé aucun problème. Je recommande FORTEMENT à tous les programmeurs C ++ utilisant LINUX d'utiliser cet outil très productif, surtout si vous écrivez des applications en temps réel qui doivent fonctionner 24h / 24 et 7j / 7, sans jamais fuir ni planter!
joe
Le cœur du problème avec std :: unique peut être résumé par cette déclaration "std :: unique renvoie des doublons dans un état non spécifié" !!!!! Pourquoi le comité des normes a fait cela, je ne le saurai jamais. Membres du comité .. des commentaires ???
joe
1
Oui, "std :: unique renvoie des doublons dans un état non spécifié". Donc, ne comptez tout simplement pas sur un tableau qui a été "unique" pour gérer manuellement la mémoire! La façon la plus simple de le faire est d'utiliser std :: unique_ptr au lieu de pointeurs bruts.
alexk7
Cela semble être une réponse à une réponse différente; il ne répond pas à la question (dans laquelle le vectorcontient des entiers, pas des pointeurs, et ne spécifie pas de comparateur).
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;}}}}

Il s'agit d'une fonction que j'ai créée que vous pouvez utiliser pour supprimer les répétitions. Les fichiers d'en-tête nécessaires sont juste <iostream>et <vector>.

GrabeS
la source