Quicksort avec Python
Dans la vraie vie, nous devrions toujours utiliser le tri intégré fourni par Python. Cependant, comprendre l' algorithme de tri rapide est instructif.
Mon but ici est de décomposer le sujet de manière à ce qu'il soit facilement compris et reproductible par le lecteur sans avoir à revenir sur les matériaux de référence.
L'algorithme de tri rapide est essentiellement le suivant:
- Sélectionnez un point de données pivot.
- Déplacez tous les points de données inférieurs (au-dessous) du pivot vers une position sous le pivot - déplacez ceux supérieurs ou égaux à (au-dessus) du pivot vers une position au-dessus.
- Appliquer l'algorithme aux zones au-dessus et en dessous du pivot
Si les données sont distribuées de manière aléatoire, la sélection du premier point de données comme pivot équivaut à une sélection aléatoire.
Exemple lisible:
Tout d'abord, regardons un exemple lisible qui utilise des commentaires et des noms de variables pour pointer vers des valeurs intermédiaires:
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
Pour reformuler l'algorithme et le code illustrés ici, nous déplaçons les valeurs au-dessus du pivot vers la droite et les valeurs sous le pivot vers la gauche, puis transmettons ces partitions à la même fonction pour être triées davantage.
Golfé:
Cela peut être joué à 88 caractères:
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]])
Pour voir comment nous y arrivons, prenez d'abord notre exemple lisible, supprimez les commentaires et les docstrings, et recherchez le pivot sur place:
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
Trouvez maintenant ci-dessous et ci-dessus, en place:
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
Maintenant, sachant que and
renvoie l'élément précédent si faux, sinon s'il est vrai, il évalue et renvoie l'élément suivant, nous avons:
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]]))
Puisque les lambdas renvoient une seule expression, et que nous avons simplifié à une seule expression (même si elle devient de plus en plus illisible), nous pouvons maintenant utiliser un lambda:
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]]))
Et pour réduire à notre exemple, raccourcissez les noms des fonctions et des variables à une lettre et éliminez les espaces qui ne sont pas nécessaires.
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]])
Notez que ce lambda, comme la plupart des jeux de code, est plutôt mauvais.
Tri rapide sur place, à l'aide du schéma de partitionnement Hoare
L'implémentation précédente crée beaucoup de listes supplémentaires inutiles. Si nous pouvons le faire sur place, nous éviterons de gaspiller de l'espace.
L'implémentation ci-dessous utilise le schéma de partitionnement Hoare, sur lequel vous pouvez en savoir plus sur wikipedia (mais nous avons apparemment supprimé jusqu'à 4 calculs redondants par partition()
appel en utilisant la sémantique en boucle while au lieu de do-while et en déplaçant les étapes de réduction à la fin de la boucle while externe.).
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
Je ne sais pas si je l'ai suffisamment testé:
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]
Conclusion
Cet algorithme est fréquemment enseigné dans les cours d'informatique et demandé lors des entretiens d'embauche. Cela nous aide à penser à la récursivité et à diviser pour conquérir.
Quicksort n'est pas très pratique en Python car notre algorithme de tri temporel intégré est assez efficace et nous avons des limites de récursivité. Nous nous attendrions à trier les listes sur place avec list.sort
ou à créer de nouvelles listes triées avec sorted
- les deux prenant un argument key
et reverse
.
my_list = list1 + list2 + ...
. Ou décompressez les listes dans une nouvelle listemy_list = [*list1, *list2]