Wie implementiert man klassische Sortieralgorithmen in modernem C ++?
Den std::sort
Algorithmus (und seiner Vettern std::partial_sort
und std::nth_element
) aus der C++ Standard-Bibliothek ist in den meisten Implementierungen eine komplizierte und hybride Verschmelzung von mehr elementaren Sortier-algorithmenwie selection sort, insertion-sort, quick-sort, merge-sort oder heap-sort.
Gibt es viele Fragen, die hier und auf den Schwester-Websites, wie https://codereview.stackexchange.com/ Bezug auf bugs, die Komplexität und andere Aspekte der Implementierungen dieser klassischen Sortier-algorithmen. Die meisten der angebotenen Implementierungen bestehen aus roh Schleifen, verwenden Sie index-manipulation und Beton-Arten, und sind im Allgemeinen nicht-trivial zu analysieren, in Bezug auf Richtigkeit und Effizienz.
Frage: wie können die oben genannten klassischen Sortier-algorithmen implementiert werden unter Verwendung moderner C++?
- keine rohen loopsaber die Kombination der Standard-Bibliothek Algorithmische Bausteine aus
<algorithm>
- iterator-interface und Verwendung von Vorlagen anstelle von index-manipulation und konkrete Typen
- C++14 styledarunter die gesamte Standard-Bibliothek als auch als syntaktische Geräusch-Reduzier-wie
auto
Vorlage Aliase, transparente Komparatoren und der polymorphe Lambda-Ausdrücke.
Hinweise:
- für weitere Verweise auf Implementierungen von algorithmen zur Sortierung siehe WikipediaRosetta Code oder http://www.sorting-algorithms.com/
- nach Sean Eltern Konventionen (Folie 39), ein raw-Schleife ist eine
for
-Schleife länger als Komposition von zwei Funktionen mit einem operator. Sof(g(x));
oderf(x); g(x);
oderf(x) + g(x);
sind nicht roh Schleifen, und weder sind die loops inselection_sort
undinsertion_sort
unten. - Folge ich Scott Meyers Terminologie für die Bezeichnung der aktuellen C++1y bereits als C++14, und bezeichnen C++98 und C++03, sowohl als C++98, so don ' T flame me.
- Wie vorgeschlagen, in den Kommentaren von @Mehrdad, ich biete vier Implementierungen, die als Live-Beispiel am Ende die Antwort: C++14, C++11, C++98 und Boost und C++98.
- Die Antwort ist in Bezug auf die C++14 nur. Wo dies relevant ist, ich bezeichne die syntaktische und Bibliothek unterschieden, wobei die verschiedenen Sprachfassungen voneinander abweichen.
InformationsquelleAutor der Frage TemplateRex | 2014-07-09
Du musst angemeldet sein, um einen Kommentar abzugeben.
Algorithmische Bausteine
Beginnen wir mit der Montage der algorithmischen Bausteine aus der Standard Library:
std::begin()
/std::end()
sowie mitstd::next()
sind nur verfügbar, von C++11 und darüber hinaus. Für C++98 muss man schreiben diese selbst. Es gibt Ersatzstoffe, die von Boost.Bereich inboost::begin()
/boost::end()
und die von Boost.Dienstprogramm inboost::next()
.std::is_sorted
- Algorithmus ist nur verfügbar für C++11 und darüber hinaus. Für C++98, kann dies umgesetzt werden in Bezug aufstd::adjacent_find
- und eine hand-geschriebene function-Objekt. Boost.Algorithmus bietet auch eineboost::algorithm::is_sorted
als Ersatz.std::is_heap
- Algorithmus ist nur verfügbar für C++11 und darüber hinaus.Syntaktische leckereien
C++14 gibt transparente Komparatoren der form
std::less<>
die wirken polymorph auf Ihre Argumente. Dies verhindert, dass ein iterator-Typ. Dies kann verwendet werden, in Kombination mit C++11 ist "Standard-Funktion template-Argumente zu erstellen eine einzelne überlast für Sortier-algorithmen, die<
als Vergleich und diejenigen, die einen benutzerdefinierten Vergleichsfunktion Objekt.In C++11, kann man definieren, eine wiederverwendbare template-alias zu extrahieren, die ein iterator - value type fügt die kleine Unordnung zu Sortieren algorithmen Unterschriften:
In C++98 muss man schreiben zwei überladungen und verwenden Sie den verbose -
typename xxx<yyy>::type
syntaxauto
Parameter abgeleitet, wie Funktions-template-Argumente).value_type_t
.std::bind1st
/std::bind2nd
/std::not1
Art der syntax.boost::bind
und_1
/_2
Platzhalter-syntax.std::find_if_not
in der Erwägung, dass C++98 mussstd::find_if
mit einemstd::not1
um ein function-Objekt.C++ - Stil
Gibt es kein allgemein akzeptiertes C++14 Stil noch. Für besser für schlechter, ich verfolge Scott Meyers ' s Entwurf Effektives Modernes C++ und Herb Sutter überarbeitet GotW. Ich verwende die folgenden Stil-Empfehlungen:
()
und{}
wenn Objekte zu erstellen" und konsequent wählen verspannt-Initialisierung{}
statt der guten alten eingeklammerte Initialisierung()
(um side-step alle die-leidige-parse-Probleme, die in generischen code).typedef
spart Zeit und fügt Konsistenz.for (auto it = first; it != last; ++it)
Muster in einigen Orten, damit die for-Schleife invariante Prüfung für bereits sortierte sub-ranges. In der Produktion code, die Verwendung vonwhile (first != last)
und ein++first
irgendwo innerhalb der Schleife könnten etwas besser.Auswahl Sortieren
Auswahl Sortieren nicht die Anpassung an die Daten in keiner Weise, so dass seine Laufzeit ist immer
O(N^2)
. Aber die Auswahl Art hat die Eigenschaft, die Minimierung der Anzahl von swaps. In Anwendungen, wo die Kosten der swapping-Produkten ist hoch, die Auswahl Sortieren sehr gut kann der Algorithmus der Wahl.Implementieren Sie unter Verwendung der Standard-Bibliothek, benutz
std::min_element
zu finden, die Verbleibende Mindest-element, unditer_swap
tauschen Sie es in Stelle:Beachten Sie, dass
selection_sort
hat die bereits bearbeiteten Bereich[first, it)
sortiert, wie seine loop-invariant. Die minimalen Anforderungen sind forward-Iteratorenim Vergleich zustd::sort
's random-access-Iteratoren.Details weggelassen:
if (std::distance(first, last) <= 1) return;
(oder für vorwärts - /bidirektionale Iteratoren:if (first == last || std::next(first) == last) return;
).[first, std::prev(last))
weil das Letzte element wird garantiert der minimale Verbleibende element und nicht um eine swap.Insertion sort
Obwohl es eine der elementaren Sortier-algorithmen mit
O(N^2)
worst-case-Zeit, insertion sort ist der Algorithmus der Wahl, wenn die Daten fast sortiert ist (denn es ist adaptive) oder, wenn das problem klein ist (weil es hat einen niedrigen overhead). Aus diesen Gründen, und weil es auch stabilinsertion sort wird Häufig verwendet, um die rekursiven basisfall (wenn das problem klein ist) für höhere overhead-divide-and-conquer-algorithmen zur Sortierung, wie merge-sort oder quick sort.Umzusetzen
insertion_sort
mit der Standard-Bibliothek, benutzstd::upper_bound
um den Ort zu finden, wo das aktuelle element braucht, um zu gehen, und verwenden Siestd::rotate
verschieben der restlichen Elemente nach oben in den Eingangsbereich:Beachten Sie, dass
insertion_sort
hat die bereits bearbeiteten Bereich[first, it)
sortiert, wie seine loop-invariant. Insertion sort funktioniert auch mit vorwärts-Iteratoren.Details weggelassen:
if (std::distance(first, last) <= 1) return;
(oder für vorwärts - /bidirektionale Iteratoren:if (first == last || std::next(first) == last) return;
) und eine Schleife über dem Intervall[std::next(first), last)
weil das erste element ist garantiert an Ort und Stelle sein und erfordert nicht drehen.std::find_if_not
Algorithmus.Vier Live-Beispiele (C++14C++11C++98 und BoostC++98) für das fragment unten:
O(N^2)
Vergleiche, aber dies verbessert sich aufO(N)
Vergleiche für fast sortierte Eingaben. Die binäre Suche verwendet immerO(N log N)
Vergleiche.Quick sort
Wenn Sie sorgfältig umgesetzt, quick sort ist robust und hat
O(N log N)
erwartet die Komplexität, aber mitO(N^2)
worst-case-Komplexität, die ausgelöst werden kann, mit adversarially gewählten input-Daten. Wenn eine stabile Sortierung ist nicht erforderlich, quick-sort ist ein hervorragendes, Allgemeines Sortieren.Selbst für die einfachsten Versionen von quick sort ist ein bisschen mehr kompliziert zu implementieren unter Verwendung der Standard-Bibliothek als die anderen klassischen Sortier-algorithmen. Der Ansatz unten verwendet ein paar iterator utilities zu suchen, die mittlere element der Eingangsbereich
[first, last)
wie der Drehpunkt, dann verwenden Sie zwei Anrufe zustd::partition
(dieO(N)
) drei-Wege-partition des Eingangsbereichs in Segmente von Elementen, die kleiner als, gleich und größer als das ausgewählte pivot, beziehungsweise. Zum Schluss die beiden äußeren Segmente mit Elementen kleiner als und größer als das pivot sind rekursiv sortiert:Jedoch, quick sort ist eher schwierig zu bekommen korrekt und effizient ist, als jede der oben genannten Schritte werden sorgfältig geprüft und für die Produktion optimiert-level-code. Insbesondere für
O(N log N)
Komplexität, die pivot hat zu Folge, dass in einer ausgewogenen Teilung der Eingangsdaten, die nicht garantiert werden kann im Allgemeinen für einenO(1)
pivot, aber garantiert werden kann, setzt man die pivot alsO(N)
median der Eingangsbereich.Details weggelassen:
O(N^2)
Komplexität für die "organ pipe" input1, 2, 3, ..., N/2, ... 3, 2, 1
(denn die Mitte ist immer größer als alle anderen Elemente).O(N^2)
.std::partition
ist nicht die effizientesteO(N)
Algorithmus, um dieses Ergebnis zu erreichen.O(N log N)
Komplexität kann erreicht werden durch median pivot-Auswahl mitstd::nth_element(first, middle, last)
gefolgt von rekursiven Aufrufequick_sort(first, middle, cmp)
undquick_sort(middle, last, cmp)
.O(N)
Komplexität derstd::nth_element
teurer sein kann als dieO(1)
Komplexität eines median-of-3 pivot, gefolgt von einerO(N)
Aufrufstd::partition
(was ist ein cache-freundliche single-vorwärts-Durchlauf über die Daten).Merge-sort
Wenn mit
O(N)
zusätzliche Raum nicht von Belang ist, dann merge-sort ist eine ausgezeichnete Wahl: es ist das einzige stabilO(N log N)
Sortier-Algorithmus.Es ist einfach zu implementieren, Verwendung von Standard-algorithmen: verwenden Sie ein paar iterator utilities zu suchen, der Mitte der Eingangsbereich
[first, last)
und kombinieren Sie zwei rekursiv sortiert Segmente mit einemstd::inplace_merge
:Merge-sort erfordert bidirektionale Iteratoren, der Engpass wird die
std::inplace_merge
. Beachten Sie, dass beim Sortieren von verketteten Listen, merge-sort erfordert nurO(log N)
extra Raum (für Rekursion). Der letztere Algorithmus wird implementiert, indemstd::list<T>::sort
in der Standard-Bibliothek.Heap-sort
Heap-sort ist einfach zu implementieren, führt eine
O(N log N)
in-place Sortieren, aber ist nicht stabil.Ersten Schleife
O(N)
"heapify" - phase, setzt das array in heap um. Die zweite Schleife, dieO(N log N
) "sortdown" - phase, wiederholt extrahiert den maximalen und stellt die heap-Ordnung. Die Standard-Bibliothek macht dies extrem einfach:Falls Sie denken, dass es "Betrug" zu verwenden
std::make_heap
undstd::sort_heap
können Sie eine Ebene tiefer und schreiben Sie diese Funktionen selbst in Bezug aufstd::push_heap
undstd::pop_heap
bzw:Die Standard-Bibliothek gibt sowohl
push_heap
undpop_heap
KomplexitätO(log N)
. Beachten Sie jedoch, dass die äußere Schleife über den Bereich[first, last)
Ergebnisse inO(N log N)
Komplexität fürmake_heap
in der Erwägung, dassstd::make_heap
hat nurO(N)
Komplexität. Für die insgesamtO(N log N)
Komplexität derheap_sort
ist es egal.Details weggelassen:
O(N)
- implementation vonmake_heap
Testen
Hier sind vier Live-Beispiele (C++14C++11C++98 und BoostC++98) Prüfung alle fünf algorithmen, die auf einer Vielzahl von Eingängen (soll nicht erschöpfend sein oder strengen). Nur beachten Sie die enormen Unterschiede in der LOC: C++11/C++14 müssen rund 130 LOC in C++98 und Boost-190 (+50%) und C++98-mehr als 270 (+100%).
InformationsquelleAutor der Antwort TemplateRex
Anderen kleinen und eher elegant ursprünglich gefunden auf code review. Ich dachte, es war es Wert.
Zählen, Sortieren
Während es ist, sondern spezielle, zählen, Sortieren ist eine einfache integer-Algorithmus Sortieren, und können oft sehr schnell die Werte der Ganzzahlen zu Sortieren sind nicht allzu weit auseinander. Wahrscheinlich ist es ideal, wenn man je nach Art eine Sammlung von einer million Ganzzahlen bekannt zu sein, zwischen 0 und 100 zum Beispiel.
Zur Umsetzung ein sehr einfaches zählen, Sortieren, das funktioniert sowohl mit signed und unsigned-Integer, muss man den kleinsten und größten Elemente in der Sammlung zu Sortieren; der Unterschied wird Ihnen sagen, die Größe des array zählt zu reservieren. Dann, in einem zweiten Durchgang durch die Sammlung wird getan, um die Anzahl der vorkommen jedes Elements. Endlich schreiben wir wieder die erforderliche Anzahl von jeder Ganzzahl zurück zu der ursprünglichen Sammlung.
Zwar ist es nur hilfreich, wenn der Bereich der ganzen zahlen zu Sortieren ist bekannt, klein zu sein (in der Regel nicht größer als die Größe der Sammlung zu Sortieren), so dass das zählen, Sortieren mehr generische würde es langsamer für seine besten Fälle. Wenn der Bereich nicht bekannt ist, wird klein sein, mit einem anderen Algorithmus eine solche radix sortska_sort oder spreadsort können stattdessen verwendet werden.
Details weggelassen:
Hätten wir passierten die Grenze des Bereichs der Werte angenommen, die vom Algorithmus als Parameter, um völlig loszuwerden, die ersten
std::minmax_element
pass durch die Kollektion. Dadurch wird der Algorithmus sogar schneller, wenn ein sinnvoll-kleine Auswahl beschränken, ist bekannt durch andere Mittel. (Es muss nicht genau sein; die übergabe einer Konstante von 0 bis 100 ist noch viel besser als ein extra-pass über eine Millionen Elemente, um herauszufinden, dass die wahren Grenzen sind 1 bis 95. Auch 0 bis 1000 wäre es Wert; zusätzliche Elemente, die geschrieben werden, einmal mit null und einmal gelesen).Wachsenden
counts
on-the-fly ist ein weiterer Weg, um zu vermeiden, einen separaten ersten Durchgang. Die Verdoppelung dercounts
Größe jeder Zeit hat zu wachsen, gibt amortisiert O(1) Zeit pro element sortiert (siehe hash-Tabelle einfügen-Kosten-Analyse für den Nachweis, die exponentiell gewachsen ist der Schlüssel). Wächst am Ende für einen neuenmax
ist einfach mitstd::vector::resize
neue gelöschte Elemente.Ändern
min
on-the-fly und das einfügen neuer gelöschte Elemente an der front getan werden kann, mitstd::copy_backward
nach Anbau der Vektor. Dannstd::fill
auf null, die neuen Elemente.Den
counts
increment loop ist ein Histogramm. Wenn die Daten wahrscheinlich zu Wiederholungen, und die Anzahl der Plätze ist klein, kann es sinnvoll sein, abrollen über mehrere arrays zu reduzieren, das serialisieren der Daten Abhängigkeit Engpass von speichern/laden auf den gleichen Lagerplatz. Das bedeutet, dass mehr zählt null am start, und mehr, um eine Schleife am Ende, aber sollte es Wert sein, auf den meisten CPUs für unser Beispiel von Millionen von 0 bis 100 zahlen, vor allem, wenn die Eingabe bereits (teilweise) sortiert und haben lange läuft die gleiche Nummer.In der oben angegebenen Algorithmus verwenden wir eine
min == max
überprüfen, vorzeitig zurück zu kehren, wenn jedes element hat den gleichen Wert (in dem Fall die Sammlung ist sortiert). Ist es tatsächlich möglich, statt vollständig zu prüfen, ob die Sammlung ist bereits sortiert sind, während der Suche nach dem extremen Werte einer Gruppe mit keine zusätzliche Zeit verschwendet (wenn der erste pass ist immer noch memory-Engpass mit der zusätzlichen Arbeit zu aktualisieren, min und max). Ein solcher Algorithmus existiert nicht in der standard-Bibliothek und eines zu schreiben wäre viel mühsamer als das schreiben den rest zu zählen, Sortieren selbst. Es bleibt als übung für den Leser.Da der Algorithmus arbeitet nur mit integer-Werten, statischen assertions verwendet werden könnten, zu verhindern, dass Benutzer offensichtliche Art Fehler. In einigen Kontexten eine substitution scheitern mit
std::enable_if_t
könnten bevorzugt werden.Während moderne C++ ist cool, Zukunft C++ könnte noch Kühler: strukturierte Bindungen und einige Teile der Reicht TS würde der Algorithmus noch sauberer.
InformationsquelleAutor der Antwort Morwenn