23.4.4 Partielles Sortieren

Teilweises Sortieren bringt die M kleinsten Elemente nach vorn, der Rest bleibt unsortiert. Der Algorithmus verlangt jedoch nicht die Zahl M, sondern einen Iterator middle auf die entsprechende Position, sodass M = middle - first gilt. Die Prototypen sind:

template<class RandomAccessIt> constexpr void partial_sort(RandomAccessIt first, RandomAccessIt middle, RandomAccessIt last) ;
template<class RandomAccessIt, class Compare> constexpr void partial_sort(RandomAccessIt first, RandomAccessIt middle, RandomAccessIt last, Compare comp);
template<class R, class Comp = ranges::less, class Proj = identity> constexpr safe_iterator_t<R> partial_sort(R&& r, iterator_t<R> middle, Comp comp = {}, Proj proj = {});

Der im letzten Fall zurückgegebene Iterator ist r.end(). Die Komplexität ist ca. O(NlogM). Der Programmauszug für einen Vektor v zeigt die teilweise Sortierung. Im Ergebnis sind in der ersten Hälfte alle Elemente kleiner als in der zweiten. In der ersten Hälfte sind sie darüber hinaus sortiert, in der zweiten jedoch nicht.

partial_sort(v.begin(), v.begin() + v.size()/2, v.end()); // bzw. ranges::partial_sort(v, v.begin() + v.size()/2);

Beide Varianten gibt es auch in einer kopierenden Form, wobei result_first beziehungsweise result_last sich auf den Zielcontainer beziehen. Die Anzahl N der sortierten Elemente ergibt sich aus der kleineren der beiden Differenzen result_last - result_first bzw. last - first. Der Ausgabebereich enthält also die ersten N Elemente in einer comp entsprechenden sortierten Reihenfolge.

template<class InputIt, class RandomAccessIt> constexpr RandomAccessIt partial_sort_copy(InputIt first, InputIt last, RandomAccessIt result_first, RandomAccessIt result_last);
template<class InputIt, class RandomAccessIt, class Compare> constexpr RandomAccessIt partial_sort_copy(InputIt first, InputIt last, RandomAccessIt result_first, RandomAccessIt result_last, Compare comp);

Der zurückgegebene Random-Access-Iterator zeigt auf das Ende des beschriebenen Bereichs, also auf (result_first + N). Die ranges-Variante ist:

template<class R1, class R2, class Comp = ranges::less, class Proj1 = identity, class Proj2 = identity> constexpr copy_result ranges::partial_sort_copy(R1&& r1, R2&& r2, Comp comp = {}, Proj1 proj1 = {}, Proj2 proj2 = {});

copy_result enthält {in: r1.end(), out: r2.begin() + N}. Das Listing 23.45 zeigt die Anwendung der Algorithmen.

23.4.5 Das n.-größte oder n.-kleinste Element finden

Das n.-größte oder n.-kleinste Element einer Sequenz mit Random-Access-Iteratoren kann mit nth_element() gefunden werden.

template<class RandomAccessIt> constexpr void nth_element(RandomAccessIt first, RandomAccessIt nth, RandomAccessIt last);
template<class RandomAccessIt, class Compare> constexpr void nth_element(RandomAccessIt first, RandomAccessIt nth, RandomAccessIt last, Compare comp);
template<class R, class Comp = ranges::less, class Proj = identity> constexpr safe_iterator_t<R> ranges:: nth_element(R&& r, iterator_t<R> nth, Comp comp = {}, Proj proj = {});

Der Iterator nth wird auf die gewünschte Stelle gesetzt, zum Beispiel auf den Beginn des Containers. Nach Aufruf von nth_element() ist das kleinste Element an diese Stelle gerutscht. Die Reihenfolge der Elemente im Container wird also geändert. Falls nth vor Aufruf zum Beispiel auf die Position v.begin() + 6 zeigt, steht dort anschließend das siebtkleinste Element.

Nach Aufruf des Algorithmus stehen links von nth alle Elemente, die kleiner oder gleich (*nth) sind. Der Aufwand des Algorithmus ist im Durchschnitt linear (O(N)). Das Beispiel ermittelt das kleinste und das größte Element sowie den Medianwert. Der Medianwert teilt eine Folge, sodass die eine Hälfte der Werte größer gleich und die andere Hälfte der Werte kleiner gleich dem Medianwert ist.

23.4.6 Verschmelzen (merge)

Verschmelzen, auch Mischen genannt, ist ein Verfahren, zwei sortierte Sequenzen zu einer zu vereinigen. Dabei werden schrittweise die jeweils ersten Elemente beider Sequenzen verglichen und das kleinere (oder größere, je nach Sortierkriterium) Element in die Ausgabesequenz gepackt. Die Prototypen sind:

template<class InputIt1, class InputIt2, class OutputIt> constexpr OutputIt merge(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt result);
template<class InputIt1, class InputIt2, class OutputIt, class Compare> constexpr OutputIt merge(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt result, Compare comp);

Zurückgegeben wird result.end(). Der Rückgabetyp der ranges-Variante ist eine Struktur mit drei Iteratoren, wie partition_copy_result auf Seite 758.

template<class R1, class R2, class OutputIt, class Comp = ranges::less, class Proj1 = identity, class Proj2 = identity> constexpr merge_result ranges::merge(R1&& r1, R2&& r2, OutputIt result, Comp comp = {}, Proj1 proj1 = {}, Proj2 proj2 = {});

merge() setzt eine vorhandene Ausgabesequenz voraus, die ausreichend Platz haben muss. Falls eine der beiden Eingangssequenzen erschöpft ist, wird der Rest der anderen in die Ausgabe kopiert. Ein kleines Programm soll dies zeigen:

Das Ergebnis des Programms in Listing 23.47 ist:

0 1 2 3 4 5 0 1 2 3 4 5 6 7 8 9 0 0 1 1 2 2 3 3 4 4 5 5 6 7 8 9

Vom Prinzip her erlaubt das Verschmelzen sehr schnelles Sortieren der Komplexität O(NlogN) nach dem rekursiven Schema:

1.      Teile die Liste in zwei Hälften.

2.      Falls die Hälften mehr als ein Element haben, sortiere beide Hälften mit diesem Verfahren (Rekursion).

3.      Beide Hälften zur Ergebnisliste verschmelzen.

Eine nichtrekursive Variante ist natürlich möglich. Die Sortierung ist stabil. Der Nachteil besteht im notwendigen zusätzlichen Speicher für das Ergebnis. Zum Vergleich mit dem obigen Schema ist der Merge-Sort genannte Algorithmus mit den Mitteln der C++-Standardbibliothek in der Datei cppbuch/k23/sortieren/mergesort.h (nicht abgedruckt) formuliert (ohne dass er selbst dazugehört!). Der Vorteil des hier beschriebenen Algorithmus gegenüber stable_sort() besteht darin, dass nicht nur Container, die mit Random-Access-Iteratoren zusammenarbeiten, sortiert werden können. Es genügen bidirektionale Iteratoren, sodass v im obigen Programm auch eine Liste sein kann. Sie kann mit push_front() gefüllt werden. Voraussetzung ist nur, dass eine Liste buffer vorhanden ist, die mindestens so viele Elemente wie v hat. Die Anwendung auf eine Liste finden Sie in der Datei cppbuch/k23/sortieren/mergesortlist.cpp.

Verschmelzen an Ort und Stelle (inplace_merge)

Hier wird der Pufferspeicher intern und implementationsabhängig bereitgestellt, sodass man sich nicht selbst darum kümmern muss. Die Funktion inplace_merge() mischt Sequenzen so, dass das Ergebnis an die Stelle der Eingangssequenzen tritt. Die Prototypen sind:

template<class BidirectionalIt> void inplace_merge(BidirectionalIt first, BidirectionalIt middle, BidirectionalIt last);
template<class BidirectionalIt, class Compare> void inplace_merge(BidirectionalIt first, BidirectionalIt middle, BidirectionalIt last, Compare comp);
template<class R, class Comp = ranges::less, class Proj = identity> safe_iterator_t<R> ranges::inplace_merge(R&& r, iterator_t<R> middle, Comp comp = {}, Proj proj = {});

Die erste Hälfte eines Vektors wird hier mit geraden Zahlen belegt, die zweite mit ungeraden. Nach dem Verschmelzen enthält derselbe Vektor alle Zahlen, ohne dass explizit ein Ergebnisbereich angegeben werden muss:

0 2 4 6 8 10 12 14 1 3 5 7 9 11 13 15 vorher 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 nachher
23.5 Suchen und Finden
23.5.1 Element finden

Der Algorithmus find() tritt in zwei Arten auf: ohne und mit erforderlichem Prädikat (als find_if() und find_if_not()). Es wird die Position in einem Container gesucht, an der ein bestimmtes Element zu finden ist. Ggf. muss das Prädikat gelten (find_if()) oder nicht gelten (find_if_not()). Das Ergebnis ist ein Iterator, der auf die gefundene Stelle zeigt oder gleich dem Ende-Iterator end() ist. Die Prototypen sind:

template<class InputIt, class T> constexpr InputIt find(InputIt first, InputIt last, const T& value);
template<class R, class T, class Proj = identity> constexpr safe_iterator_t<R> ranges::find(R&& r, const T& value, Proj proj = {});
template<class InputIt, class Pred> constexpr InputIt find_if(InputIt first, InputIt last, Pred pred);
template<class R, class Proj = identity, Pred> constexpr safe_iterator_t<R> ranges::find_if(R&& r, Pred pred, Proj proj = {});
template<class InputIt, class Pred> constexpr InputIt find_if_not(InputIt first, InputIt last, Pred p);
template<class R, class Proj = identity, Pred> constexpr safe_iterator_t<R> ranges::find_if_not(R&& r, Pred pred, Proj p = {});

Im folgenden Beispiel wird nur die ranges-Variante von find_if() gezeigt. Es wird die erste ungerade Zahl im Vektor v gesucht. Statt der Lambda-Funktion wäre ein Funktor einsetzbar.

23.5.2 Element einer Menge in der Folge finden

Der Algorithmus findet das erste Auftreten eines Elements einer Menge innerhalb einer Sequenz. Die Prototypen sind:

template<class ForwardIt1, class ForwardIt2> constexpr ForwardIt1 find_first_of(ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2, ForwardIt2 last2);
template<class ForwardIt1, ForwardIt2, class BinaryPred> constexpr ForwardIt1 find_first_of(ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2, ForwardIt2 last2, BinaryPred pred);

Das Intervall [first1, last1) ist der zu durchsuchende Bereich, das Intervall [first2, last2) beschreibt einen Bereich mit zu suchenden Elementen. Zurückgegeben wird der erste Iterator i im zu durchsuchenden Bereich, der auf ein Element zeigt, das auch im zweiten Bereich vorhanden ist. Es sei angenommen, dass ein Iterator j auf das Element im zweiten Bereich zeigt. Dann gilt

*i == *j beziehungsweise pred(*i, *j) == true.

Falls kein Element aus dem ersten Bereich im zweiten Bereich gefunden wird, gibt der Algorithmus last1 zurück. Die Komplexität ist O(N1 ∗ N2), wenn N1 und N2 die Längen der Bereiche sind. Die ranges-Variante ist:

