Sortieralgorithmen
Ein Sortieralgorithmus ist ein Algorithmus, der eine Datenfolge nach einer bestimmten Sortiermethode anordnet; die sortierten Daten können dann in einem geordneten Array abgelegt werden.
SortierenEs bezeichnet den Prozess, eine Menge von Daten (wie ein Array oder eine Liste) in einer bestimmten Reihenfolge (aufsteigend oder absteigend) neu anzuordnen.
Die Grundlage der Sortierung ist normalerweise eine bestimmte Eigenschaft der DatenelementeSchlüsselwie die Größe von Zahlen, die lexikografische Reihenfolge von Zeichenketten usw.
Sortieren ist eines der grundlegendsten und zentralsten algorithmischen Probleme der Informatik. Ob es um die Organisation von Kontakten, die Analyse von Prüfungsergebnissen oder die Optimierung von Datenbankabfragen geht – Sortieralgorithmen sind überall.
Das Verständnis verschiedener Sortieralgorithmen hilft uns nicht nur, praktische Probleme zu lösen, sondern ist auch ein ausgezeichneter Weg, um Algorithmendesign- und Analyseideen tiefgehend zu verstehen.
Klassifizierung von Sortieralgorithmen
Sortieralgorithmen können anhand mehrerer Dimensionen klassifiziert werden, wobei die gebräuchlichste Klassifizierung auf ihrerZeitkomplexitätundSpeicherkomplexität basiert。
| Klassifizierungsdimension | Kategorie | Beschreibung | Typische Algorithmen |
|---|---|---|---|
| Zeitkomplexität | O(n²)-Niveau | einfach und intuitiv, aber wenig effizient, geeignet für kleine Datenmengen. | Bubblesort, Selectionsort, Insertionsort |
| O(n log n)-Niveau | relativ hohe Effizienz und ist die erste Wahl für die Verarbeitung großer Datenmengen. | Quicksort, Mergesort, Heapsort | |
| Speicherkomplexität | In-place-Sortierung | Verwendet während des Sortiervorgangs nur eine konstante Menge an zusätzlichem Speicherplatz. | Bubblesort, Selectionsort, Insertionsort, Shellsort, Heapsort, Quicksort. |
| Out-of-place-Sortierung | Während des Sortiervorgangs ist zusätzlicher Speicher proportional zur Datengröße erforderlich. | Mergesort, Countingsort, Bucketsort, Radixsort. | |
| Stabilität | Stabile Sortierung | Die relative Reihenfolge gleicher Elemente bleibt nach dem Sortieren unverändert. | Bubblesort, Insertionsort, Mergesort, Countingsort, Bucketsort, Radixsort. |
| Instabile Sortierung | Die relative Reihenfolge gleicher Elemente kann sich nach dem Sortieren ändern. | Selectionsort, Shellsort, Heapsort, Quicksort |
Erläuterung der Schlüsselkonzepte:
- ZeitkomplexitätMisst den Trend der Ausführungszeit eines Algorithmus bei zunehmender Datengröße.
- SpeicherkomplexitätMisst die Menge an zusätzlichem Speicherplatz, die während der Ausführung eines Algorithmus benötigt wird.
- StabilitätBei Elementen mit gleichen Werten, ob ihre relative Reihenfolge nach dem Sortieren unverändert bleibt. Dies ist bei der Sortierung nach mehreren Schlüsseln sehr wichtig.
Das folgende Flussdiagramm zeigt, wie man je nach unterschiedlichen Anforderungsszenarien eine vorläufige Auswahl unter mehreren klassischen Algorithmen trifft:

Grundlegende Sortieralgorithmen auf O(n²)-Niveau
Diese Algorithmen sind vom Konzept her einfach und bilden den Ausgangspunkt zum Verständnis der Sortierlogik.
Blasensortierung
KernideeWiederholt die zu sortierende Sequenz „durchlaufen“, dabei jeweils zwei benachbarte Elemente vergleichen und sie vertauschen, wenn sie in der falschen Reihenfolge sind. Bei jedem Durchlauf steigt das größte (oder kleinste) Element im aktuell unsortierten Teil an seine richtige Position auf.
Algorithmusschritte:
- Benachbarte Elemente vergleichen. Wenn das erste größer als das zweite ist (für aufsteigende Reihenfolge), tausche sie.
- Dasselbe für jedes Paar benachbarter Elemente tun, vom ersten Paar am Anfang bis zum letzten Paar am Ende. Nach diesem Schritt ist das letzte Element die größte Zahl.
- Die obigen Schritte für alle Elemente außer dem letzten wiederholen (weil jede Runde ein endgültiges Element festlegt).
- Die obigen Schritte weiterhin für immer weniger Elemente wiederholen, bis es kein Paar von Zahlen mehr zu vergleichen gibt.
Code-Implementierung:
Beispiel
"""
Bubble Sort (aufsteigend)
:param arr: Die zu sortierende Liste
"""
n = len(arr)
# Die äußere Schleife steuert die Anzahl der benötigten Durchläufe (n-1 Durchläufe)
for i in range(n - 1):
# Die innere Schleife führt den Vergleich und das Vertauschen benachbarter Elemente durch
# Nach jedem Durchlauf sind die letzten i+1 Elemente bereits sortiert, daher ist der Vergleichsbereich 0 bis n-1-i
for j in range(0, n - 1 - i):
if arr[j] > arr[j + 1]: # Wenn das vorherige Element größer als das folgende Element ist
# Tausche die Positionen der beiden Elemente
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
# Testdaten
test_data = [64, 34, 25, 12, 22, 11, 90]
print("Ursprüngliches Array:", test_data)
sorted_data = bubble_sort(test_data.copy()) # Verwende eine Kopie, um die ursprünglichen Daten nicht zu verändern
print("Nach dem Bubble-Sort:", sorted_data)
Ausgabe:
原始数组: [64, 34, 25, 12, 22, 11, 90] 冒泡排序后: [11, 12, 22, 25, 34, 64, 90]
Leistungsanalyse:
- ZeitkomplexitätBester Fall O(n) (erreichbar mit Optimierung, wenn bereits sortiert), schlechtester und durchschnittlicher Fall O(n²).
- Speicherkomplexität: O(1), es ist eine In-Place-Sortierung.
- Stabilität: stabil.
Auswahlsortierung
GrundideeFinde das kleinste (oder größte) Element in der unsortierten Sequenz, platziere es am Anfang der sortierten Sequenz, suche dann weiter das kleinste (größte) Element aus den verbleibenden unsortierten Elementen und platziere es am Ende der sortierten Sequenz. Fahre auf diese Weise fort, bis alle Elemente sortiert sind.
Algorithmusschritte:
- Anfangszustand: Die gesamte Sequenz ist der unsortierte Bereich.
- In der i-ten Runde (i beginnt bei 0) der Sortierung ist der aktuelle unsortierte Bereich
arr[i...n-1]Wähle aus diesem Bereich den Datensatz mit dem kleinsten Schlüsselarr[min_index]。 - wird
arr[min_index]Mit dem ersten Datensatz des unsortierten Bereichsarr[i]Tauschen. Auf diese Weise,arr[0...i]Dann wird der sortierte Bereich gebildet. - Nach n-1 Runden ist das Array sortiert.
Code-Implementierung:
Beispiel
"""
Selection Sort (aufsteigend)
:param arr: Die zu sortierende Liste
"""
n = len(arr)
for i in range(n):
# Angenommen, das Element am aktuellen Index i ist das kleinste
min_index = i
# Finde ein kleineres Element im Bereich von i+1 bis n-1
for j in range(i + 1, n):
if arr[j] < arr[min_index]:
min_index = j # Aktualisiere den Index des kleinsten Elements
# Tausche das gefundene kleinste Element mit dem Element an Position i
arr[i], arr[min_index] = arr[min_index], arr[i]
return arr
# Test
test_data = [64, 25, 12, 22, 11]
print("Ursprüngliches Array:", test_data)
print("Nach Selection Sort:", selection_sort(test_data.copy()))
Ausgabe:
原始数组: [64, 25, 12, 22, 11] 选择排序后: [11, 12, 22, 25, 64]
Leistungsanalyse:
- ZeitkomplexitätImmer O(n²), da vollständige Vergleiche erforderlich sind, unabhängig davon, ob die Daten sortiert sind.
- Speicherkomplexität: O(1), In-Place-Sortierung.
- Stabilität:instabil. Zum Beispiel die Sequenz
[5, 8, 5, 2, 9], in der ersten Runde wird das erste [Element]5und2getauscht, wodurch die beiden ihre5ursprüngliche Reihenfolge verlieren.
Insertionssortierung
KernideeDurch den Aufbau einer geordneten Sequenz werden für unsortierte Daten die sortierte Sequenz rückwärts durchsucht, die passende Position gefunden und dort eingefügt. Das ist wie das Anordnen von Spielkarten.
Algorithmusschritte:
- Behandle das erste Element als die sortierte Sequenz.
- Nimm das nächste Element und durchsuche die sortierte Elementfolge rückwärts.
- Wenn das sortierte Element größer als das neue Element ist, verschiebe das sortierte Element an die nächste Position.
- Wiederhole Schritt 3, bis du eine Position findest, an der das sortierte Element kleiner oder gleich dem neuen Element ist.
- Nach dem Einfügen des neuen Elements an dieser Position.
- Wiederhole die Schritte 2-5, bis alle Elemente verarbeitet sind.
Code-Implementierung:
Beispiel
"""
Insertionssortierung (aufsteigend)
:param arr: Die zu sortierende Liste
"""
n = len(arr)
# Beginne beim zweiten Element (Index 1), weil das erste Element standardmäßig bereits sortiert ist
for i in range(1, n):
key = arr[i] # Das aktuelle Element, das eingefügt werden soll
j = i - 1 # Index des letzten Elements in der sortierten Sequenz
# Durchsuche die sortierte Sequenz von hinten nach vorne, um die Einfügeposition von key zu finden
# Verschiebe gleichzeitig Elemente, die größer als key sind, um eine Position nach hinten
while j >= 0 and key < arr[j]:
arr[j + 1] = arr[j]
j -= 1
# Schlüssel an der gefundenen korrekten Position einfügen
arr[j + 1] = key
return arr
# Test
test_data = [12, 11, 13, 5, 6]
print("Ursprüngliches Array:", test_data)
print("Nach Insertionsort:", insertion_sort(test_data.copy()))
Ausgabe:
原始数组: [12, 11, 13, 5, 6] 插入排序后: [5, 6, 11, 12, 13]
Leistungsanalyse:
- ZeitkomplexitätBester Fall O(n) (bereits sortiert), schlechtester und durchschnittlicher Fall O(n²).
- Speicherkomplexität: O(1), In-Place-Sortierung.
- Stabilität: stabil.
- Eigenschaften: Für kleine Datenmengen oder nahezu sortierte Daten ist Insertionsort sehr effizient. Er ist der Algorithmus, zu dem fortgeschrittene Sortieralgorithmen (wie Timsort) bei kleinen Datenmengen wechseln.
Effiziente Sortieralgorithmen auf O(n log n)-Niveau
Wenn die Datenmenge wächst, werden O(n²)-Algorithmen sehr langsam. Die folgenden effizienteren Algorithmen sind das Rückgrat der Ingenieurspraxis.
Quicksort
Kernidee:Teile und herrscheWähle ein Pivot-Element. Durch einen Sortierdurchlauf werden die zu sortierenden Datensätze in zwei unabhängige Teile partitioniert, wobei die Schlüssel des einen Teils alle kleiner sind als die Schlüssel des anderen Teils; dann werden diese beiden Teile separat sortiert, um die gesamte Sequenz zu ordnen.
Algorithmusschritte:
- Pivot auswählenWähle ein Element aus der Sequenz und nenne es das "Pivot".
- PartitionsoperationOrdne die Sequenz so um, dass alle Elemente, die kleiner als das Pivot sind, davor platziert werden und alle Elemente, die größer als das Pivot sind, dahinter platziert werden (gleiche Zahlen können auf beiden Seiten stehen). Nachdem diese Partition beendet ist, befindet sich das Pivot an seiner endgültigen Position in der Mitte der Sequenz. Dies wird als Partitionsoperation bezeichnet.
- rekursiv sortierenSortiere rekursiv die Teilsequenz mit Elementen, die kleiner als das Pivot sind, und die Teilsequenz mit Elementen, die größer als das Pivot sind.
Code-Implementierung:
Beispiel
"""
Quick Sort (Aufsteigend) - Hauptfunktion
"""
def _quick_sort(arr, low, high):
if low < high:
# pi ist der Partitionsindex, arr[pi] ist jetzt an der richtigen Position
pi = partition(arr, low, high)
# Teilarrays vor und nach der Partition rekursiv sortieren
_quick_sort(arr, low, pi - 1)
_quick_sort(arr, pi + 1, high)
def partition(arr, low, high):
"""
Partitionsfunktion, das letzte Element als Pivot auswählen
:return: der endgültige korrekte Positionsindex des Pivot-Elements
"""
pivot = arr[high] # Wähle einen Pivot
i = low - 1 # Verweist auf das Ende des Teilarrays der Elemente, die kleiner als das Pivot-Element sind
for j in range(low, high):
# Wenn das aktuelle Element kleiner oder gleich dem Pivot-Element ist
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i] # Tauschen
# Platziere das Pivot-Element an der richtigen Position (i+1)
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
_quick_sort(arr, 0, len(arr) - 1)
return arr
# Test
test_data = [10, 80, 30, 90, 40, 50, 70]
print("Ursprüngliches Array:", test_data)
print("Nach Quick Sort:", quick_sort(test_data.copy()))
Ausgabe:
原始数组: [10, 80, 30, 90, 40, 50, 70] 快速排序后: [10, 30, 40, 50, 70, 80, 90]
Leistungsanalyse:
- ZeitkomplexitätDurchschnittsfall O(n log n), Worst Case O(n²) (wenn das Array bereits sortiert oder umgekehrt sortiert ist und das Pivot-Element schlecht gewählt wird). Der Worst Case kann weitgehend vermieden werden, indem das Pivot-Element zufällig ausgewählt wird oder die "Median-of-Three"-Methode verwendet wird.
- Speicherkomplexität: Durchschnittlich O(log n) (rekursiver Aufrufstapel), Worst-Case O(n).
- Stabilität:instabil。
Mergesort
Kernidee:Teile und herrscheEine typische Anwendung. Bereits geordnete Teilfolgen zusammenführen, um eine vollständig geordnete Sequenz zu erhalten. Das heißt, zuerst jede Teilfolge ordnen und dann die Teilfolgensegmente relativ zueinander ordnen.
Algorithmusschritte:
- TeilenTeile das aktuelle Intervall in zwei Teile, d. h., finde den Teilungspunkt
mid = (low + high)/2。 - ErobernDie beiden Teilintervalle rekursiv [sortieren]
arr[low...mid]undarr[mid+1...high]Führe Merge Sort aus. Die Abbruchbedingung für die Rekursion ist, dass die Länge des Teilintervalls 1 beträgt (ein Element gilt als natürlich geordnet). - ZusammenführenZwei sortierte Teilintervalle zu einem geordneten Intervall zusammenführen.
Code-Implementierung:
Beispiel
"""
Merge Sort (aufsteigend) - Hauptfunktion
"""
if len(arr) > 1:
mid = len(arr) // 2 # Mittelpunkt finden und das Array aufteilen
left_half = arr[:mid]
right_half = arr[mid:]
# Die linken und rechten Hälften rekursiv sortieren
merge_sort(left_half)
merge_sort(right_half)
# Die beiden sortierten Teilarrays zusammenführen
i = j = k = 0
# Vergleiche die Elemente der linken und rechten Teilarrays und setze das kleinere in das ursprüngliche Array
while i < len(left_half) and j < len(right_half):
if left_half[i] < right_half[j]:
arr[k] = left_half[i]
i += 1
else:
arr[k] = right_half[j]
j += 1
k += 1
# Prüfe, ob noch Elemente übrig sind (linke Hälfte oder rechte Hälfte)
while i < len(left_half):
arr[k] = left_half[i]
i += 1
k += 1
while j < len(right_half):
arr[k] = right_half[j]
j += 1
k += 1
return arr
# Test
test_data = [38, 27, 43, 3, 9, 82, 10]
print("Ursprüngliches Array:", test_data)
print("Nach Merge Sort:", merge_sort(test_data.copy()))
Ausgabe:
原始数组: [38, 27, 43, 3, 9, 82, 10] 归并排序后: [3, 9, 10, 27, 38, 43, 82]
Leistungsanalyse:
- ZeitkomplexitätBest-, Worst- und Average-Fall sind alle O(n log n). Die Leistung ist sehr stabil.
- SpeicherkomplexitätO(n), erfordert zusätzlichen Speicher, der dem ursprünglichen Array entspricht, für das Zusammenführen.
- Stabilität: stabil.
Heap-Sortierung
Kernidee: unter Verwendung vonHeapEin für diese Datenstruktur entwickelter Sortieralgorithmus. Ein Heap ist eine nahezu vollständige Binärbaumstruktur, die zudem die Heap-Eigenschaft erfüllt: Der Wert eines Elternknotens ist stets größer oder gleich (Max-Heap) bzw. kleiner oder gleich (Min-Heap) dem Wert eines beliebigen Kindknotens.
Algorithmusschritte:
- Max-Heap aufbauenDie zu sortierende Sequenz zu einem Max-Heap umbauen. Zu diesem Zeitpunkt ist der maximale Wert der gesamten Sequenz der Wurzelknoten an der Spitze des Heaps.
- Das oberste Heap-Element mit dem letzten Element vertauschenDas oberste Element (das Maximum) mit dem letzten Element vertauschen; das letzte Element ist nun das Maximum.
- Heap-Struktur anpassen: für die verbleibenden
n-1Die verbleibenden Elemente wieder zu einem Max-Heap aufbauen, was den nächstgrößten Wert ergibt. Diesen mit dem neuen letzten Element (Position n-1) vertauschen. - Wiederholung ausführenSchritte 2 und 3 wiederholen, bis die Heap-Größe 1 beträgt; die Sortierung ist abgeschlossen.
Code-Implementierung:
Beispiel
"""
Heap-Sortierung (aufsteigend) - Verwendung eines Max-Heaps
"""
n = len(arr)
# Schritt 1: Den Max-Heap aufbauen. Beginne mit dem Anpassen nach oben vom letzten Nicht-Blattknoten
# Index des letzten Nicht-Blattknotens = n//2 - 1
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
# Schritte 2 & 3: Extrahiere das oberste Element (Maximum) nacheinander und passe den Heap an
for i in range(n - 1, 0, -1):
# Tausche das aktuelle oberste Element (Maximum) mit dem letzten Element
arr[i], arr[0] = arr[0], arr[i]
# Passe die verbleibenden Elemente an, um wieder einen Max-Heap zu bilden, wobei die Heap-Größe auf i gesetzt ist
heapify(arr, i, 0)
return arr
def heapify(arr, n, i):
"""
Passe den bei i verwurzelten Teilbaum an, um daraus einen Max-Heap zu machen
:param arr: Heap-Array
:param n: aktuelle Heap-Größe
:param i: Index der aktuellen Wurzel
"""
largest = i # Initialisiere den Maximalwert als Wurzel
left = 2 * i + 1 # Index des linken Kindes
right = 2 * i + 2 # Index des rechten Kindes
# Wenn das linke Kind existiert und größer als die Wurzel ist
if left < n and arr[left] > arr[largest]:
largest = left
# Wenn das rechte Kind existiert und größer als das aktuelle Maximum ist
if right < n and arr[right] > arr[largest]:
largest = right
# Wenn das Maximum nicht die Wurzel ist, tausche und passe den defekten Teil-Heap rekursiv an
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)
# Test
test_data = [4, 10, 3, 5, 1]
print("Ursprüngliches Array:", test_data)
print("Nach dem Heap-Sort:", heap_sort(test_data.copy()))
Ausgabe:
原始数组: [4, 10, 3, 5, 1] 堆排序后: [1, 3, 4, 5, 10]
Leistungsanalyse:
- ZeitkomplexitätBest-, Worst- und Average-Case sind alle O(n log n).
- Speicherkomplexität: O(1), In-Place-Sortierung.
- Stabilität:instabil。
Zusammenfassung des Leistungsvergleichs der Algorithmen
Um die Eigenschaften der oben genannten Algorithmen intuitiver zu vergleichen, fassen wir sie in der folgenden Tabelle zusammen:
| Algorithmen | Durchschnittliche Zeitkomplexität | Zeitkomplexität im schlechtesten Fall | Speicherkomplexität | Stabilität | Hauptfunktionen |
|---|---|---|---|---|---|
| Blasensortierung | O(n²) | O(n²) | O(1) | Stabil | Einfach, geringe Effizienz, für den Unterricht geeignet. |
| Auswahlsortierung | O(n²) | O(n²) | O(1) | instabil | Weniger Vertauschungen, aber eine feste große Anzahl von Vergleichen. |
| Insertionssortierung | O(n²) | O(n²) | O(1) | Stabil | Effizient für kleine Datenmengen oder nahezu sortierte Daten. |
| Quicksort | O(n log n) | O(n²) | O(log n) | instabil | Beste durchschnittliche Leistung, ist der am weitesten verbreitete Sortieralgorithmus. |
| Mergesort | O(n log n) | O(n log n) | O(n) | Stabil | Stabile Leistung, benötigt aber zusätzlichen Speicher. Wird häufig für externes Sortieren verwendet. |
| Heap-Sortierung | O(n log n) | O(n log n) | O(1) | instabil | In-Place-Sortierung und O(n log n) selbst im schlechtesten Fall. |
Übungsaufgaben
Nach dem theoretischen Lernen ist praktische Übung der beste Weg, das Wissen zu festigen.
Übung 1: Algorithmusauswahl und -implementierung
Welchen Sortieralgorithmus würden Sie in den folgenden Szenarien wählen? Erklären Sie kurz Ihre Gründe.
- Sortiere ein Array mit 10 Zahlen.
- Sortiere eine verkettete Liste von 1 Million Studierendenobjekten nach Studierenden-ID, wobei Stabilität erforderlich ist.
- Sortiere ein großes Array in einem eingebetteten System mit begrenztem Speicher.
- Es müssen in Echtzeit die K größten Elemente aus einem Datenstrom ermittelt werden.
Ansatz der Referenzantwort:
- Insertion SortGeringe Datenmenge; Insertion Sort ist einfach, vorteilhaft bei nahezu sortierten Daten und hat einen kleinen konstanten Faktor.
- Merge SortGroße Datenmenge, stabile Sortierung ist erforderlich, und die Struktur der verketteten Liste ermöglicht es, die Merge-Operation von Merge Sort in O(1)-Speicher abzuschließen (nur durch Ändern von Zeigern).
- Heap SortBegrenzter Speicher, In-Place-Sortierung ist erforderlich, und Heap Sort garantiert O(n log n)-Laufzeit auch im schlechtesten Fall.
- Einen Min-Heap der Größe K beibehaltenDurchlaufe den Datenstrom und verwende einen Heap, um die aktuellen K größten Elemente beizubehalten. Dies ist keine vollständige Sortierung, sondern nutzt die Eigenschaften eines Heaps aus.
Übung 2: Code-Debugging und Optimierung
Die folgende Bubble-Sort-Implementierung hat einen kleinen Fehler und eine Stelle, die optimiert werden kann; bitte finde und behebe sie.
Beispiel
n = len(arr)
for i in range(n):
for j in range(n - 1):
if arr[j] > arr[j]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
Hinweis:
- Vergleichsbedingung
arr[j] > arr[j]Immer falsch. - Die Grenze der inneren Schleife kann optimiert werden, da nach jeder Runde die hinteren Elemente bereits sortiert sind.
Übung 3: Praktische Umsetzung
Versuchen Sie, es selbst von Grund auf zu implementierenQuicksort, und denken Sie:
- Wenn der Benchmark
pivotWas passiert, wenn beim Sortieren eines bereits sortierten Arrays immer das erste Element als Pivot gewählt wird? - Wie Sie es ändern
partitionÄndern Sie die Partitionierungsfunktion so ab, dass sie zu einem "Drei-Wege-Quicksort" wird, der Arrays mit vielen Duplikaten effizienter verarbeiten kann.