web-dev-qa-db-ger.com

Wie implementiere ich klassische Sortieralgorithmen in modernem C ++?

Der Algorithmus std::sort (Und seine Cousins ​​std::partial_sort Und std::nth_element) Aus der C++ - Standardbibliothek ist in den meisten Implementierungen eine komplizierte und hybride Verschmelzung von elementareren Elementen Sortieralgorithmen , z. B. Auswahlsortierung, Einfügesortierung, Schnellsortierung, Zusammenführungssortierung oder Heapsortierung.

Hier und auf Schwesterseiten wie https://codereview.stackexchange.com/ gibt es viele Fragen zu Fehlern, Komplexität und anderen Aspekten der Implementierung dieser klassischen Sortieralgorithmen. Die meisten der angebotenen Implementierungen bestehen aus unformatierten Schleifen, verwenden Indexmanipulation und konkrete Typen und sind im Allgemeinen nicht trivial, um sie hinsichtlich Korrektheit und Effizienz zu analysieren.

Frage : Wie können die oben genannten klassischen Sortieralgorithmen mit modernem C++ implementiert werden?

  • keine rohen Schleifen , sondern Kombination der algorithmischen Bausteine ​​der Standardbibliothek aus <algorithm>
  • Iteratorschnittstelle und Verwendung von Vorlagen anstelle von Indexmanipulation und konkreten Typen
  • C++ 14-Stil , einschließlich der vollständigen Standardbibliothek sowie syntaktischer Rauschunterdrücker wie auto, Vorlagen-Aliase und transparente Komparatoren und polymorphe Lambdas.

Anmerkungen :

  • weitere Hinweise zur Implementierung von Sortieralgorithmen finden Sie in Wikipedia , Rosetta Code oder http: // www .sorting-algorithms.com/
  • nach Sean Parents Konventionen (Folie 39) ist eine rohe Schleife ein for-Schleife länger als die Komposition zweier Funktionen mit einem Operator. Also sind f(g(x)); oder f(x); g(x); oder f(x) + g(x); keine unformatierten Schleifen und auch die Schleifen in selection_sort Und insertion_sort Unten nicht.
  • Ich folge der Terminologie von Scott Meyers, um das aktuelle C++ 1y bereits als C++ 14 zu bezeichnen, und um C++ 98 und C++ 03 beide als C++ 98 zu bezeichnen.
  • Wie in den Kommentaren von @Mehrdad vorgeschlagen, stelle ich am Ende der Antwort vier Implementierungen als Live-Beispiel zur Verfügung: C++ 14, C++ 11, C++ 98 und Boost und C++ 98.
  • Die Antwort selbst wird nur in C++ 14 dargestellt. Wo relevant, bezeichne ich die syntaktischen und bibliothekarischen Unterschiede, bei denen sich die verschiedenen Sprachversionen unterscheiden.
320
TemplateRex

Algorithmische Bausteine

Wir beginnen mit der Zusammenstellung der algorithmischen Bausteine ​​aus der Standardbibliothek:

#include <algorithm>    // min_element, iter_swap, 
                        // upper_bound, rotate, 
                        // partition, 
                        // inplace_merge,
                        // make_heap, sort_heap, Push_heap, pop_heap,
                        // is_heap, is_sorted
#include <cassert>      // assert 
#include <functional>   // less
#include <iterator>     // distance, begin, end, next
  • die Iterator-Tools wie Nicht-Member std::begin()/std::end() sowie mit std::next() sind nur ab C++ 11 verfügbar. Für C++ 98 muss man diese selbst schreiben. Es gibt Substitute von Boost.Range in boost::begin()/boost::end() und von Boost.Utility in boost::next().
  • der Algorithmus std::is_sorted ist nur für C++ 11 und höher verfügbar. Für C++ 98 kann dies in Form von std::adjacent_find und einem handgeschriebenen Funktionsobjekt implementiert werden. Boost.Algorithm bietet auch einen boost::algorithm::is_sorted als Ersatz.
  • der Algorithmus std::is_heap ist nur für C++ 11 und höher verfügbar.

Syntaktische Leckereien

C++ 14 bietet transparente Komparatoren der Form std::less<>, die polymorph auf ihre Argumente einwirken. Dies vermeidet die Angabe eines Iteratortyps. Dies kann in Kombination mit den Standardfunktionsvorlagenargumenten von C++ 11 verwendet werden, umzu erstellen/eine einzelne Überladungfür Sortieralgorithmen, die < als Vergleich verwenden, und solche, die ein benutzerdefiniertes Vergleichsfunktionsobjekt haben.

