web-dev-qa-db-de.com

Unterschied zwischen std :: entfernen und für Vektor löschen?

Ich habe einen Zweifel, den ich in meinem Kopf klären möchte. Ich bin mir des unterschiedlichen Verhaltens für std::vector zwischen erase und std::remove bewusst, wobei das erste Element ein Element physisch aus dem Vektor entfernt, die Größe reduziert und das andere Element nur ein Element bewegt, wobei die Kapazität gleich bleibt.

Ist das nur aus Effizienzgründen? Bei Verwendung von erase werden alle Elemente in einem std::vector um 1 verschoben, wodurch eine große Anzahl von Kopien entsteht. std::remove löscht nur eine "logische" Löschung und lässt den Vektor durch Verschieben unverändert. Wenn die Objekte schwer sind, könnte dieser Unterschied eine Rolle spielen, oder?

Ist das nur aus Effizienzgründen? Durch das Löschen werden alle Elemente in einem std :: vector um 1 verschoben, wodurch eine große Anzahl von Kopien entsteht. std :: remove löscht nur "logisch" und lässt den Vektor durch Verschieben unverändert. Wenn die Objekte schwer sind, ist der Unterschied nicht so wichtig, oder?

Der Grund für die Verwendung dieses Idioms ist genau das. Es gibt einen Leistungsvorteil, jedoch nicht bei einer einzigen Löschung. Es ist wichtig, ob Sie mehrere Elemente aus dem Vektor entfernen müssen. In diesem Fall kopiert der std::remove jedes nicht entfernte Element nur einmal an seinen endgültigen Ort, während der vector::erase-Ansatz alle Elemente mehrmals von der Position an das Ende verschieben würde. Erwägen:

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

Wenn Sie nacheinander die Vektorentfernungselemente durchgingen, würden Sie die 1 entfernen, wodurch Kopien der übrigen Elemente verschoben werden (4). Dann würden Sie 2 entfernen und alle verbleibenden Elemente um eins (3) verschieben. Wenn Sie das Muster sehen, handelt es sich um einen O(N^2)-Algorithmus.

Im Fall von std::remove unterhält der Algorithmus einen Lese- und Schreibkopf und durchläuft den Container. Bei den ersten 4 Elementen wird der Lesekopf verschoben und das Element getestet, es wird jedoch kein Element kopiert. Nur für das fünfte Element wird das Objekt von der letzten zur ersten Position kopiert, und der Algorithmus wird mit einer einzelnen Kopie abgeschlossen und ein Iterator wird an die zweite Position zurückgegeben. Dies ist ein O(N)-Algorithmus. Durch den späteren std::vector::erase mit dem Bereich werden alle übrigen Elemente zerstört und die Größe des Containers geändert.

Wie andere bereits erwähnt haben, werden Algorithmen in der Standardbibliothek auf Iteratoren angewendet und es fehlt ihr Wissen über die Sequenz, die iteriert wird. Dieser Entwurf ist flexibler als andere Ansätze, bei denen Algorithmen die Container kennen, indem eine einzelne Implementierung des Algorithmus mit einer beliebigen Sequenz verwendet werden kann, die den Iteratoranforderungen entspricht. Betrachten Sie zum Beispiel std::remove_copy_if, es kann auch ohne Container verwendet werden, indem Iteratoren verwendet werden, die Sequenzen erzeugen/akzeptieren:

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

Diese einzige Codezeile filtert alle geraden Zahlen aus der Standardeingabe heraus und speichert sie in die Standardausgabe, ohne dass alle Nummern in den Speicher eines Containers geladen werden müssen. Dies ist der Vorteil der Aufteilung, der Nachteil besteht darin, dass die Algorithmen nicht den Container selbst ändern können, sondern nur die Werte, auf die sich die Iteratoren beziehen.

std::remove ist ein Algorithmus aus der STL, der durchaus Container-Agnostiker ist. Dies erfordert zwar ein gewisses Konzept, aber es wurde auch für C-Arrays entwickelt, deren Größe statisch ist.

8
yves Baumes

std::remove gibt einfach einen neuen end()-Iterator zurück, der auf eins hinter dem letzten nicht entfernten Element zeigt (die Anzahl der Elemente vom zurückgegebenen Wert für end() stimmt mit der Anzahl der zu entfernenden Elemente überein. Es kann jedoch nicht garantiert werden, dass ihre Werte gleich sind Die, die Sie entfernt haben - sie befinden sich in einem gültigen, jedoch nicht näher definierten Zustand. Dies ist so, dass mehrere Containertypen verwendet werden können (grundsätzlich jeder Containertyp, den eine ForwardIterator durchlaufen kann).

std::vector::erase setzt den neuen end()-Iterator nach dem Anpassen der Größe. Dies liegt daran, dass die vector-Methode tatsächlich weiß, wie mit der Einstellung der Iteratoren umzugehen ist (das gleiche kann mit std::list::erase, std::deque::erase usw. durchgeführt werden).

remove organisiert einen bestimmten Container, um unerwünschte Objekte zu entfernen. Die Löschfunktion des Containers erledigt das "Entfernen" so, wie es für den Container erforderlich ist. Deshalb sind sie getrennt.

6
Zac Howland

Ich denke, es hat damit zu tun, dass man direkten Zugriff auf den Vektor selbst haben muss, um die Größe ändern zu können. std :: remove hat nur Zugriff auf die Iteratoren, so dass es nicht möglich ist, dem Vektor "Hey, Sie haben jetzt weniger Elemente" zu sagen.

Sehen Sie sich die Antwort von yves Baumes an, warum std :: remove so gestaltet ist.

5

Ja, das ist das Wesentliche. Beachten Sie, dass erase auch von den anderen Standardcontainern unterstützt wird, deren Leistungsmerkmale unterschiedlich sind (z. B. list :: erase ist O (1)), während std::remove containeragnostisch ist und mit jedem Typ von Forward-Iterator funktioniert (so funktioniert es zB auch für bloße Arrays).

4
Jon

So'ne Art. Algorithmen wie das Entfernen von Arbeit an Iteratoren (die eine Abstraktion darstellen, um ein Element in einer Auflistung darzustellen), die nicht notwendigerweise wissen, mit welcher Art von Auflistung sie arbeiten, und daher keine Mitglieder der Auflistung aufrufen können, um die tatsächliche Entfernung durchzuführen.

Dies ist gut, da Algorithmen generisch für jeden Container und auch für Bereiche arbeiten können, die Teilmengen der gesamten Sammlung sind.

Wie Sie sagen, ist es aus Performance-Gründen nicht unbedingt erforderlich, die Elemente tatsächlich zu entfernen (und zu löschen), wenn Sie lediglich auf die logische Endposition zugreifen müssen, um sie an einen anderen Algorithmus weiterzuleiten.

0
Duncan Smith

Standard-Bibliotheksalgorithmen arbeiten mit Sequenzen . Eine Sequenz wird durch ein Paar von Iteratoren definiert. Der erste Punkt ist auf das erste Element in der Sequenz und der zweite Punkt hinter dem Ende der Sequenz. Das ist alles; Algorithmen ist es egal, woher die Sequenz kommt.

Standard-Bibliothekscontainer enthalten Datenwerte und stellen ein Paar von Iteratoren bereit, die eine Sequenz für die Verwendung durch Algorithmen angeben. Sie bieten auch Member-Funktionen, die möglicherweise dieselben Operationen wie ein Algorithmus ausführen können, indem sie die interne Datenstruktur des Containers nutzen.

0
Pete Becker

Versuchen Sie folgenden Code, um ein besseres Verständnis zu erhalten.

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