template<class R1, class R2, class Pred = ranges::equal_to, class Proj1 = identity, class Proj2 = identity> constexpr safe_iterator_t<R1> ranges::find_first_of(R1&& r1, R2&& r2, Pred pred = {}, Proj1 proj1 = {}, Proj2 proj2 = {});

Der Bereich r1 ist der zu durchsuchende Bereich, der Bereich r2 derjenige mit den zu suchenden Elementen. Das Programm in Listing 23.50 gibt aus:

Ist eines der Elemente der Menge (1 5 7 ) in der Folge 0 2 4 6 8 10 12 14 enthalten? Nein. Menge modifiziert: Ist eines der Elemente der Menge (1 6 7 ) in der Folge 0 2 4 6 8 10 12 14 enthalten? Ja. Element 6 ist in beiden Bereichen vorhanden. Sein erstes Vorkommen in der Folge ist Position 3.
23.5.3 Teilfolge finden

Der Algorithmus search() durchsucht eine Sequenz, ob eine zweite in ihr enthalten ist. Es wird ein Iterator auf die Position innerhalb der ersten Sequenz zurückgegeben, an der die zweite Sequenz beginnt, sofern sie in der ersten enthalten ist. Andernfalls wird ein Iterator auf die last1-Position der ersten Sequenz zurückgegeben. Die Prototypen sind:

template<class ForwardIt1, class ForwardIt2> constexpr ForwardIt1 search(ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2, ForwardIt2 last2);
template<class ForwardIt1, class ForwardIt2, class BinaryPred> constexpr ForwardIt1 search(ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2, ForwardIt2 last2, BinaryPred pred);
template<class R1, class R2, class Pred = ranges::equal_to, class Proj1 = identity, class Proj2 = identity> constexpr safe_subrange_t<R1> ranges::search(R1&& r1, R2&& r2, Pred pred = {}, Proj1 proj1 = {}, Proj2 proj2 = {});

Geht es darum, das letzte Auftreten einer Teilfolge zu finden, bietet sich der Algorithmus find_end() an (siehe unten). Ein Beispiel für search():

23.5.4 Teilfolge mit speziellem Algorithmus finden

Dieser Algorithmus leistet dasselbe wie der vorherstehende, nur dass ein spezieller Algorithmus für den Suchvorgang übergeben wird. Der Prototyp ist:

template<class ForwardIt, class Searcher> constexpr ForwardIt search(ForwardIt first, ForwardIt last, const Searcher& searcher);

Mögliche Objekte des Typs Searcher sind:

Image       default_searcher (implementierungsabhängiger Algorithmus)

Image       boyer_moore_searcher (Boyer-Moore-Algorithmus)

Image       boyer_moore_horspool_searcher (Boyer-Moore-Horspool-Algorithmus)

Diese Algorithmen eignen sich für sehr lange Folgen. Eine weitergehende Beschreibung übersteigt den Rahmen dieses Buchs.

Letztes Auftreten einer Teilfolge finden

Der Algorithmus findet das letzte Auftreten einer Teilfolge innerhalb einer Sequenz. Die Prototypen sind:

template<class ForwardIt1, class ForwardIt2> constexpr ForwardIt1 find_end(ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2, ForwardIt2 last2);
template<class ForwardIt1, class ForwardIt2, class BinaryPred> constexpr ForwardIt1 find_end(ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2, ForwardIt2 last2, BinaryPred pred);

Das Intervall [first1, last1) ist der zu durchsuchende Bereich, das Intervall [first2, last2) beschreibt die zu suchende Folge. Zurückgegeben wird der letzte Iterator im zu durchsuchenden Bereich, der auf den Beginn der Teilfolge zeigt. Falls die Teilfolge nicht gefunden wird, gibt der Algorithmus last1 zurück. Falls der zurückgegebene Iterator mit i bezeichnet wird, gilt

*(i+n) == *(first2+n) beziehungsweise pred(*(i+n), *(first2+n)) == true

für alle n im Bereich 0 bis (last2-first2). Die Komplexität ist O(N2(N1N2)), wenn N1 und N2 die Länge des zu durchsuchenden Bereichs bzw. der zu suchenden Teilfolge sind. Die ranges-Variante ist

template<class R1, class R2, class Pred = ranges::equal_to, class Proj1 = identity, class Proj2 = identity> constexpr safe_subrange_t<R1> ranges::find_end(R1&& r1, R2&& r2, Pred pred = {}, Proj1 proj1 = {}, Proj2 proj2 = {});

Die Benutzung entspricht der von search(), sodass hier nur ein Auszug eines Beispiels gezeigt wird:

23.5.5 Bestimmte benachbarte Elemente finden

Zwei gleiche, direkt benachbarte (englisch adjacent) Elemente werden mit der Funktion adjacent_find() gefunden. Es gibt auch hier zwei überladene Varianten – eine ohne und eine mit binärem Prädikat. Die erste Variante vergleicht die Elemente mit dem Gleichheitsoperator ==, die zweite benutzt das Prädikat. Die Prototypen sind:

template<class ForwardIt> constexpr ForwardIt adjacent_find(ForwardIt first, ForwardIt last);
template<class ForwardIt, class BinaryPred> constexpr ForwardIt adjacent_find(ForwardIt first, ForwardIt last, BinaryPred pred);
template<class R, class Proj = identity, Pred = ranges::equal_to> constexpr safe_iterator_t<R> ranges::adjacent_find(R&& r, Pred pred = {}, Proj proj = {});

Der zurückgegebene Iterator zeigt auf das erste der beiden Elemente, sofern ein entsprechendes Paar gefunden wird. Beispiel:

Wenn zum Beispiel nach einem doppelt so großen Nachfolger gesucht werden soll, kommt ein binäres Prädikat zum Einsatz. Der entsprechende Aufruf, um den doppelt so großen Nachfolger zu finden, lautet mit einer Lambda-Funktion:

Die Auswertung kann wie oben mit der Iterator-Differenz (iter - v.begin()) geschehen.

23.5.6 Bestimmte aufeinanderfolgende Werte finden

Der Algorithmus search_n() durchsucht eine Sequenz daraufhin, ob eine Folge von gleichen Werten in ihr enthalten ist. Die Prototypen sind:

template<class ForwardIt, class Size, class T> constexpr ForwardIt search_n(ForwardIt first, ForwardIt last, Size count, const T& value);
template<class ForwardIt, class Size, class T, class BinaryPred> constexpr ForwardIt search_n(ForwardIt first, ForwardIt last, Size count, const T& value, BinaryPred pred);

Zurückgegeben wird von der ersten Funktion der Iterator auf den Beginn der ersten Folge mit wenigstens count Werten, die gleich value sind. Falls eine derartige Folge nicht gefunden wird, gibt die Funktion last zurück. Die zweite Funktion prüft nicht auf Gleichheit, sondern wertet das binäre Prädikat aus. Im Erfolgsfall muss für wenigstens count aufeinanderfolgende Werte X das Prädikat binary_pred(X, value) gelten. Die ranges-Variante ist

template<class R, class T, class Pred = ranges::equal_to, class Proj = identity> constexpr safe_subrange_t<R> ranges::search_n(R&& r, ptrdiff_t count, const T& value, Pred pred = {}, Proj proj = {});

Im Listing 23.55 wird erst gesucht, ob der Wert 4 mindestens dreimal nacheinander auftritt. Dann wird gesucht, ob es mindestens dreimal nacheinander Werte gibt, die größer als vier sind:

23.5.7 Binäre Suche

Alle Algorithmen dieses Abschnitts sind Variationen der binären Suche. Wenn ein wahlfreier Zugriff mit einem Random-Access-Iterator auf eine sortierte Folge mit n Elementen möglich ist, ist die binäre Suche sehr schnell. Es werden maximal 1+log2n Zugriffe benötigt, um das Element zu finden oder festzustellen, dass es nicht vorhanden ist. Falls ein wahlfreier Zugriff nicht möglich ist, wie zum Beispiel bei einer Liste, in der man sich von Element zu Element hangeln muss, um ein bestimmtes Element zu finden, ist die Zugriffszeit von der Ordnung O(n). Die C++-Standardbibliothek stellt die Algorithmen binary_search, lower_bound, upper_bound und equal_range bereit, die im Zusammenhang mit dem Suchen und Einfügen in sortierte Folgen sinnvoll sind und sich algorithmisch sehr ähneln:

binary_search

template<class ForwardIt, class T> constexpr bool binary_search(ForwardIt first, ForwardIt last, const T& value);
template<class ForwardIt, class T, class Compare> constexpr bool binary_search(ForwardIt first, ForwardIt last, const T& value, Compare comp);
template<class R, class T, class Proj = identity, Comp = ranges::less> constexpr bool ranges::binary_search(R&& r, const T& value, Comp comp = {}, Proj proj = {});

Dies ist die eigentliche binäre Suche. Die Funktion gibt true zurück, falls der Wert value gefunden wird. In der ersten Variante wird der Operator < benutzt, indem die Beziehung

(!(*i < value) && !(value < *i))

betrachtet wird (Äquivalenz). i ist ein Iterator im Bereich [first, last). Falls ein Bereich mit einem Vergleichsobjekt comp sortiert wurde, muss dies bei der Suche berücksichtigt werden (zweite Variante). Es wird entsprechend

(!comp(*i, value) && !comp(value, *i))

ausgewertet. Die Äquivalenzbeziehung entspricht nicht in jedem Fall der Gleichheit. Beim Vergleich ganzer Zahlen mit dem <-Operator fallen beide Begriffe zusammen. Aber: Im Duden werden Umlaute bei der Sortierung wie Selbstlaute behandelt. Die Wörter Mucke und Mücke stehen beide vor dem Wort mucken. Obwohl ungleich, sind sie doch äquivalent bezüglich der Sortierung. Ein Beispiel für binary_search() wird nach Vorstellung der nächsten drei Algorithmen gezeigt.

lower_bound

Dieser Algorithmus findet die erste Stelle, an der ein Wert value eingefügt werden kann, ohne die Sortierung zu stören. Der zurückgegebene Iterator, er sei hier i genannt, zeigt auf diese Stelle, sodass ein Einfügen ohne weitere Suchvorgänge mit insert(i, value) möglich ist. Für alle Iteratoren j im Bereich [first, i) gilt, dass *j < value ist bzw. comp(*j, value) == true. Die Prototypen sind:

template<class ForwardIt, class T> constexpr ForwardIt lower_bound(ForwardIt first, ForwardIt last, const T& value);
template<class ForwardIt, class T, class Compare> constexpr ForwardIt lower_bound(ForwardIt first, ForwardIt last, const T& value, Compare comp);
template<class R, class T, class Proj = identity, Comp = ranges::less> constexpr safe_iterator_t<R> ranges::lower_bound(R&& r, const T& value, Comp comp = {}, Proj proj = {});

upper_bound

Dieser Algorithmus findet die letzte Stelle, an der ein Wert value eingefügt werden kann, ohne die Sortierung zu stören. Der zurückgegebene Iterator i zeigt auf diese Stelle, sodass ein schnelles Einfügen mit insert(i, value) möglich ist. Die Prototypen sind:

template<class ForwardIt, class T> constexpr ForwardIt upper_bound(ForwardIt first, ForwardIt last, const T& value);
template<class ForwardIt, class T, class Compare> constexpr ForwardIt upper_bound(ForwardIt first, ForwardIt last, const T& value, Compare comp);
template<class R, class T, class Proj = identity, Comp = ranges::less> constexpr safe_iterator_t<R> ranges::upper_bound(R&& r, const T& value, Comp comp = {}, Proj proj = {});