template<class It, class Compare = std::less<>>
void xxx_sort(It first, It last, Compare cmp = Compare{});

In C++ 11 kann man einen wiederverwendbaren Template-Alias ​​ definieren, um den Werttyp eines Iterators zu extrahieren, der das Ganze etwas übersichtlicher macht Signaturen der Algorithmen sortieren:

template<class It>
using value_type_t = typename std::iterator_traits<It>::value_type;

template<class It, class Compare = std::less<value_type_t<It>>>
void xxx_sort(It first, It last, Compare cmp = Compare{});

In C++ 98 muss man zwei Überladungen schreiben und die ausführliche typename xxx<yyy>::type -Syntax verwenden

template<class It, class Compare>
void xxx_sort(It first, It last, Compare cmp); // general implementation

template<class It>
void xxx_sort(It first, It last)
{
    xxx_sort(first, last, std::less<typename std::iterator_traits<It>::value_type>());
}
  • Eine weitere syntaktische Besonderheit ist, dass C++ 14 das Umbrechen von benutzerdefinierten Komparatoren durchpolymorphe Lambda(mit auto -Parametern, die wie Argumente von Funktionsschablonen abgeleitet werden) erleichtert.
  • C++ 11 enthält nur monomorphe Lambdas, für die der obige Template-Alias ​​value_type_t verwendet werden muss.
  • In C++ 98 muss entweder ein eigenständiges Funktionsobjekt geschrieben oder auf den ausführlichen Syntaxtyp std::bind1st/std::bind2nd/std::not1 zurückgegriffen werden.
  • Boost.Bind verbessert dies mit den Platzhaltersyntaxen boost::bind und _1/_2.
  • C++ 11 und höher haben auch std::find_if_not, wohingegen C++ 98 std::find_if mit einem std::not1 um ein Funktionsobjekt benötigt.

C++ Style

Es gibt noch keinen allgemein akzeptablen C++ 14-Stil. Zum Guten oder zum Schlechten folge ich Scott Meyers draft Effective Modern C++ und Herb Sutters GotW überarbeitet . Ich verwende die folgenden Stilempfehlungen:

  • Herb Sutters "Fast immer automatisch" und Scott Meyers "Auto bestimmten Typdeklarationen vorziehen" Empfehlung, für die die Kürze unübertroffen ist, obwohl ihre Klarheit manchmal umstritten .
  • Scott Meyers "Unterscheide () und {} beim Erstellen von Objekten" und wähle konsequent die geschweifte Initialisierung {} anstelle der guten alten Initialisierung in Klammern () (um alle lästigen Analyseprobleme im generischen Code zu umgehen).
  • Scott Meyers "Bevorzugen Sie Alias-Deklarationen gegenüber typedefs" . Für Vorlagen ist dies ohnehin ein Muss und die Verwendung überall anstelle von typedef spart Zeit und erhöht die Konsistenz.
  • An einigen Stellen verwende ich ein for (auto it = first; it != last; ++it) - Muster, um eine schleifeninvariante Überprüfung bereits sortierter Unterbereiche zu ermöglichen. Im Produktionscode ist die Verwendung von while (first != last) und einem ++first irgendwo innerhalb der Schleife möglicherweise etwas besser.

Auswahl sortieren

Selection sort passt sich in keiner Weise an die Daten an, daher ist seine Laufzeit immer O(N²). Die Auswahlsortierung hat jedoch die Eigenschaft, um die Anzahl der Auslagerungenzu minimieren. In Anwendungen, in denen die Kosten für das Austauschen von Elementen hoch sind, kann die Auswahlsortierung der Algorithmus der Wahl sein.

Um es mithilfe der Standardbibliothek zu implementieren, verwenden Sie wiederholt std::min_element, um das verbleibende minimale Element zu finden, und iter_swap, um es auszutauschen:

template<class FwdIt, class Compare = std::less<>>
void selection_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last; ++it) {
        auto const selection = std::min_element(it, last, cmp);
        std::iter_swap(selection, it); 
        assert(std::is_sorted(first, std::next(it), cmp));
    }
}

