Quicksort mit Python
Im wirklichen Leben sollten wir immer die von Python bereitgestellte integrierte Sortierung verwenden. Das Verständnis des Quicksort- Algorithmus ist jedoch aufschlussreich.
Mein Ziel hier ist es, das Thema so aufzuschlüsseln, dass es für den Leser leicht verständlich und reproduzierbar ist, ohne auf Referenzmaterialien zurückgreifen zu müssen.
Der Quicksort-Algorithmus ist im Wesentlichen der folgende:
- Wählen Sie einen Pivot-Datenpunkt aus.
- Verschieben Sie alle Datenpunkte, die kleiner als (unter) dem Drehpunkt sind, an eine Position unterhalb des Drehpunkts. Verschieben Sie diejenigen, die größer oder gleich (über) dem Drehpunkt sind, an eine Position darüber.
- Wenden Sie den Algorithmus auf die Bereiche über und unter dem Drehpunkt an
Wenn die Daten zufällig verteilt sind, entspricht die Auswahl des ersten Datenpunkts als Drehpunkt einer zufälligen Auswahl.
Lesbares Beispiel:
Schauen wir uns zunächst ein lesbares Beispiel an, in dem Kommentare und Variablennamen verwendet werden, um auf Zwischenwerte zu verweisen:
def quicksort(xs):
"""Given indexable and slicable iterable, return a sorted list"""
if xs: # if given list (or tuple) with one ordered item or more:
pivot = xs[0]
# below will be less than:
below = [i for i in xs[1:] if i < pivot]
# above will be greater than or equal to:
above = [i for i in xs[1:] if i >= pivot]
return quicksort(below) + [pivot] + quicksort(above)
else:
return xs # empty list
Um den hier gezeigten Algorithmus und Code neu zu formulieren, verschieben wir Werte über dem Drehpunkt nach rechts und Werte unter dem Drehpunkt nach links und übergeben diese Partitionen dann an dieselbe Funktion, um sie weiter zu sortieren.
Golf:
Dies kann auf 88 Zeichen gespielt werden:
q=lambda x:x and q([i for i in x[1:]if i<=x[0]])+[x[0]]+q([i for i in x[1:]if i>x[0]])
Um zu sehen, wie wir dorthin gelangen, nehmen Sie zuerst unser lesbares Beispiel, entfernen Sie Kommentare und Dokumentzeichenfolgen und suchen Sie den Drehpunkt an Ort und Stelle:
def quicksort(xs):
if xs:
below = [i for i in xs[1:] if i < xs[0]]
above = [i for i in xs[1:] if i >= xs[0]]
return quicksort(below) + [xs[0]] + quicksort(above)
else:
return xs
Finden Sie jetzt unten und oben vor Ort:
def quicksort(xs):
if xs:
return (quicksort([i for i in xs[1:] if i < xs[0]] )
+ [xs[0]]
+ quicksort([i for i in xs[1:] if i >= xs[0]]))
else:
return xs
and
Wenn wir nun wissen, dass das vorherige Element zurückgegeben wird, wenn es falsch ist, oder wenn es wahr ist, wertet es das folgende Element aus und gibt es zurück:
def quicksort(xs):
return xs and (quicksort([i for i in xs[1:] if i < xs[0]] )
+ [xs[0]]
+ quicksort([i for i in xs[1:] if i >= xs[0]]))
Da Lambdas einen einzelnen Ausdruck zurückgeben und wir zu einem einzelnen Ausdruck vereinfacht haben (obwohl er unleserlicher wird), können wir jetzt ein Lambda verwenden:
quicksort = lambda xs: (quicksort([i for i in xs[1:] if i < xs[0]] )
+ [xs[0]]
+ quicksort([i for i in xs[1:] if i >= xs[0]]))
Um auf unser Beispiel zu reduzieren, kürzen Sie die Funktions- und Variablennamen auf einen Buchstaben und entfernen Sie das nicht erforderliche Leerzeichen.
q=lambda x:x and q([i for i in x[1:]if i<=x[0]])+[x[0]]+q([i for i in x[1:]if i>x[0]])
Beachten Sie, dass dieses Lambda, wie die meisten Code-Golfer, ein ziemlich schlechter Stil ist.
In-Place-Quicksort mithilfe des Hoare-Partitionierungsschemas
Die vorherige Implementierung erstellt viele unnötige zusätzliche Listen. Wenn wir dies vor Ort tun können, vermeiden wir Platzverschwendung.
Die folgende Implementierung verwendet das Hoare-Partitionierungsschema, über das Sie auf Wikipedia mehr lesen können (aber wir haben anscheinend bis zu 4 redundante Berechnungen pro partition()
Aufruf entfernt, indem wir die while-Schleifensemantik anstelle von do-while verwendet und die Verengungsschritte an das Ende von verschoben haben die äußere while-Schleife.).
def quicksort(a_list):
"""Hoare partition scheme, see https://en.wikipedia.org/wiki/Quicksort"""
def _quicksort(a_list, low, high):
# must run partition on sections with 2 elements or more
if low < high:
p = partition(a_list, low, high)
_quicksort(a_list, low, p)
_quicksort(a_list, p+1, high)
def partition(a_list, low, high):
pivot = a_list[low]
while True:
while a_list[low] < pivot:
low += 1
while a_list[high] > pivot:
high -= 1
if low >= high:
return high
a_list[low], a_list[high] = a_list[high], a_list[low]
low += 1
high -= 1
_quicksort(a_list, 0, len(a_list)-1)
return a_list
Ich bin mir nicht sicher, ob ich es gründlich genug getestet habe:
def main():
assert quicksort([1]) == [1]
assert quicksort([1,2]) == [1,2]
assert quicksort([1,2,3]) == [1,2,3]
assert quicksort([1,2,3,4]) == [1,2,3,4]
assert quicksort([2,1,3,4]) == [1,2,3,4]
assert quicksort([1,3,2,4]) == [1,2,3,4]
assert quicksort([1,2,4,3]) == [1,2,3,4]
assert quicksort([2,1,1,1]) == [1,1,1,2]
assert quicksort([1,2,1,1]) == [1,1,1,2]
assert quicksort([1,1,2,1]) == [1,1,1,2]
assert quicksort([1,1,1,2]) == [1,1,1,2]
Fazit
Dieser Algorithmus wird häufig in Informatikkursen gelehrt und in Vorstellungsgesprächen nachgefragt. Es hilft uns, über Rekursion und Teilen und Erobern nachzudenken.
Quicksort ist in Python nicht sehr praktisch, da unser integrierter Timsort- Algorithmus sehr effizient ist und wir Rekursionsgrenzen haben. Wir würden erwarten, Listen an Ort und Stelle zu sortieren list.sort
oder neue sortierte Listen mit zu erstellen sorted
- beide nehmen ein key
und ein reverse
Argument an.
my_list = list1 + list2 + ...
. Oder packen Sie Listen in eine neue Liste ausmy_list = [*list1, *list2]