equal_range

Dieser Algorithmus ermittelt den größtmöglichen Bereich, innerhalb dessen an jeder beliebigen Stelle ein Wert value eingefügt werden kann, ohne die Sortierung zu stören. Bezüglich der Sortierung enthält dieser Bereich also äquivalente Werte. Die Prototypen sind:

template<class ForwardIt, class T> constexpr pair<ForwardIt, ForwardIt> equal_range(ForwardIt first, ForwardIt last, const T& value);
template<class ForwardIt, class T, class Compare> constexpr pair<ForwardIt, ForwardIt> equal_range(ForwardIt first, ForwardIt last, const T& value, Compare comp);

Die Elemente p.first und p.second des zurückgegebenen Iteratorpaars, hier p genannt, begrenzen den Bereich. Für jeden Iterator k, der die Bedingung p.first k p.second erfüllt, ist schnelles Einfügen mit insert(k, value) möglich.

template<class R, class T, class Proj = identity, Comp = ranges::less> constexpr safe_subrange_t<R> ranges::equal_range(R&& r, const T& value, Comp comp = {}, Proj proj = {});

Das zurückgegebene Ergebnis ist ein Bereich von Iteratoren. Die range-Varianten der beschriebenen Algorithmen werden anhand des Listings 23.56 demonstriert, wobei die Funktion upper_bound() wegen der Ähnlichkeit mit lower_bound() nicht aufgeführt ist. Die Sortierung des Containers muss gewährleistet sein, weil alle Algorithmen dieses Abschnitts dies voraussetzen.

23.6 Mengenoperationen auf sortierten Strukturen

Die folgenden Algorithmen beschreiben die grundlegenden Mengenoperationen wie Vereinigung, Durchschnitt usw. auf sortierten Strukturen. In der C++-Standardbibliothek basiert auch die Klasse set auf sortierten Strukturen (siehe Abschnitt 27.4.1). Die Komplexität der Algorithmen ist O(N1 + N2), wobei N1 und N2 die jeweilige Anzahl der Elemente der beteiligten Mengen sind. Mengenoperationen auf sortierten Strukturen sind nur unter bestimmten Bedingungen sinnvoll und Randbedingungen sind zu beachten.

Image       Standardcontainer aus Kapitel 27: vector, list, deque

Image       Der Ergebniscontainer bietet ausreichend Platz. Nachteil: Nach dem Ende der Ergebnissequenz stehen noch alte Werte im Container, falls der Platz mehr als genau ausreichend ist.

Image       Der Output-Iterator darf nicht identisch mit v1.begin() oder v2.begin() sein. v1 und v2 sind die zu verknüpfenden Mengen.

Image       Der Ergebniscontainer ist leer. In diesem Fall ist ein Insert-Iterator als Output-Iterator zu nehmen.

Image       Assoziative Container: set, map
Grundsätzlich ist ein Insert-Iterator zu nehmen. Der Inhalt eines Elements darf nicht direkt, das heißt über eine Referenz auf das Element, geändert werden. So würde sich ein nicht einfügender Output-Iterator verhalten und die Sortierung innerhalb des Containers und damit seine Integrität würde verletzt.

23.6.1 Teilmengenrelation

Die Funktion includes gibt an, ob jedes Element einer zweiten sortierten Struktur S2 in der ersten Struktur S1 enthalten ist, also eine Teilmenge der ersten ist. Der Rückgabewert ist true, falls S2 ⊆ S1 gilt, ansonsten false. Die Prototypen sind:

template<class InputIt1, class InputIt2> constexpr bool includes(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2);
template<class InputIt1, class InputIt2, class Compare> constexpr bool includes(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, Compare comp);
template<class R1, class R2, class Proj1 = identity, class Proj2 = identity, class Comp = ranges::less> constexpr bool ranges::includes(R1&& r1, R2&& r2, Comp comp = {}, Proj1 proj1 = {}, Proj2 proj2 = {});

Das Listing 23.57 initialisiert einige set-Objekte als sortierte Strukturen. Man kann an deren Stelle natürlich auch schlichte Vektoren nehmen, vorausgesetzt, sie sind sortiert. Weil das Beispiel in den weiteren Abschnitten aufgegriffen wird, enthält es bereits hier mehr, als für includes() notwendig ist. Im Unterschied zu den meisten Beispielen dieses Abschnitts enthalten die folgenden Listings die Algorithmen aus dem Namespace std, nicht die aus dem Namespace std::ranges. Der Grund ist, dass die Ergebnisse ebenfalls Sets sein sollen. Die zum Erzeugen verwendete inserter()-Funktion bestückt die Ergebnismenge mit genau der richtigen Anzahl von Einträgen, kann aber bei den ranges-Algorithmen nicht verwendet werden. Letztere würden als Ergebnistyp, für den ein Ausgabe-Iterator angegeben wird, einen Vektor oder ein C-Array akzeptieren, die von vornherein mit ausreichend Platz ausgestattet sind. Nicht genutzter Platz bleibt allerdings bestehen, d.h., ab einem bestimmten Index gibt es undefinierte Elemente. Meines Erachtens ist es sinnvoll, wenn Operationen auf Sets selbst wieder Sets ergeben.

23.6.2 Vereinigung

Die Funktion set_union bildet eine sortierte Struktur, in der alle Elemente enthalten sind, die in wenigstens einer von zwei anderen sortierten Strukturen S1 und S2 vorkommen. Es wird die Vereinigung beider Strukturen gebildet:

S = S1 ∪ S2

Voraussetzung ist, dass die aufnehmende Struktur genügend Platz bietet oder dass sie leer ist und ein Insert-Iterator als Output-Iterator verwendet wird. Die Prototypen sind:

template<class InputIt1, class InputIt2, class OutputIt> constexpr OutputIt set_union(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt result);
template<class InputIt1, class InputIt2, class OutputIt, class Compare> constexpr OutputIt set_union(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt result, Compare comp);

Der zurückgegebene Iterator hat den Wert result.end();

template<class R1, class R2, class OutputIt, class Comp = ranges::less, class Proj1 = identity, class Proj2 = identity> constexpr set_result ranges::set_union(R1&& r1, R2&& r2, OutputIt result, Comp comp = {}, Proj1 proj1 = {}, Proj2 proj2 = {});

Die zurückgegebene Struktur set_result enthält drei Iteratoren mit den Werten {in1: r1.end(), in2: r2.end(), out: result.end()}.

Die Ergebnismenge result (siehe unten) ist anfangs leer. Im folgenden Beispiel muss der Output-Iterator ein Insert-Iterator sein. Dazu wird die Funktion inserter(), die auf Seite 902 beschrieben ist, in der Parameterliste aufgeführt. Sie gibt einen Insert-Iterator zurück. Nur result.begin() als Output-Iterator zu verwenden, führt bei sets zu Fehlern.

23.6.3 Schnittmenge

Die Funktion set_intersection bildet eine sortierte Struktur, in der alle Elemente enthalten sind, die sowohl in der einen als auch in der anderen von zwei sortierten Strukturen S1 und S2 vorkommen. Es wird die Schnittmenge oder der Durchschnitt beider Strukturen gebildet:

S = S1 ∩ S2

Die Prototypen sind:

template<class InputIt1, class InputIt2, class OutputIt> constexpr OutputIt set_intersection(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt result);
template<class InputIt1, class InputIt2, class OutputIt, class Compare> constexpr OutputIt set_intersection(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt result, Compare comp);
template<class R1, class R2, class OutputIt, class Comp = ranges::less, class Proj1 = identity, class Proj2 = identity> constexpr set_result ranges::set_intersection(R1&& r1, R2&& r2, OutputIt result, Comp comp = {}, Proj1 proj1 = {}, Proj2 proj2 = {});

Zu den Rückgabetypen siehe Abschnitt 23.6.2. Um die alten Ergebnisse zu löschen, wird clear() aufgerufen. Andernfalls würden sie mit ausgegeben.

23.6.4 Differenz

Die Funktion set_difference bildet eine sortierte Struktur, in der alle Elemente enthalten sind, die in der ersten Struktur S1, aber nicht in einer zweiten sortierten Struktur S2 vorkommen. Es wird die Differenz S1S2 beider Strukturen gebildet, auch als S1\S2 geschrieben. Die Prototypen sind:

template<class InputIt1, class InputIt2, class OutputIt> constexpr OutputIt set_difference(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt result);
template<class InputIt1, class InputIt2, class OutputIt, class Compare> constexpr OutputIt set_difference(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt result, Compare comp);
template<class R1, class R2, class OutputIt, class Comp = ranges::less, class Proj1 = identity, class Proj2 = identity> constexpr set_result ranges::set_difference(R1&& r1, R2&& r2, OutputIt result, Comp comp = {}, Proj1 proj1 = {}, Proj2 proj2 = {});

Zu den Rückgabetypen siehe Abschnitt 23.6.2. Das Beispiel folgt dem obigen Muster:

23.6.5 Symmetrische Differenz

Die Funktion set_symmetric_difference bildet eine sortierte Struktur, in der alle Elemente enthalten sind, die entweder in der ersten Struktur S1 oder in einer zweiten sortierten Struktur S2 vorkommen, aber nicht in beiden. Es wird die symmetrische Differenz beider Strukturen gebildet, auch als Exklusiv-Oder bezeichnet. Mit den vorangegangenen Operationen kann die symmetrische Differenz ausgedrückt werden:

S = (S1S2) (S2S1) oder S = (S1 ∪ S2) – (S2 ∩ S1). Die Prototypen sind:

template<class InputIt1, class InputIt2, class OutputIt> constexpr OutputIt set_symmetric_difference(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt result);
template<class InputIt1, class InputIt2, class OutputIt, class Compare> constexpr OutputIt set_symmetric_difference(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, OutputIt result, Compare comp);
template<class R1, class R2, class OutputIt, class Comp = ranges::less, class Proj1 = identity, class Proj2 = identity> constexpr set_result ranges::set_symmetric_difference(R1&& r1, R2&& r2, OutputIt result, Comp comp = {}, Proj1 proj1 = {}, Proj2 proj2 = {});

Zu den Rückgabetypen siehe Abschnitt 23.6.2. Das letzte Beispiel dieser Art zeigt die symmetrische Differenz:

23.7 Heap-Algorithmen

Die in Abschnitt 27.3.3 beschriebene Priority-Queue basiert auf einem binären Heap (englisch für Haufen oder Halde). Vor der Beschreibung der Heap-Algorithmen seien kurz die wichtigsten Eigenschaften der Datenstruktur Heap (nicht zu verwechseln mit dem ebenfalls Heap genannten Freispeicher) charakterisiert:

Image       Die N Elemente eines Heaps liegen in einem kontinuierlichen Array auf den Positionen 0 bis N – 1. Es wird vorausgesetzt, dass ein wahlfreier Zugriff möglich ist (Random-Access-Iterator).

Image       Die Art der Anordnung der Elemente im Array entspricht einem vollständigen binären Baum, bei dem alle Ebenen mit Elementen besetzt sind. Die einzig mögliche Ausnahme bildet die unterste Ebene, in der alle Elemente auf der linken Seite erscheinen. Abbildung 23.1 zeigt die Array-Repräsentation eines Heaps H mit 14 Elementen, wobei die Zahlen in den Kreisen die Array-Indizes darstellen (nicht die Elementwerte). Das Element H[0] ist also stets die Wurzel und jedes Element H[j] mit (j > 0) hat einen Elternknoten H[(j – 1)/2].