Beachten Sie, dass selection_sort den bereits verarbeiteten Bereich [first, it) als seine Schleifeninvariante sortiert hat. Die Mindestanforderungen sindforward iteratorsim Vergleich zu den Direktzugriffsiteratoren von std::sort.

Details weggelassen:

  • die Auswahlsortierung kann mit einem frühen Test if (std::distance(first, last) <= 1) return; optimiert werden (oder für vorwärts-/bidirektionale Iteratoren: if (first == last || std::next(first) == last) return;).
  • fürbidirektionale Iteratorenkann der obige Test mit einer Schleife über das Intervall [first, std::prev(last)) kombiniert werden, da das letzte Element garantiert das minimale verbleibende Element und ist benötigt keinen Swap.

Sortieren durch Einfügen

Obwohl dies einer der elementaren Sortieralgorithmen mit O(N²) Worst-Case-Zeit ist, ist Einfügesort die Algorithmus der Wahl, entweder wenn die Daten nahezu sortiert sind (weil es sich umadaptivehandelt) oder wenn die Problemgröße gering ist (weil der Overhead gering ist). Aus diesen Gründen und weil es auchstableist, wird die Einfügesortierung häufig als rekursiver Basisfall (wenn die Problemgröße klein ist) für eine höhere Aufteilung zwischen Aufwand und Aufwand verwendet. Erobern Sie Sortieralgorithmen wie Merge Sort oder Quick Sort.

Um insertion_sort mit der Standardbibliothek zu implementieren, verwenden Sie wiederholt std::upper_bound, um die Position des aktuellen Elements zu ermitteln, und std::rotate, um die verbleibenden Elemente im Eingabebereich nach oben zu verschieben:

template<class FwdIt, class Compare = std::less<>>
void insertion_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last; ++it) {
        auto const insertion = std::upper_bound(first, it, *it, cmp);
        std::rotate(insertion, it, std::next(it)); 
        assert(std::is_sorted(first, std::next(it), cmp));
    }
}

Beachten Sie, dass insertion_sort den bereits verarbeiteten Bereich [first, it) als seine Schleifeninvariante sortiert hat. Einfügesortierung funktioniert auch mit Forward-Iteratoren.

Details weggelassen:

  • die Einfügesortierung kann mit einem frühen Test if (std::distance(first, last) <= 1) return; (oder für vorwärts-/bidirektionale Iteratoren: if (first == last || std::next(first) == last) return;) und einer Schleife über das Intervall [std::next(first), last) optimiert werden, da das erste Element garantiert wird an Ort und Stelle sein und erfordert keine Drehung.
  • fürbidirektionale Iteratorenkann die binäre Suche zum Finden der Einfügemarke durch eineumgekehrte lineare Sucheersetzt werden std::find_if_not -Algorithmus der Standard Library.

VierLive-Beispiele( C++ 14 , C++ 11 , C++ 98 und Boost , C++ 98 ) für das folgende Fragment:

using RevIt = std::reverse_iterator<BiDirIt>;
auto const insertion = std::find_if_not(RevIt(it), RevIt(first), 
    [=](auto const& elem){ return cmp(*it, elem); }
).base();
  • Bei zufälligen Eingaben werden Vergleiche mit O(N²) erstellt, bei fast sortierten Eingaben jedoch mit O(N). Die binäre Suche verwendet immer O(N log N) Vergleiche.
  • Bei kleinen Eingabebereichen dominiert möglicherweise auch die bessere Speicherlokalität (Cache, Prefetching) einer linearen Suche eine binäre Suche (dies sollte man natürlich testen).

Schnelle Sorte

Bei sorgfältiger Implementierung ist quick sort robust und hat O(N log N) erwartete Komplexität, jedoch mit O(N²) Worst-Case-Komplexität, die mit entgegengesetzt gewählten Eingabedaten ausgelöst werden kann. Wenn eine stabile Sortierung nicht benötigt wird, ist die schnelle Sortierung eine hervorragende Allzweck-Sortierung.

Selbst in den einfachsten Versionen ist das schnelle Sortieren mit der Standardbibliothek viel komplizierter als mit den anderen klassischen Sortieralgorithmen. Der folgende Ansatz verwendet einige Iterator-Dienstprogramme, um dasmittlere Elementdes Eingabebereichs [first, last) als Pivot zu lokalisieren. Verwenden Sie dann zwei Aufrufe von std::partition. (Dies sind O(N)), um den Eingabebereich in Segmente von Elementen zu unterteilen, die kleiner, gleich oder größer als der ausgewählte Drehpunkt sind. Schließlich werden die beiden äußeren Segmente mit Elementen, die kleiner als und größer als der Drehpunkt sind, rekursiv sortiert:

template<class FwdIt, class Compare = std::less<>>
void quick_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    auto const N = std::distance(first, last);
    if (N <= 1) return;
    auto const pivot = *std::next(first, N / 2);
    auto const middle1 = std::partition(first, last, [=](auto const& elem){ 
        return cmp(elem, pivot); 
    });
    auto const middle2 = std::partition(middle1, last, [=](auto const& elem){ 
        return !cmp(pivot, elem);
    });
    quick_sort(first, middle1, cmp); // assert(std::is_sorted(first, middle1, cmp));
    quick_sort(middle2, last, cmp);  // assert(std::is_sorted(middle2, last, cmp));
}

Eine schnelle Sortierung ist jedoch recht schwierig, um korrekt und effizient zu sein, da jeder der oben genannten Schritte sorgfältig überprüft und für den Code auf Produktionsebene optimiert werden muss. Insbesondere für die Komplexität von O(N log N) muss der Pivot zu einer ausgeglichenen Aufteilung der Eingabedaten führen, die für einen O(1) - Pivot im Allgemeinen nicht garantiert werden kann, die aber garantiert werden kann, wenn ein Pivot gesetzt wird der Drehpunkt als O(N) Median des Eingabebereichs.

Details weggelassen:

  • die obige Implementierung ist besonders anfällig für spezielle Eingaben, z. Es hat die Komplexität von O(N^2) für die Eingabe "organ pipe" 1, 2, 3, ..., N/2, ... 3, 2, 1 (da die Mitte immer größer als alle anderen Elemente ist).
  • Median-von-3 Pivot-Auswahl aus zufällig ausgewählt Die Elemente aus dem Eingabebereich schützen vor fast sortierten Eingaben, deren Komplexität sich ansonsten auf O(N^2) verschlechtern würde.
  • 3-Wege-Partitionierung (Trennelemente kleiner, gleich und größer als der Drehpunkt), wie durch die beiden Aufrufe von gezeigt std::partition ist nicht der effizienteste Algorithmus O(N), um dieses Ergebnis zu erzielen.
  • fürIteratoren mit wahlfreiem Zugriffkann eine garantierte O(N log N) -Komplexität durchAuswahl des mittleren Pivotsusing erreicht werden std::nth_element(first, middle, last), gefolgt von rekursiven Aufrufen von quick_sort(first, middle, cmp) und quick_sort(middle, last, cmp).
  • diese Garantie ist jedoch mit Kosten verbunden, da der konstante Faktor der O(N) - Komplexität von std::nth_element teurer sein kann als der der O(1) - Komplexität eines Medians von 3 Pivot, gefolgt von einem O(N) - Aufruf von std::partition (dies ist ein Cache-freundlicher einfacher Weiterleitungsdurchlauf über die Daten).

Zusammenführen, sortieren

Wenn die Verwendung von O(N) zusätzlichem Leerzeichen keine Rolle spielt, ist sortieren eine ausgezeichnete Wahl: es ist der einzigestableO(N log N) Sortieralgorithmus.

Die Implementierung mit Standardalgorithmen ist einfach: Verwenden Sie einige Iterator-Dienstprogramme, um die Mitte des Eingabebereichs [first, last) zu lokalisieren und zwei rekursiv sortierte Segmente mit einem std::inplace_merge zu kombinieren:

template<class BiDirIt, class Compare = std::less<>>
void merge_sort(BiDirIt first, BiDirIt last, Compare cmp = Compare{})
{
    auto const N = std::distance(first, last);
    if (N <= 1) return;                   
    auto const middle = std::next(first, N / 2);
    merge_sort(first, middle, cmp); // assert(std::is_sorted(first, middle, cmp));
    merge_sort(middle, last, cmp);  // assert(std::is_sorted(middle, last, cmp));
    std::inplace_merge(first, middle, last, cmp); // assert(std::is_sorted(first, last, cmp));
}

Für die Zusammenführung sind bidirektionale Iteratoren erforderlich, wobei der Engpass der std::inplace_merge ist. Beachten Sie, dass beim Sortieren von verknüpften Listen für das Sortieren von Zusammenführungen nur O(log N) zusätzlicher Speicherplatz benötigt wird (für die Rekursion). Letzterer Algorithmus wird von std::list<T>::sort in der Standard Library implementiert.

