web-dev-qa-db-fra.com

Différence entre std :: remove et erase for vector?

J'ai un doute que je voudrais clarifier dans ma tête. Je suis conscient du comportement différent pour std::vector entre erase et std::remove où le premier supprime physiquement un élément du vecteur, réduisant la taille, et l'autre déplace simplement un élément laissant la même capacité.

Est-ce juste pour des raisons d'efficacité? En utilisant erase, tous les éléments d'un std::vector seront décalés de 1, ce qui entraînera un grand nombre de copies. std::remove effectue simplement une suppression "logique" et laisse le vecteur inchangé en déplaçant des éléments. Si les objets sont lourds, cette différence pourrait avoir de l'importance, non?

29

Est-ce juste pour des raisons d'efficacité? En utilisant effacer, tous les éléments d’un vecteur std :: seront décalés de 1, ce qui entraînera un grand nombre de copies; std :: remove ne fait qu'une suppression 'logique' et laisse le vecteur inchangé en déplaçant des éléments. Si les objets sont lourds, cette différence est minime, non?

La raison d'utiliser cet idiome est exactement cela. La performance présente un avantage, mais pas dans le cas d’un effacement unique. Ce qui compte, c'est si vous devez supprimer plusieurs éléments du vecteur. Dans ce cas, le std::remove copiera chaque élément non supprimé / une seule fois vers son emplacement final, tandis que l'approche vector::erase déplacera tous les éléments de la position à la fin plusieurs fois. Considérer:

std::vector<int> v{ 1, 2, 3, 4, 5 };
// remove all elements < 5

Si vous parcouriez le vecteur en supprimant des éléments un par un, vous supprimez le 1, ce qui entraîne la copie des éléments restants décalés (4). Ensuite, vous supprimeriez 2 et décaleriez tous les éléments restants de un (3) ... Si vous voyez le motif, il s'agit d'un algorithme O(N^2).

Dans le cas de std::remove, l'algorithme gère les têtes de lecture et d'écriture et effectue une itération sur le conteneur. Pour les 4 premiers éléments, la tête de lecture sera déplacée et l'élément testé, mais aucun élément n'est copié. Seulement pour le cinquième élément, l'objet serait copié de la dernière à la première position, et l'algorithme se terminera par une copie unique et ramènera un itérateur à la deuxième position. Ceci est un algorithme O(N). Le dernier std::vector::erase avec la plage entraînera la destruction de tous les éléments restants et le redimensionnement du conteneur.

Comme d'autres l'ont mentionné, dans la bibliothèque standard, les algorithmes sont appliqués aux itérateurs et manquent de connaissances sur la séquence itérée. Cette conception est plus souple que d'autres approches sur lesquelles les algorithmes connaissent les conteneurs, en ce sens qu'une seule mise en œuvre de l'algorithme peut être utilisée avec toute séquence conforme aux exigences de l'itérateur. Par exemple, std::remove_copy_if, il peut être utilisé même sans conteneurs, en utilisant des itérateurs qui génèrent/acceptent des séquences:

std::remove_copy_if(std::istream_iterator<int>(std::cin),
                    std::istream_iterator<int>(),
                    std::ostream_iterator<int>(std::cout, " "),
                    [](int x) { return !(x%2); } // is even
                    );

Cette simple ligne de code va filtrer tous les nombres pairs de l'entrée standard et les transférer dans la sortie standard, sans exiger le chargement de tous les nombres en mémoire dans un conteneur. C’est l’avantage de la scission, l’inconvénient est que les algorithmes ne peuvent pas modifier le conteneur lui-même, mais uniquement les valeurs mentionnées par les itérateurs.

std::remove est un algorithme de la STL qui est assez agnostique pour les conteneurs. Cela nécessite certes un certain concept, mais il a été conçu pour fonctionner également avec les tableaux C, de taille statique.

8
yves Baumes

std::remove renvoie simplement un nouvel itérateur end() pour qu'il pointe vers un point après le dernier élément non supprimé (le nombre d'éléments de la valeur renvoyée à end() correspond au nombre d'éléments à supprimer, mais rien ne garantit que leurs valeurs sont identiques à celles ceux que vous supprimiez - ils sont dans un état valide mais non spécifié). Ceci est fait pour qu'il puisse fonctionner avec plusieurs types de conteneurs (essentiellement tout type de conteneur pouvant être parcouru par une variable ForwardIterator).

std::vector::erase définit le nouvel itérateur end() après le réglage de la taille. En effet, la méthode vector sait réellement comment ajuster ses itérateurs (il en va de même avec std::list::erase, std::deque::erase, etc.).

remove organise un conteneur donné pour supprimer les objets indésirables. La fonction d'effacement du conteneur gère en fait la "suppression" de la manière dont le conteneur a besoin de le faire. C'est pourquoi ils sont séparés.

6
Zac Howland

Je pense que cela a à voir avec la nécessité d'un accès direct au vecteur lui-même pour pouvoir le redimensionner. std :: remove n'a accès qu'aux itérateurs, il n'a donc aucun moyen de dire au vecteur "Hey, vous avez maintenant moins d'éléments".

Voir yves Baumes pour savoir pourquoi std :: remove est conçu de cette façon.

5
Nathan Monteleone

Oui, c'est l'essentiel. Notez que erase est également pris en charge par les autres conteneurs standard dans lesquels ses caractéristiques de performance sont différentes (par exemple, list :: erase est O (1)), alors que std::remove est indépendant du conteneur et fonctionne avec tout type d'itérateur forward. (cela fonctionne donc aussi pour les tableaux nus, par exemple).

4
Jon

Genre de. Des algorithmes tels que supprimer le travail sur les itérateurs (qui sont une abstraction pour représenter un élément dans une collection) qui ne savent pas nécessairement sur quel type de collection ils opèrent - et ne peuvent donc pas appeler des membres de la collection pour effectuer la suppression réelle.

C'est une bonne chose car cela permet aux algorithmes de fonctionner de manière générique sur n'importe quel conteneur, ainsi que sur des plages qui sont des sous-ensembles de la collection entière.

En outre, comme vous le dites, pour améliorer les performances, il n’est peut-être pas nécessaire de supprimer (et de détruire) les éléments si vous avez simplement besoin d’un accès à la position logique de fin pour pouvoir passer à un autre algorithme.

0
Duncan Smith

Les algorithmes de bibliothèque standard fonctionnent sur séquences . Une séquence est définie par une paire d'itérateurs; les premiers points sur le premier élément de la séquence et les seconds points un-après-la-fin de la séquence. C'est tout; les algorithmes ne font pas attention à l'origine de la séquence.

Les conteneurs de bibliothèque standard contiennent des valeurs de données et fournissent une paire d'itérateurs qui spécifient une séquence à utiliser par des algorithmes. Ils fournissent également des fonctions membres qui peuvent être capables d'effectuer les mêmes opérations qu'un algorithme plus efficacement en tirant parti de la structure de données interne du conteneur.

0
Pete Becker

Essayez de suivre le code pour mieux comprendre.

std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};
const auto newend (remove(begin(v), end(v), 2));

for(auto a : v){
    cout << a << " ";
}
cout << endl;
v.erase(newend, end(v));
for(auto a : v){
    cout << a << " ";
}
0
RLT