Abbildung 23.1: Array-Repräsentation eines Heaps (Zahl = Array-Index)

Image       Jedem Element H[j] ist eine Priorität zugeordnet, die größer oder gleich der Priorität der Kindknoten H[2j + 1] und H[2j + 2] ist. Hier und im Folgenden sei zur Vereinfachung angenommen, dass große Zahlen hohe Prioritäten bedeuten. Im Allgemeinen kann es auch umgekehrt sein oder es können gänzlich andere Kriterien die Priorität bestimmen. Abbildung 23.2 zeigt beispielhafte Elementwerte eines Heaps: H[0] ist gleich 98 usw.

Abbildung 23.2: Array-Repräsentation eines Heaps (Zahl = Elementwert)

Beachten Sie, dass der Heap nicht vollständig sortiert ist, sondern dass es nur auf die Prioritätsrelation zwischen Eltern- und zugehörigen Kindknoten ankommt.

Ein Array H mit N Elementen ist genau dann ein Heap, wenn H[(j – 1)/2] ≥ H[j] für 1 ≤ j < N gilt. Daraus folgt automatisch, dass H[0] das größte Element ist. Eine Priority-Queue entnimmt einfach immer das oberste Element eines Heaps. Anschließend wird er restrukturiert, das heißt, das nächstgrößte Element wandert an die Spitze. Bezogen auf die Abbildungen 23.1 und 23.2 wäre dies das Element Nr. 2 mit dem Wert 57.

Die C++-Standardbibliothek bietet Heap-Algorithmen für alle Container, auf die mit Random-Access-Iteratoren zugegriffen werden kann.

Image       pop_heap() entfernt das Element mit der höchsten Priorität.

Image       push_heap() fügt ein Element einem vorhandenen Heap hinzu.

Image       make_heap() arrangiert alle Elemente innerhalb eines Bereichs, sodass dieser Bereich einen Heap darstellt.

Image       sort_heap() verwandelt einen Heap in eine sortierte Folge.

Diese Algorithmen müssen keine Einzelheiten über die Container wissen. Ihnen werden lediglich zwei Iteratoren übergeben, die den zu bearbeitenden Bereich markieren. Zwar ist less<T> als Prioritätskriterium vorgegeben, aber vielleicht wird ein anderes Kriterium gewünscht. Daher gibt es für jeden Algorithmus eine überladene Variante, welche die Übergabe eines Vergleichsobjekts erlaubt.

pop_heap

Die Funktion pop_heap() entnimmt ein Element aus einem Heap. Der Bereich2 [first, last) sei dabei ein gültiger Heap. Die Prototypen sind:

template<class RandomAccessIt> constexpr void pop_heap(RandomAccessIt first, RandomAccessIt last);
template<class RandomAccessIt, class Compare> constexpr void pop_heap(RandomAccessIt first, RandomAccessIt last, Compare comp);
template<class R, class Comp = ranges::less, class Proj = identity> constexpr safe_iterator_t<R> ranges::pop_heap(R&& r, Comp c = {}, Proj p = {});

ranges::pop_heap() gibt r.end() zurück. In den folgenden Listings werden die Heap-Algorithmen des Namespace std verwendet, nicht die des Namespace std::ranges. Letztere verlangen etwas mehr Codieraufwand, wie der Vergleich der Dateien heap.cpp mit heapranges.cpp im Verzeichnis cppbuch/k23/ zeigt.

Die Entnahme besteht nur darin, dass der Wert mit der höchsten Priorität, der an der Stelle first steht, mit dem Wert an der Stelle (last -1) vertauscht wird. Anschließend wird der Bereich [first, last-1) in einen Heap verwandelt. Die Komplexität von pop_heap() ist O(log(lastfirst)). Anwendung für einen Vektor v:

Listing 23.62: Heap-Operationen (Auszug aus cppbuch/k23/heap.cpp)

make_heap(v.begin(), v.end()); //gültigen Heap erzeugen (Seite 784) // Die beiden Zahlen mit der höchsten Priorität anzeigen und entnehmen: auto last = v.end(); cout << *v.begin() << ’\n’; pop_heap(v.begin(), last--); cout << *v.begin() << ’\n’; pop_heap(v.begin(), last--);

Image

Hinweis

Bitte beachten Sie, dass nicht mehr v.end() das Heap-Ende anzeigt, sondern der Iterator last. Der Bereich dazwischen ist bezüglich der Heap-Eigenschaften von v undefiniert.

Image

push_heap

Die Funktion push_heap() fügt ein Element einem vorhandenen Heap hinzu. Wie die Prototypen zeigen, werden der Funktion nur zwei Iteratoren und gegebenenfalls ein Vergleichsobjekt übergeben. Das einzufügende Element tritt hier nicht auf:

template<class RandomAccessIt> constexpr void push_heap(RandomAccessIt first, RandomAccessIt last);
template<class RandomAccessIt, class Compare> constexpr void push_heap(RandomAccessIt first, RandomAccessIt last, Compare c);
template<class R, class Comp = ranges::less, class Proj = identity> constexpr safe_iterator_t<R> ranges::push_heap(R&& r, Comp c = {}, Proj p = {});

Es muss die Vorbedingung gelten, dass der Bereich [first, last) ein gültiger Heap ist. push_heap() kümmert sich nicht selbst um den einzutragenden Wert. An die Stelle last wird deshalb vorher der auf den Heap abzulegende Wert eingetragen. Der anschließende Aufruf von push_heap(first, ++last) sorgt dafür, dass nach dem Aufruf der Bereich [first, last) ein Heap ist. Die Komplexität von push_heap() ist O(log(lastfirst)). In den Beispiel-Heap werden nun zwei Zahlen wie beschrieben eingefügt (das vorhergehende Beispiel wird fortgesetzt):

// eine »wichtige Zahl« (99) eintragen *last = 99; // 99 gehört noch nicht zum Heap, push_heap(v.begin(), ++last); // aber jetzt! *last = -1; // Eine »unwichtige Zahl« (-1) eintragen push_heap(v.begin(), ++last);

Beim Einfügen muss beachtet werden, dass last nicht über v.end() hinausläuft. Durch die Tatsache, dass bei der Entnahme immer der Wert mit der höchsten Priorität an die Spitze gesetzt wird, ist die Ausgabe sortiert:

// Ausgabe und Entfernen aller Zahlen der Priorität nach: while (last != v.begin()) { cout << *v.begin() << ’˽’; pop_heap(v.begin(), last--); }

make_heap

make_heap() sorgt dafür, dass die Heap-Bedingung für alle Elemente innerhalb eines Bereichs gilt. Die Prototypen sind:

template<class RandomAccessIt> constexpr void make_heap(RandomAccessIt first, RandomAccessIt last);
template<class RandomAccessIt, class Compare> constexpr void make_heap(RandomAccessIt first, RandomAccessIt last, Compare c);
template<class R, class Comp = ranges::less, class Proj = identity> constexpr safe_iterator_t<R> ranges::make_heap(R&& r, Comp c = {}, Proj p = {});

Die Komplexität ist proportional zur Anzahl der Elemente zwischen first und last. Das Beispiel von oben zeigt die Anwendung auf einem Vektor als Container:

make_heap(v.begin(), v.end());

sort_heap

sort_heap() verwandelt einen Heap in eine sortierte Sequenz. Die Sortierung ist nicht stabil, die Komplexität ist O(NlogN), wenn N die Anzahl der zu sortierenden Elemente ist. Die Prototypen sind:

template<class RandomAccessIt> constexpr void sort_heap(RandomAccessIt first, RandomAccessIt last);
template<class RandomAccessIt, class Compare> constexpr void sort_heap(RandomAccessIt first, RandomAccessIt last, Compare c);
template<class R, class Comp = ranges::less, class Proj = identity> constexpr safe_iterator_t<R> ranges::sort_heap(R&& r, Comp c = {}, Proj p = {});

Die Sequenz ist aufsteigend sortiert. Damit ist gemeint, dass die Elemente hoher Priorität an das Ende der Sequenz kommen:

make_heap(v.begin(), v.end()); // neuen gültigen Heap aus allen Elementen erzeugen sort_heap(v.begin(), v.end()); // und sortieren
Image

Übung

23.1 Gegeben sei die Template-Klasse Heap mit den folgenden Schnittstellen:

template<typename T, class Compare = std::less<T>> class Heap { public: Heap(const Compare& cmp = Compare()); void push(const T& t); void pop(); const T& top() const; bool empty() const; vector<T> toSortedVector() const; };

Die letzte Methode gibt den Heap-Inhalt als sortierten Vektor zurück. Implementieren Sie die Klasse unter Verwendung einiger Algorithmen des Abschnitts 23.7. Testen Sie die Klasse, indem Sie nach jeder Änderung prüfen, ob die Heap-Eigenschaft noch gilt. Dazu lässt sich der Algorithmus is_heap des folgenden Abschnitts einsetzen.

Image

is_heap

is_heap() gibt zurück, ob ein Bereich den Heap-Kriterien genügt. Gegebenenfalls kann ein Vergleichsobjekt übergeben werden.

template<class RandomAccessIt> constexpr bool is_heap(RandomAccessIt first, RandomAccessIt last);
template<class RandomAccessIt, class Compare> constexpr bool is_heap(RandomAccessIt first, RandomAccessIt last, Compare comp);
template<class R, class Comp = ranges::less, class Proj = identity> constexpr bool ranges::is_heap(R&& r, Comp c = {}, Proj p = {});

Ergänzend dazu gibt es die Algorithmen is_heap_until(first, last) und is_heap_until( first, last, comp), die den letzten Iterator im Bereich [first, last) zurückgeben, bis zu dem der Bereich als Heap angesehen werden kann.

23.8 Vergleich von Containern auch ungleichen Typs
23.8.1 Unterschiedliche Elemente finden

mismatch() überprüft zwei Container auf Übereinstimmung ihres Inhalts, wobei einige Varianten ein binäres Prädikat benutzen. Die Prototypen sind:

template<class InputIt1, class InputIt2> constexpr pair<InputIt1, InputIt2> mismatch(InputIt1 first1, InputIt1 last1, InputIt2 first2);
template<class InputIt1, class InputIt2, class BinaryPred> constexpr pair<InputIt1, InputIt2> mismatch(InputIt1 first1, InputIt1 last1, InputIt2 first2, BinaryPred pred);

Falls last2 nicht explizit angegeben wird, wird last2 == first2 + (last1 - first1) angenommen. Falls der zweite Bereich weniger Elemente hat, ist das Ergebnis fehlerhaft. Deswegen gibt es diese zusätzlichen Varianten:

template<class InputIt1, class InputIt2> constexpr pair<InputIt1, InputIt2> mismatch(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2);
template<class InputIt1, class InputIt2, class BinaryPred> constexpr pair<InputIt1, InputIt2> mismatch(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, BinaryPred pred);
template<class R1, class R2, class Pred = ranges::equal_to, class Proj1 = identity, class Proj2 = identity> constexpr mismatch_result ranges::mismatch(R1&& r1, R2&& r2, Pred pred = {}, Proj1 proj1 = {}, Proj2 proj2 = {});