Haufen sortieren

Heap sort ist einfach zu implementieren, führt eine O(N log N) -Inplace-Sortierung durch, ist jedoch nicht stabil.

Die erste Schleife, O(N) "Heapify" -Phase, versetzt das Array in die Heap-Reihenfolge. Die zweite Schleife, die O(N log N) "Sortdown" -Phase, extrahiert wiederholt das Maximum und stellt die Heap-Reihenfolge wieder her. Die Standardbibliothek macht dies extrem einfach:

template<class RandomIt, class Compare = std::less<>>
void heap_sort(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    lib::make_heap(first, last, cmp); // assert(std::is_heap(first, last, cmp));
    lib::sort_heap(first, last, cmp); // assert(std::is_sorted(first, last, cmp));
}

Wenn Sie die Verwendung von std::make_heap und std::sort_heap für "betrügerisch" halten, können Sie eine Ebene tiefer gehen und diese Funktionen selbst in Form von std::Push_heap bzw. std::pop_heap schreiben:

namespace lib {

// NOTE: is O(N log N), not O(N) as std::make_heap
template<class RandomIt, class Compare = std::less<>>
void make_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last;) {
        std::Push_heap(first, ++it, cmp); 
        assert(std::is_heap(first, it, cmp));           
    }
}

template<class RandomIt, class Compare = std::less<>>
void sort_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    for (auto it = last; it != first;) {
        std::pop_heap(first, it--, cmp);
        assert(std::is_heap(first, it, cmp));           
    } 
}

}   // namespace lib

Die Standardbibliothek gibt sowohl Push_heap als auch pop_heap als Komplexität O(log N) an. Beachten Sie jedoch, dass die äußere Schleife über den Bereich [first, last) zu einer Komplexität von O(N log N) für make_heap führt, während std::make_heap nur eine Komplexität von O(N) aufweist. Für die Gesamtkomplexität von O(N log N) von heap_sort spielt dies keine Rolle.

Details weggelassen: O(N) Implementierung von make_heap

Testen

Hier sind vierLive-Beispiele( C++ 14 , C++ 11 , C++ 98 und Boost , C++ 98 ) Testen aller fünf Algorithmen auf einer Vielzahl von Eingänge (nicht erschöpfend oder streng gemeint). Man beachte nur die großen Unterschiede im LOC: C++ 11/C++ 14 benötigen rund 130 LOC, C++ 98 und Boost 190 (+ 50%) und C++ 98 mehr als 270 (+ 100%).

377
TemplateRex

Ein anderes kleines und ziemlich elegantes ursprünglich gefunden bei Code Review . Ich dachte, es lohnt sich zu teilen.

Zählen sortieren

counting sort ist ein einfacher ganzzahliger Sortieralgorithmus und kann oft sehr schnell ausgeführt werden, vorausgesetzt, die Werte der zu sortierenden ganzen Zahlen liegen nicht zu weit auseinander. Es ist wahrscheinlich ideal, wenn man jemals eine Sammlung von einer Million Ganzzahlen sortieren muss, von denen bekannt ist, dass sie beispielsweise zwischen 0 und 100 liegen.

Um eine sehr einfache Zählsortierung zu implementieren, die sowohl mit vorzeichenbehafteten als auch mit vorzeichenlosen Ganzzahlen funktioniert, müssen die kleinsten und größten zu sortierenden Elemente in der Sammlung gefunden werden. Ihre Differenz gibt Auskunft über die Größe des zuzuordnenden Zählwerks. Dann wird ein zweiter Durchlauf durch die Sammlung durchgeführt, um die Anzahl der Vorkommen jedes Elements zu zählen. Schließlich schreiben wir die erforderliche Anzahl jeder Ganzzahl in die ursprüngliche Sammlung zurück.

template<typename ForwardIterator>
void counting_sort(ForwardIterator first, ForwardIterator last)
{
    if (first == last || std::next(first) == last) return;

    auto minmax = std::minmax_element(first, last);  // avoid if possible.
    auto min = *minmax.first;
    auto max = *minmax.second;
    if (min == max) return;

    using difference_type = typename std::iterator_traits<ForwardIterator>::difference_type;
    std::vector<difference_type> counts(max - min + 1, 0);

    for (auto it = first ; it != last ; ++it) {
        ++counts[*it - min];
    }

    for (auto count: counts) {
        first = std::fill_n(first, count, min++);
    }
}