Diese Varianten stellen sicher, dass nur so viele Vergleiche stattfinden, dass keiner der Bereiche überschritten wird. Die Algorithmen geben jeweils ein Paar P von Iteratoren zurück, die auf die erste Stelle der Nichtübereinstimmung in den jeweiligen korrespondierenden Containern zeigen. Im Fall der std-Algorithmen ist es ein pair-Objekt mit den Elementen {first, second}, im Fall des std::ranges-Algorithmus eine Struktur mit den Elementen {in1, in2}. Bei Übereinstimmung gilt, wobei die Größen der Bereiche mit size1 (= last1 - first1) und size2 (= last2 - first2) abgekürzt seien:

size1 <= size2 : P.first == last1 P.second = first2 + size1 size1 > size2 : P.first == first1 + size2 P.second = last2

Die Container müssen nicht vom selben Typ sein. In dem Beispiel wird ein Vektor v mit einem Set s verglichen:

Eine indexartige Position ist in einem Set nicht definiert, deswegen ist ein Ausdruck der Art (wo.second - s.begin()) ungültig. Zwar zeigt wo.second auf die Stelle der Nichtübereinstimmung in s, aber die Arithmetik ist nicht erlaubt. Wenn man die relative Nummer bezüglich des ersten Elements in s unbedingt benötigt, kann man distance() verwenden.

Die Variante mit dem binären Prädikat ist geeignet, wenn es um nicht exakte Übereinstimmung geht. Wenn etwa berechnete Resultate, die mit Rundungsfehlern behaftet sind, verglichen werden sollen, genügt es, wenn die Ergebnisse innerhalb einer gewissen Genauigkeit übereinstimmen. Im folgenden Beispiel werden zwei nicht genau gleiche Container als übereinstimmend betrachtet. Wenn der Schwellenwert 0.01 auf z.B. 0.005 abgesenkt wird, werden Unterschiede angezeigt.

23.8.2 Prüfung auf gleiche Inhalte

equal() überprüft zwei Container auf Übereinstimmung ihres Inhalts, wobei eine Variante ein binäres Prädikat benutzt. Im Unterschied zu mismatch() wird jedoch kein Hinweis auf die Position gegeben. Wie am Rückgabetyp bool erkennbar, wird nur festgestellt, ob die Übereinstimmung besteht oder nicht. Die Prototypen sind:

template<class InputIt1, class InputIt2> constexpr bool equal(InputIt1 first1, InputIt1 last1, InputIt2 first2);
template<class InputIt1, class InputIt2, class BinaryPred> constexpr bool equal(InputIt1 first1, InputIt1 last1, InputIt2 first2, BinaryPred pred);

Wie im vorherigen Abschnitt und mit derselben Begründung gibt es auch die robusteren Varianten mit einem zusätzlichen End-Iterator. Diese Varianten geben auf jeden Fall false zurück, wenn die Bereiche unterschiedlich groß sind.

template<class InputIt1, class InputIt2> constexpr bool equal(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2);
template<class InputIt1, class InputIt2, class BinaryPred> constexpr bool equal(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, BinaryPred pred);
template<class R1, class R2, class Pred = ranges::equal_to, class Proj1 = identity, class Proj2 = identity> constexpr bool ranges::equal(R1&& r1, R2&& r2, Pred pred = {}, Proj1 proj1 = {}, Proj2 proj2 = {});
23.9 Rechnen mit komplexen Zahlen: Der C++-Standardtyp complex

Tabelle 23.1 enthält die Funktionen, die mit komplexen Zahlen verwendet werden können. Komplexe Zahlen werden in den Natur- und Ingenieurwissenschaften viel verwendet. Sie bestehen aus dem Real- und dem Imaginärteil, die beide von einem der möglichen Typen für reelle Zahlen sein können (float, double, long double). Der Standarddatentyp für komplexe Zahlen ist deshalb kein Grunddatentyp, sondern zusammengesetzt. Weil die beiden Bestandteile eines complex-Objekts von einem der Typen float, double oder long double sein können, sind komplexe Zahlen als Klassen-Template realisiert.

Die für den Compiler nötigen Informationen enthält der Header <complex>. Einige Beispiele zeigen, wie mit komplexen Zahlen in C++ gerechnet wird. Die Verwendung des Suffixes i erlaubt eine abkürzende Schreibweise. Die Grundlagen für diese Technik kennen Sie von Abschnitt 8.9.2. Statt i ist auch il (für complex<long double>) oder if (für complex<float>) einsetzbar.

Tabelle 23.1: Mathematische Funktionen für complex-Zahlen

Schnittstelle

mathematische Entsprechung

T real(const C& x)

Realteil von x (= x.real())

T imag(const C& x)

Imaginärteil von x (= x.imag())

T abs(const C& x)

Betrag von x

T arg(const C& x)

Phasenwinkel von x in rad

T norm(const C& x)

Betragsquadrat von x

C conj(const C& x)

zu x konjugiert-komplexe Zahl

C proj(const C& x)

Projektion von x auf die Riemann-Kugel

C polar(const T& rho, const T& theta)

Zahl entsprechend Betrag rho und Phase theta

C acos(const C& x)

acos x

C acosh(const C& x)

acosh x

C asin(const C& x)

asin x

C asinh(const C& x)

asinh x

C atan(const C& x)

atan x

C atanh(const C& x)

atanh x

C cos(const C& x)

cos x

C cosh(const C& x)

cosh x

C exp(const C& x)

ex

C log(const C& x)

ln x

C log10(const C& x)

log10 x

C pow(const C& x, const T& y)

xy

C pow(const T& x, const C& y)

xy

C pow(const C& x, const C& y)

xy

C sin(const C& x)

sin x

C sinh(const C& x) C sqrt(const C& x)

sinh x +

C tan(const C& x)

tan x

C tanh(const C& x)

tanh x

Abkürzungen: T = einer der Typen float, double oder long double, C = complex<T>

// Beispiele für komplexe Zahlen. Anstatt double sind auch float und long double möglich. int main() { complex<double> c1; // komplexe Zahl 0.0 + 0.0i complex<double> c2(1.2, 3.4); // (1.2 + 3.4i) erzeugen complex<double> c2b = 1.2 + 3.4i; // dito mit Rechenoperation und Suffix i complex<double> imaginaer{3.7i}; // Imaginäre Zahl mit Suffix i cout << "Standard-Ausgabeformat:" << c2 << ’˽’ << c2b << ’˽’ << imaginaer << ’\n’; c1 += c2 + c1; // beispielhafte Rechenoperationen c1 = c2 * 5.0; double re = c1.real(); // Realteil ermitteln cout << re << ’\n’; // und ausgeben cout << c1.imag() << ’\n’; // Imaginärteil direkt ausgeben // Beispiele mit Hilfsfunktionen complex<double> c3 = {1.0, 2.0}; // (1.0 + 2.0i) erzeugen c1 = conj(c3); // konjugiert komplex: (1.0 - 2.0i) // Umrechnung aus Polarkoordinaten double betrag = 100.0; double phase = numbers::pi / 4.0; // π/4 = 45 Grad c1 = polar(betrag, phase); // Umrechnung in Polarkoordinaten double rho = abs(c1); // Betrag ρ double theta = arg(c1); // Winkel θ double nrm = norm(c1); // Betragsquadrat cout << "Betrag˽˽=˽" << betrag << ’\n’; cout << "rho˽˽˽˽˽=˽" << rho << ’\n’; cout << "Norm˽˽˽˽=˽" << nrm << ’\n’; cout << "Phase˽˽˽=˽" << phase << ’\n’; cout << "theta˽˽˽=˽" << theta << "˽=˽" << theta / numbers::pi * 180. << "˽Grad\n"; cout << "Komplexe˽Zahl˽eingeben.˽Erlaubte˽Formate˽(Beispiel):"˽"\n˽(1.78,˽-98.2)\n˽(1.78)\n˽1.78\n:"; cin >> c1; cout << "komplexe˽Zahl˽=˽" << c1 << ’\n’; }

Mit komplexen Zahlen kann wie mit reellen Zahlen gerechnet werden. Ausgenommen sind nur die Operatoren ++, -- und %. Auch die Prüfung auf Gleichheit (==) oder Ungleichheit (!=) ist möglich. Die anderen Vergleichsoperatoren ergeben bei komplexen Zahlen keinen Sinn.

Image

Übung

23.2 Schreiben Sie ein Programm zur Lösung der quadratischen Gleichung x2 + px + q = 0, wobei Sie komplexe Zahlen für das Ergebnis verwenden. Die Lösungsformeln: Eine komplexe Lösung ergibt sich, wenn D < 0 ist.

Image

23.10 Vermischtes
23.10.1 Erkennung eines Datums

Mithilfe eines regulären Ausdrucks soll überprüft werden, ob ein Datum der Form tt.mm.jjjj gültig ist. Dabei soll es auch möglich sein, nur eine Ziffer für den Tag bzw. den Monat einzugeben. Da hier nur die Gültigkeit interessiert und nicht, wo sich ein gültiges Datum innerhalb einer Zeichenkette befindet, genügt die Abfrage mit regex_match(). Ein passender regulärer Ausdruck (von vielen möglichen) ist \d\d?\.\d\d?\.\d\d\d\d. Das Listing 23.66 zeigt die Anwendung; auf mögliche Probleme wird danach eingegangen.

Syntax oder Semantik?

Am obigen Beispiel wird sofort ein Problem klar: 00.0.0000 ist ein ungültiges Datum, wird aber vom Programm als gültiges Datum interpretiert. Der Grund liegt darin, dass das Programm nur die vorgegebene Syntax prüft, aber die Semantik (Bedeutung) des Inhalts ignoriert. Was tun? Es gibt verschiedene Abstufungen der Prüfung:

1.      Die einfachste Syntaxprüfung ist die oben dargestellte. Um die Gültigkeit zu prüfen, müssen Tag, Monat und Jahr aus dem String extrahiert werden, um dann die Gültigkeit zu prüfen.

2.      Man kann aber auch in Erwägung ziehen, bereits bei der syntaktischen Analyse die Gültigkeit der Daten zu überprüfen. Für den Tag gibt es folgende Möglichkeiten:

Image       Wenn der Tag einstellig ist, liegt die Ziffer im Bereich [1-9].

Image       Ist die erste Ziffer eine 0, liegt die zweite im Bereich [1-9].

Image       Ist die erste Ziffer eine 1 oder eine 2, liegt die zweite im Bereich [0-9].

Image       Ist die erste Ziffer eine 3, liegt die zweite im Bereich [0-1].

Der erste und der zweite Punkt werden mit dem regulären Ausdruck 0?[1-9] realisiert, der dritte mit (1|2)\d, der vierte mit 3[01]. Der reguläre Ausdruck für den Tag einschließlich des Punkts kann damit als (0?[1-9])|((1|2)\d)|(3[01])\. formuliert werden. Für den Monat gilt:

Image       Wenn der Monat einstellig ist oder mit einer 0 beginnt, liegt die Ziffer vor dem Punkt im Bereich [1-9].

Image       Wenn der Monat zweistellig ist, kann die erste Ziffer nur eine 1 sein und die zweite liegt im Bereich [0-2].

Der reguläre Ausdruck für den Monat einschließlich des Punkts kann damit als (0?[1-9])|(1[0-2])\. formuliert werden.

3.      Mit der vorstehenden Lösung ist aber noch nicht alles erreicht, denn manche Monate haben nur 30 Tage, der Februar sogar nur 28 bzw. 29 in einem Schaltjahr. 30 oder 28 Tage lassen sich mit einigem Aufwand in der Syntax berücksichtigen, aber das Schaltjahr nur noch mit sehr großem, unverhältnismäßigem Aufwand.

Man sieht, dass der Aufwand recht hoch getrieben werden kann. Bei der Überprüfung von Dialogeingaben oder von einer Datei eingelesenen Informationen ist daher abzuwägen, wie der Gesamtaufwand aus Syntaxprüfung und inhaltlicher Prüfung minimiert werden kann.

Image

Empfehlung

Einfache reguläre Ausdrücke bevorzugen, ergänzt durch eine inhaltliche Prüfung.

Image

Diese Empfehlung entspricht dem obigen Punkt 1. Sie erspart Planungs- und Rechenzeitaufwand. Die Funktion datumok() des obigen Listings wird dazu ersetzt durch die folgende:

23.10.2 Erkennung einer IPv4-Adresse

Die Lösung dieses Problem lässt sich auf das vorhergehende zurückführen. Eine IPv4-Adresse hat vier durch einen Punkt getrennte Felder, wobei jedes Feld aus einer vorzeichenlosen Zahl mit maximal drei Ziffern besteht. Ein möglicher regulärer Ausdruck ist \d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d? oder kürzer (?:\d\d?\d?\.){3}\d\d?\d?. Es gilt die Bedingung, dass jede Zahl höchstens den Wert 255 annehmen kann. Im Folgenden ist nur die überprüfende Funktion gezeigt:

23.10.3 Erzeugen von Zufallszahlen

[ISOC++] enthält einen großen Abschnitt über die Erzeugung von (Pseudo-)Zufallszahlen. Der Abschnitt ist für dieses Buch zu umfangreich und eher aus mathematischer Sicht als aus programmiersprachlicher Sicht interessant. Deshalb beschränke ich mich auf die am häufigsten genutzten Möglichkeiten:

1.      ganze gleichverteilte Zufallszahlen

2.      reelle gleichverteilte Zufallszahlen

3.      zufällig erzeugte Wahrheitswerte

4.      normalverteilte Zufallszahlen

Die Erzeugung der Zufallszahlen wird von einem Generator übernommen. Es gibt nach [ISOC++] mehrere Generatoren. In diesem Buch wird der Zufallszahlengenerator mt19937 verwendet. Es gibt auch andere, die zum Teil mit etwas weniger Rechenzeit und Speicher auskommen, aber nicht ganz so hohen Ansprüchen an die Zufälligkeit genügen, etwa default_random_engine. Auch sie wären für die Zwecke dieses Buchs geeignet. Der Generator erzeugt unsigned int-Zahlen:

Das ist natürlich nicht komfortabel. Daher gibt es Verteilungen, denen der gewünschte Zahlenbereich mitgegeben wird. Beim Aufruf wird der entsprechende Generator übergeben, wie Sie unten in den Beispielen sehen.

Reproduzierbarkeit

Für alle gezeigten Verfahren gilt, dass die berechneten Zufallswerte reproduzierbar, also pseudo-zufällig sind. Erneutes Starten eines Programms liefert dieselbe Zahlenfolge. Entscheidend für die Reihenfolge der Zahlen ist die Initialisierung des Zufallszahlengenerators mit den Funktionen seed(unsigned seed). Die Reproduzierbarkeit erleichtert das Testen. Um bei jedem Programmstart eine andere Zahlenfolge zu erreichen, kann man den Generator mit random_device initialisieren:

std::random_device rd; std::mt19937 generator(rd());

Alternativ kann rd() der seed-Funktion übergeben werden.

Gleichverteilung ganzer Zahlen

Der Gleichverteilung ganzer Zahlen wird mit uniform_int_distribution realisiert. Dem Konstruktor der Verteilung wird der gewünschte Bereich übergeben. In vielen Beispielen dieses Buchs werden gleichverteilte ganze Zufallszahlen benötigt. Deshalb wird die Erzeugung in einer eigenen Klasse Random gekapselt. Die Datei Random.h liegt im include-Verzeichnis der Beispiele. Ein Random-Objekt wird mit den Grenzwerten bzw. dem Grenzwert initialisiert. Gegebenenfalls kann mit setSeed(size_t newseed) ein Anfangswert für eine andere Zufallsfolge eingestellt werden. Durch Überladen des ()-Operators kann das Objekt wie eine Funktion benutzt werden.

Die Eigenschaft »Funktionsobjekt« kann gut zusammen mit manchen Standardalgorithmen benutzt werden, siehe zum Beispiel generate_n() in Abschnitt 23.3.3. Im folgenden Beispiel werden 100000 Zufallszahlen zwischen 10 und 20 erzeugt. Zur Kontrolle werden die Häufigkeiten für jeden Wert und der berechnete Mittelwert ausgegeben.

Gleichverteilung reeller Zahlen

Die Gleichverteilung reeller Zahlen wird mit uniform_real_distribution realisiert. Der Datentyp kann double, long double oder float sein. Das Beispiel zeigt die Erzeugung gleichverteilter double-Zahlen im Bereich [0, 1]. Der Mittelwert liegt dann bei etwa 0.5.

Normalverteilung

Neben der Gleichverteilung ist die Normalverteilung am bekanntesten, charakterisiert durch die Gaußsche Glockenkurve. Sie wird insbesondere in der Messtechnik verwendet. So gehorchen die Abweichungen vom Sollmaß bei der industriellen Herstellung von Werkstücken der Normalverteilung. Das folgende Beispiel berechnet 1000000 normalverteilte Zufallszahlen, wobei zur Veranschaulichung das Ergebnis auf der Konsole ausgegeben wird (Abbildung 23.3).

Abbildung 23.3: Glockenkurve auf der Konsole

Zufällig erzeugte Wahrheitswerte

Manchmal möchte man, dass ein Ereignis mit einer gewissen Wahrscheinlichkeit eintritt, zum Beispiel bei der Simulation eines Sechser-Wurfs beim Würfeln. Dazu ist die Bernoulli-Verteilung geeignet. Sie liefert true mit einer Wahrscheinlichkeit p und false mit einer Wahrscheinlichkeit 1 – p. Falls p nicht angegeben wird, gilt p = 0.5.

Diese Verteilung lässt sich alternativ mit der Verteilung uniform_real_distribution realisieren, wenn der Zahlenbereich 0.0 bis 1.0 ist. Das Ergebnis ist dann true, wenn die Zufallszahl < p ist.

23.10.4 for_each — auf jedem Element eine Funktion ausführen

Die Algorithmen for_each bzw. for_each_n bewirken, dass auf jedem Element eines Containers im Intervall [first, last) bzw. [first, first + n) eine Funktion ausgeführt wird. Die Deklarationen sind:

template<class InputIt, class Function> constexpr Function for_each(InputIt first, InputIt last, Function f);
template<class InputIt, class Size, class Function> constexpr Function for_each_n(InputIt first, Size n, Function f);

f kann sowohl eine Funktion (aber keine Methode einer Klasse) als auch ein Funktionsobjekt sein und wird nach Gebrauch zurückgegeben.

template<class R, class Proj = identity, Function> constexpr for_each_result ranges::for_each(R&& r, Function f, Proj proj = {});

for_each_result ist eine Struktur mit den Elementen {in, F} und dem Wert {r.end(), std::move(f)}. Das Listing 23.75 zeigt die Ausgabe aller Elemente eines Vektors. Der Rückgabewert wird hier ignoriert, im Listing 23.76 jedoch ausgewertet.

Das Listing 23.76 zeigt die Auswertung des zurückgegebenen Funktors, der in diesem Fall alle Werte aufsummiert.

23.10.5 Verschiedene Möglichkeiten, Container-Bereiche zu kopieren

Der Algorithmus copy() kopiert die Elemente eines Quellbereichs in den Zielbereich, wobei das Kopieren am Anfang oder am Ende der Bereiche (mit copy_backward()) beginnen kann. Falls der Zielbereich nicht überschrieben, sondern in ihn eingefügt werden soll, ist als Output-Iterator ein Iterator zum Einfügen (Insert-Iterator) zu nehmen. Der Algorithmus copy() ist immer dann zu nehmen, wenn Ziel- und Quellbereich sich nicht oder so überlappen, dass der Anfang des Quellbereichs im Zielbereich liegt. result muss zu Beginn auf den Anfang des Zielbereichs zeigen. Zur Verdeutlichung der Wirkungsweise sind hier ausnahmsweise nicht die Prototypen, sondern mögliche Definitionen gezeigt:

template<class InputIt, class OutputIt> constexpr OutputIt copy(InputIt first, InputIt last, OutputIt result) { while (first != last) { *result++ = *first++; } return result; }

Der Algorithmus copy_backward() ist immer dann zu nehmen, wenn Ziel- und Quellbereich sich so überlappen, dass der Anfang des Zielbereichs im Quellbereich liegt. result muss anfangs auf das Ende des Zielbereichs zeigen.

template<class BidirectionalIt1, class BidirectionalIt2> constexpr BidirectionalIt2 copy_backward(BidirectionalIt1 first, BidirectionalIt1 last, BidirectionalIt2 result)
{ while (first != last) { *--result = *--last; } return result; }

Auch hier gilt wie allgemein in der C++-Standardbibliothek, dass last nicht die Position des letzten Elements bezeichnet, sondern die Position nach dem letzten Element. result darf niemals zwischen first und last liegen. Wie Abbildung 23.4 zeigt, sind drei Fälle zu berücksichtigen:

Abbildung 23.4: Kopieren ohne und mit Bereichsüberlappung

Image       a) Die Bereiche sind vollständig getrennt. Die Bereiche können in demselben oder in verschiedenen Containern liegen. result zeigt auf den Beginn des Zielbereichs. copy() kopiert den Quellbereich beginnend mit *first. Zurückgegeben wird result + (last - first), also die Position nach dem letzten Element des Zielbereichs.

Image       b) Die Bereiche überlappen sich so, dass der Zielbereich vor dem Quellbereich beginnt. result zeigt auf den Beginn des Zielbereichs. copy() kopiert den Quellbereich beginnend mit *first. Wie bei a) wird die Position nach dem letzten Element des Zielbereichs zurückgegeben.

Image       c) Die Bereiche überlappen sich so, dass der Zielbereich mitten im Quellbereich beginnt. Um die Daten nicht zu zerstören, muss vom Ende her beginnend kopiert werden. result zeigt auf die Position direkt nach dem Ende des Zielbereichs. copy_backward() kopiert den Quellbereich, indem zuerst *(--last) an die Stelle --result kopiert wird. Hier wird result - (last - first) zurückgegeben, also die Position des zuletzt kopierten Elements im Zielbereich.

Kopieren mit Bedingung

In Analogie zu anderen Algorithmen gibt es auch den Algorithmus copy_if(), der nur Elemente kopiert, die einer bestimmten Bedingung genügen. Der Prototyp ist

template<class InputIt, class OutputIt, class Pred> constexpr OutputIt copy_if(InputIt first, InputIt last, OutputIt result, Pred pred)

Zurückgegeben wird (result + N), wobei N die Zahl der kopierten Elemente ist.

template<class R, class OutputIt, class Proj = identity, Pred> constexpr copy_result ranges::copy_if(R&& r, OutputIt result, Pred pred, Proj proj = {});