Dies ist nur dann sinnvoll, wenn der Bereich der zu sortierenden Ganzzahlen bekanntermaßen klein ist (im Allgemeinen nicht größer als die Größe der zu sortierenden Auflistung). Wenn Sie jedoch die Sortierung generischer gestalten, wird die Sortierung für die besten Fälle langsamer. Wenn nicht bekannt ist, dass der Bereich klein ist, kann ein anderer Algorithmus wie radix sort , ska_sort oder spreadsort) verwendet werden kann stattdessen verwendet werden.

Details weggelassen :

  • Wir hätten die Grenzen des vom Algorithmus als Parameter akzeptierten Wertebereichs überschreiten können, um das erste std::minmax_element durch die Sammlung gehen. Dadurch wird der Algorithmus noch schneller, wenn auf andere Weise eine sinnvoll kleine Bereichsgrenze bekannt ist. (Es muss nicht genau sein; eine Konstante von 0 bis 100 zu übergeben ist immer noch viel besser als ein zusätzlicher Durchlauf über eine Million Elemente, um herauszufinden, dass die wahren Grenzen 1 bis 95 sind 0 bis 1000 wäre es wert; Die zusätzlichen Elemente werden einmal mit Null geschrieben und einmal gelesen.

  • Das schnelle Wachsen von counts ist ein weiterer Weg, um einen separaten ersten Durchgang zu vermeiden. Das Verdoppeln der counts Größe bei jedem Wachstum ergibt eine amortisierte O(1) Zeit pro sortiertem Element (siehe Analyse der Kosten für das Einfügen von Hash-Tabellen für den Beweis, dass exponentielles Wachstum der Schlüssel ist) ). Am Ende für ein neues max wachsen ist einfach mit std::vector::resize, um neue Elemente mit Nullen hinzuzufügen. Das Ändern von min im laufenden Betrieb und das Einfügen neuer nullter Elemente an der Vorderseite kann mit std::copy_backward nach dem Wachsen des Vektors. Dann std::fill, um die neuen Elemente auf Null zu setzen.

  • Die Inkrementschleife counts ist ein Histogramm. Wenn sich die Daten wahrscheinlich stark wiederholen und die Anzahl der Fächer gering ist, kann es sich lohnen, über mehrere Arrays zu entfernen , um den Engpass bei der Serialisierung der Datenabhängigkeit beim Speichern/Neuladen auf zu verringern die gleiche Tonne. Dies bedeutet, dass am Anfang mehr Zählungen auf Null gesetzt werden und am Ende mehr durchlaufen werden müssen. Dies sollte sich jedoch für die meisten CPUs für unser Beispiel mit Millionen von Zahlen zwischen 0 und 100 lohnen, insbesondere, wenn die Eingabe bereits (teilweise) sortiert und sortiert ist habe lange Läufe mit der gleichen Nummer.

  • Im obigen Algorithmus verwenden wir ein min == max Aktivieren Sie diese Option, um frühzeitig zurückzukehren, wenn jedes Element denselben Wert hat (in diesem Fall wird die Sammlung sortiert). Stattdessen kann vollständig überprüft werden, ob die Auflistung bereits sortiert ist, während die Extremwerte einer Auflistung ohne zusätzliche Zeitverschwendung ermittelt werden (wenn der erste Durchgang immer noch durch die zusätzliche Arbeit des Aktualisierens von min und max in einem Speicherengpass ist). Ein solcher Algorithmus existiert jedoch nicht in der Standardbibliothek, und das Schreiben eines Algorithmus wäre langwieriger als das Schreiben des Restes der Zählsortierung selbst. Es ist eine Übung für den Leser.

  • Da der Algorithmus nur mit ganzzahligen Werten arbeitet, können statische Zusicherungen verwendet werden, um zu verhindern, dass Benutzer offensichtliche Tippfehler machen. In einigen Kontexten ist ein Substitutionsfehler mit std::enable_if_t könnte bevorzugt werden.

  • Während modernes C++ cool ist, könnte zukünftiges C++ noch cooler sein: strukturierte Bindungen und einige Teile der Ranges TS würden den Algorithmus erstellen noch sauberer.

14
Morwenn