Zu copy_result siehe Abschnitt 23.3.1. In Listing 23.78 werden alle Elemente größer als 10 von container1 am Ende von container2 eingefügt.

Kopieren einer bestimmten Zahl von Elementen

copy_n() kopiert eine bestimmte Anzahl von Elementen. Der Prototyp ist

template<class InputIt, class Size, class OutputIt> constexpr OutputIt copy_n(InputIt first, Size n, OutputIt result);
23.10.6 Vertauschen von Elementen, Bereichen und Containern

Der Algorithmus swap() vertauscht Elemente von Containern oder Container selbst. Er tritt in Varianten auf:

Image       swap() vertauscht zwei Container oder einzelne Elemente. Die beiden Elemente können in verschiedenen, in demselben oder in keinem Container sein. swap() ist im Header <utility>, iter_swap() und swap_ranges() sind im Header <algorithm>.

template<typename T> constexpr void swap(T& a, T& b);

Image       iter_swap() nimmt zwei Iteratoren und vertauscht die dazugehörenden Elemente. Die beiden Iteratoren können zu verschiedenen oder zu demselben Container gehören.

template<class ForwardIt1, class ForwardIt2> constexpr void iter_swap(ForwardIt1 a, ForwardIt2 b);
// Beispiel: erstes und letztes Element per Iterator vertauschen: auto first = v.begin(); auto last = v.end(); --last; iter_swap(first, last); // Tausch

Image       swap_ranges() vertauscht zwei Bereiche.

template<class ForwardIt1, class ForwardIt2> constexpr ForwardIt2 swap_ranges(ForwardIt1 first1, ForwardIt1 last1, ForwardIt2 first2);

first1 zeigt auf den Anfang des ersten Bereichs, last1 auf die Position nach dem letzten Element des ersten Bereichs. Der Anfang des zweiten Bereichs wird durch first2 gegeben. Die Anzahl der auszutauschenden Elemente wird durch die Größe des ersten Bereichs gegeben. Die Bereiche können in demselben Container sein, dürfen sich jedoch nicht überlappen. std::swap_ranges() gibt einen Iterator auf das Ende des zweiten Bereichs zurück. Die ranges-Variante ist

template<class R1, class R2> constexpr swap_ranges_result<safe_iterator_t<R1>, safe_iterator_t<R2>> ranges::swap_ranges(R1&& r1, R2&& r2);

Es wird eine Struktur mit den Elementen {in1, in2} und dem Wert {r1.begin(), r2.begin() + N} zurückgegeben, wobei N die Größe des kleineren Bereichs ist.

// Vertauschen der Hälften eines Vektors v mit einer geraden Anzahl von Elementen auto mitte = v.begin() + v.size()/2; swap_ranges(v.begin(), mitte, mitte);

Image       swap() ist für diejenigen Container spezialisiert, die eine Methode swap() zum Vertauschen bereitstellen, also array, deque, list, vector, set, map, multiset und multimap. Diese Methoden sind mit Ausnahme von array::swap() sehr schnell (O(1)), weil nur Verwaltungsinformationen ausgetauscht werden. swap() ruft intern die Methoden der Container auf.

exchange()

Der Algorithmus exchange() weist dem Objekt obj den neuen Wert newValue zu und gibt den alten Wert zurück. Die Deklaration ist:

template< class T, class U = T > constexpr T exchange(T& obj, U&& newValue) noexcept( /* (weggelassen) */);

Die Funktion ist im Header <utility>. Die Datei cppbuch/k23/vermischtes/exchange.cpp zeigt ein Beispiel.

23.10.7 Elemente transformieren

Wenn es darum geht, nicht nur etwas zu kopieren, sondern dabei gleich umzuwandeln, ist transform() der richtige Algorithmus. Die Umwandlung kann sich auf nur ein Element oder auf zwei Elemente gleichzeitig beziehen. Dementsprechend gibt es überladene Formen:

template<class InputIt, class OutputIt, class UnaryOperation> constexpr OutputIt transform(InputIt first, InputIt last, OutputIt result, UnaryOperation op);

Hier wird auf jedes Element von first bis ausschließlich last die Operation op angewendet und das Ergebnis in den mit result beginnenden Bereich kopiert. result darf identisch mit first sein, wobei dann die Elemente durch die transformierten ersetzt werden. Der Rückgabewert ist ein Iterator auf die Position nach dem Ende des Zielbereichs. Die ranges-Variante ist

template<class R, class OutputIt, class Function, class Proj = identity> constexpr copy_result ranges::transform(R&& r, OutputIt result, Function op, Proj proj = {});

Die zurückgegebene Struktur copy_result hat den Wert {r1.begin() + N, result + N}. Im Beispiel

string s("ABC123"); ranges::transform(s, s.begin(), tolower);

werden alle Großbuchstaben eines Strings in Kleinbuchstaben umgewandelt. Die Funktion tolower() ist im Header <cctype> deklariert.

template<class InputIt1, class InputIt2, class OutputIt, class BinaryOp> constexpr OutputIt transform(InputIt1 first1, InputIt1 last1, InputIt2 first2, OutputIt result, BinaryOp binop);

In der zweiten Form werden zwei Bereiche betrachtet. Der erste ist das Intervall [first1, last1), der zweite das Intervall [first2, first2 + last1 - first1), das heißt, der zweite Bereich ist genauso groß wie der erste. Die Operation binop nimmt jeweils ein Element aus jedem der zwei Bereiche und legt ihr Ergebnis in result ab. result darf identisch mit first1 oder first2 sein, wobei dann die Elemente durch die transformierten ersetzt werden. Der Rückgabewert ist ein Iterator auf die Position nach dem Ende des Zielbereichs. Die ranges-Variante ist

template<class R1, class R2, class OutputIt, class BinOp, class Proj = identity> constexpr binary_transform_result ranges::transform(R&& r1, R&& r2,OutputIt result, BinOp op, Proj proj = {});

binary_transform_result ist eine Struktur mit den Elementen {in1, in2, out} und dem Wert {r1.begin() + N, r2.begin() + N, result + N}. Das Listing 23.80 zeigt, wie jeweils zwei Strings verkettet werden. Dabei werden sowohl eine unäre Operation als Funktion eingesetzt wie auch eine binäre Operation als Funktor. Erstere wandelt alle Buchstaben eines C++-Strings in Großbuchstaben um und gibt den veränderten String zurück. Der Funktor verkettet zwei String-Objekte miteinander und fügt dabei das Wort »und« ein.

23.10.8 Ersetzen und Varianten

Der Algorithmus replace() ersetzt in einer Sequenz jeden vorkommenden Wert old_value durch new_value. Alternativ ist mit replace_if() eine bedingungsgesteuerte Ersetzung mit einem unären Prädikat möglich:

template<class ForwardIt, class T> constexpr void replace(ForwardIt first, ForwardIt last, const T& old_value, const T& new_value);
template<class R, class T1, class T2, class Proj = identity> constexpr safe_iterator_t<R> ranges::replace(R&& r, const T1& old_value, const T2& new_value, Proj proj = {});
template<class ForwardIt, class Pred, class T> constexpr void replace_if(ForwardIt first, ForwardIt last, Pred pred, const T& new_value);
template<class R, class T, class Proj = identity, class Pred> constexpr safe_iterator_t<R> ranges::replace_if(R&& r, Pred pred, const T& new_value, Proj proj = {});

Die kopierenden Varianten unterscheiden sich im Namen durch ein hinzugefügtes _copy:

template<class InputIt, class OutputIt, class T> constexpr OutputIt replace_copy(InputIt first, InputIt last, OutputIt result, const T& old_value, const T& new_value);
template<class R, class OutputIt, class T1, class T2, class Proj = identity> constexpr copy_result ranges::replace_copy(R&& r, OutputIt result, const T1& old_value, const T2& new_value, Proj proj = {});
template<class InputIt, class OutputIt, class Pred, class T> constexpr OutputIt replace_copy_if(Iterator first, Iterator last, OutputIt result, Pred pred, const T& new_value);
template<class R, class T, class OutputIt, class Proj = identity, class Pred> constexpr copy_result ranges::replace_copy_if(R&& r, OutputIt result, Pred pred, const T& new_value, Proj proj = {});

Das Beispiel zeigt die vier Algorithmen replace(), replace_if(), replace_copy() und replace_copy_if() in der jeweiligen ranges-Variante:

23.10.9 Elemente herausfiltern

Der Algorithmus verschiebt alle Elemente aus einer Sequenz, die gleich einem Wert value sind beziehungsweise einem Prädikat pred genügen, an das Ende. Hier sind die Prototypen einschließlich der kopierenden Varianten aufgeführt:

template<class ForwardIt, class T> constexpr ForwardIt remove(ForwardIt first, ForwardIt last, const T& value);
template<class ForwardIt, class Pred> constexpr ForwardIt remove_if(ForwardIt first, ForwardIt last, Pred pred);
template<class InputIt, class OutputIt, class T> constexpr OutputIt remove_copy(InputIt first, InputIt last, OutputIt result, const T& value);
template<class InputIt, class OutputIt, class Pred> constexpr OutputIt remove_copy_if(InputIt first, InputIt last, OutputIt result, Pred pred);

Das Wort »remove« heißt eigentlich »entfernen«. Es bedeutet hier nur, dass die zu löschenden Elemente an das Ende geschoben werden. remove() gibt einen Iterator auf den Anfang des Teilbereichs mit den gelöschten Elementen zurück. Dabei ist zu beachten, dass die gesamte Länge der Sequenz sich nicht ändert! Falls nicht kopiert wird, enthält der Bereich zwischen dem zurückgegebenen Iterator und last nur noch bedeutungslos gewordene Elemente. Wenn sie tatsächlich entfernt werden sollen, ist für den Container erase() ab diesem Iterator bis zum Ende auszuführen. Das Listing 23.82 zeigt nur die nicht kopierenden Varianten:

Der Vollständigkeit halber sind die ranges-Varianten hier aufgeführt. Die nicht abgedruckte Datei cppbuch/k23/vermischtes/removeranges.cpp zeigt das Programm aus Listing 23.82 mit den ranges-Algorithmen.

template<class R, class T, class Proj = identity> constexpr safe_subrange_t<R> ranges::remove(R&& r, const T& val, Proj prj = {});
template<class R, class Proj = identity, class Pred> constexpr safe_subrange_t<R> ranges::remove_if(R&& r, Pred pred, Proj prj = {});
template<class R, class OutputIt, class T, class Proj = identity> constexpr copy_result ranges::remove_copy(R&& r, OutputIt result, const T& value, Proj proj = {});
template<class R, class OutputIt, class Proj = identity, class Pred> constexpr copy_result ranges::remove_copy_if(R&& r, OutputIt result, Pred pred, Proj proj = {});

Tücken bei modifizierenden Funktionen

Nehmen wir ein Beispiel: In einem Vektor sollen alle Werte gelöscht werden, die gleich dem ersten Element sind, hier also alle Nullen:

vector v{0, 2, 4, 5, 6, 1, 0, 0, 9, 11}; auto last = remove(v.begin(), v.end(), v[0]); // ?

Diese Lösung ist falsch! Tatsächlich wird nur die erste Null gelöscht. Der Grund: Der letzte Parameter wird per Referenz übergeben und der Indexoperator gibt eine Referenz zurück. Nach dem ersten Schritt ist v[0] damit gleich 2. Mit diesem Wert wird weiter gesucht – aber den gibt es nach der Position 0 nicht mehr. Richtig ist daher

auto wert{v[0]}; // Kopie auto last = remove(v.begin(), v.end(), wert); // richtig!

Seit C++23 gibt es die Möglichkeit, die Zwischenkopie mit auto() einzusparen. auto(x) bewirkt die Umwandlung des Arguments x in einen reinen R-Wert.

auto last = remove(v.begin(), v.end(), auto(v[0])); // richtig (C++23)

Die Wirkung entspricht hier remove(v.begin(), v.end(), 0);. Statt auto(x) kann auto{x} geschrieben werden. Die Datei cppbuch/k23/vermischtes/removeProblem.cpp zeigt ein vollständiges Programm.

Image

Merke:

Bei Funktionen, die einen Container modifizieren, können Referenzparameter, die in dem zu modifizierenden Bereich liegen, zu Problemen führen.

Image

23.10.10 Grenzwerte von Zahltypen

Im Header <limits> wird die Template-Klasse numeric_limits definiert. Sie hat Spezialisierungen für die ganzzahligen Grunddatentypen bool, char, signed char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long sowie für die Gleitkommazahltypen float, double und long double. Für diese Grunddatentypen beschreiben die Spezialisierungen verschiedene implementationsabhängige Funktionen und Eigenschaften, die alle public sind und von denen die wichtigsten in der Tabelle 23.2 aufgelistet sind. Eine Anwendung wird in Listing 1.6 (Seite 48) gezeigt.

Tabelle 23.2: <limits>: Attribute und Funktionen (Auszug). T = Typ der Zahl

Schnittstelle

Bedeutung

bool is_specialized

true nur für Grunddatentypen, für die eine Spezialisierung vorliegt, false für alle anderen

T min()

minimal möglicher Wert

T max()

maximal möglicher Wert

int radix

Zahlenbasis, normal 2 (Ausnahme z.B. BCD-Zahlen)

int digits

Ganzzahlen: Anzahl der Bits (ohne Vorzeichen-Bit) Gleitkommazahlen: Anzahl der Mantissenbits

int digits10

Anzahl signifikanter Dezimalziffern bei Gleitkommazahlen, zum Beispiel 6 bei float, 15 bei double

bool is_signed

true bei vorzeichenbehafteten Zahlen

bool is_integer

true bei Ganzzahltypen

bool is_exact

true bei exakten Zahlen, z.B. ganze oder rationale Zahlen – nicht aber Gleitkommazahlen

T epsilon()

kleinster positiver Wert x, für den die Maschine die Differenz zwischen 1.0 und (1.0 + x) noch unterscheidet

T round_error()

maximaler Rundungsfehler

int min_exponent

kleinster negativer Exponent für Gleitkommazahlen

int min_exponent10

kleinster negativer 10er-Exponent für Gleitkommazahlen ( –37)

int max_exponent

größtmöglicher Exponent für Gleitkommazahlen

int max_exponent10

größtmöglicher 10er-Exponent für Gleitkommazahlen ( +37)

T infinity()

Repräsentation von +, falls vorhanden

bool has_infinity

true, falls der Zahltyp eine Repräsentation für + hat

bool is_iec559

true, falls der Zahltyp dem IEC 559(= IEEE 754)-Standard genügt

bool is_bounded

true für alle Grunddatentypen, false wenn die Menge der darstellbaren Werte unbegrenzt ist, z.B. bei Typen mit beliebiger Genauigkeit

bool is_modulo

true, falls bereichsüberschreitende Operationen wieder eine gültige Zahl ergeben. Z.B. ergibt die Addition einer Zahl auf die größtmögliche Integerzahl bei den meisten Maschinen wieder eine Integerzahl, die kleiner als die größtmögliche ist. Dies gilt im Allgemeinen nicht für Gleitkommazahlen.

round_style

Art der Rundung. Ganzzahlen: round_toward_zero (= 0) Gleitkommazahlen: round_to_nearest (= 1)

23.10.11 Minimum und Maximum

Die Templates min() und max() geben jeweils das kleinste (bzw. das größte) von zwei oder mehr Elementen zurück. Bei Gleichheit wird das erste Element zurückgegeben. Die Prototypen für nur zwei zu vergleichende Elemente sind:

template<typename T> constexpr const T& min(const T& a, const T& b); template<typename T, class Compare> constexpr const T& min(const T& a, const T& b, Compare comp); template<typename T> constexpr const T& max(const T& a, const T& b); template<typename T, class Compare> constexpr const T& max(const T& a, const T& b, Compare comp);

minmax() gibt ein pair-Objekt zurück, das den kleineren Wert an der Stelle first und den größeren an der Stelle second enthält:

template<typename T> constexpr pair<const T&, const T&> minmax(const T& a, const T& b); template<typename T, class Compare> constexpr pair<const T&, const T&> minmax(const T& a, const T& b, Compare comp);

Die genannten Algorithmen gibt es auch mit einer Initialisierungsliste oder einem Bereich als Parameter. Damit können mehr als zwei Elemente verglichen werden:

template<typename T> constexpr T min(initializer_list<T> t);
template<typename T, class Compare> constexpr T min(initializer_list<T> t, Compare comp);
template<class R, class Proj = identity, class Comp = ranges::less> constexpr range_value_t<R> ranges::min(R&& r, Comp comp = {}, Proj proj = {});

range_value_t<R> ist der Datentyp der Elemente des Bereichstyps R.

template<typename T> constexpr T max(initializer_list<T> t);
template<typename T, class Compare> constexpr T max(initializer_list<T> t, Compare comp);
template<class R, class Proj = identity, class Comp = ranges::less> constexpr range_value_t<R> ranges::max(R&& r, Comp comp = {}, Proj proj = {});
template<typename T> constexpr pair<const T, const T> minmax(initializer_list<T> t);
template<typename T, class Compare> constexpr pair<const T, const T> minmax(initializer_list<T> t, Compare comp);
template<class R, class Proj = identity, class Comp = ranges::less> constexpr minmax_result ranges::minmax(R&& r, Comp comp = {}, Proj proj = {});

minmax_result ist eine Struktur mit den Elementen {min, max}. Das folgende kleine Programm zeigt einige Möglichkeiten.

23.10.12 Wert begrenzen

Der Algorithmus clamp() begrenzt einen Wert. Die Schnittstelle ist:

template<class T> constexpr const T& clamp(const T& val, const T& lo, const T& hi );
template<class T> constexpr const T& clamp(const T& val, const T& lo, const T& hi, Compare comp );

Dabei ist val der zu begrenzende Wert, lo die Untergrenze und hi die Obergrenze. Optional kann ein Vergleichsobjekt comp übergeben werden, wenn nicht mit dem <-Operator verglichen werden soll. Zurückgegeben wird lo, wenn val < lo ist, und hi, wenn val >= hi ist. Ist keine der Bedingungen erfüllt, wird val zurückgegeben. Im Beispiel werden verschiedene Werte auf die Bereiche -128..127 und 0..255 begrenzt.

Das Programm gibt aus:

Wert -128 ..127 0..255 -300 -128 0 200 127 200 -50 -50 0 -1 -1 0 0 0 0 1 1 1 50 50 50 200 127 200 300 127 255
23.10.13 ggT, kgV und Mitte

ggT und kgV sind die gängigen Abkürzungen für »größter gemeinsamer Teiler« (englisch greatest common divisor, abgekürzt gcd) und »kleinstes gemeinsames Vielfaches« (englisch least common multiple, abgekürzt lcm). midpoint(T a, T b) ergibt die Hälfte der Summe, midpoint(T* p, T* q) zeigt auf das mittlere Element in einem Array, wenn p und q Zeiger auf Elemente dieses Arrays sind. Die Schnittstellen sind:

template <class M, class N> constexpr common_type_t<M,N> gcd(M m, N n);
template <class M, class N> constexpr common_type_t<M,N> lcm(M m, N n);

Die Typen M und N müssen ganzzahlig sein. common_type_t<M,N> ist ein Typ, der Werte beider Typen M und N abbilden kann. Wenn zum Beispiel ein Typ short und der andere int ist, ist das Ergebnis vom Typ int. gcd(0, 0) ergibt 0. lcm(a, b) ergibt 0, wenn wenigstens eines der beiden Argumente 0 ist.

template<class T> constexpr T midpoint(T a, T b) noexcept; template<class T> constexpr T* midpoint(T* a, T* b);

Die Datei cppbuch/k23/vermischtes/gcdlcmmidpoint.cpp (hier nicht abgedruckt) zeigt die Anwendung aller vier Algorithmen.

23.11 Parallelisierbare Algorithmen

Zu den meisten Algorithmen der C++-Standardbibliothek gibt es überladene parallelisierbare Varianten. So eine Variante kann schematisch aus der jeweiligen Schnittstelle abgeleitet werden. In den tabellarischen Übersichten in Abschnitt 29.2 sind die nicht-parallelisierbaren Algorithmen mit einem * markiert. Die parallelisierbaren Formen sind nur bei wirklich sehr großen Datenmengen effizienter, sollten also auf Basis einer Laufzeitmessung eingesetzt werden.

Jeder der überladenen parallelisierbaren Algorithmen hat einen zusätzlichen Parameter des Typs ExecutionPolicy an erster Stelle. Ein möglicherweise vorhandenes constexpr entfällt. Das sei am Beispiel von min_element() gezeigt:

// Sequenzieller Algorithmus template<class ForwardIt> constexpr ForwardIt min_element(ForwardIt first, ForwardIt last);
// Parallelisierbarer Algorithmus template<class ExecutionPolicy, class ForwardIt> ForwardIt min_element(ExecutionPolicy&& exec, ForwardIt first, ForwardIt last);

Beim Aufruf muss also ggf. ein ExecutionPolicy-Objekt übergeben werden. Dieses Schema gilt für alle parallelisierbaren Algorithmen. Es gibt drei globale ExecutionPolicy-Objekte:

Image       std::execution::seq besagt, dass der Algorithmus nicht parallelisiert werden soll.

Image       std::execution::par sagt dem Compiler, dass der Algorithmus parallelisiert werden kann. Das Wort »kann« drückt aus, dass eine Parallelisierung nicht zwingend vorgenommen wird, etwa weil die Anzahl der zu bearbeitenden Elemente zu klein ist.

Image       std::execution::par_unseq bedeutet, dass der Algorithmus parallelisiert und vektorisiert3 werden kann.

Der Namespace ist std::execution, der Header ist <execution>. Die Objekte sind von jeweils verschiedenen Typen und beim Überladen für einen Compiler eindeutig zuzuordnen. Beispiel für einen Aufruf:

#include <algorithm> #include <execution> // ... std::vector<int> v = initialisiereVektorMitVielenDaten(); // Iterator auf kleinstes Element ermitteln: auto iter = std::min_element(std::execution::par, v.begin(), v.end());

Sie müssen selbst darauf achten, dass eine Parallelisierung kein data race verursacht, etwa durch nichtsynchronisiertes Schreiben von Daten.


1 Siehe Glossar Seite 981

2 Siehe Eintrag »Intervall« im Glossar Seite 980.

3 Ein SIMD-Prozessor kann eine Operation parallel auf vielen Daten ausführen. SIMD = single instruction, multiple data.