Quicksort avec Python
Dans la vie réelle, nous devrions toujours utiliser le tri intégré fourni par Python. Cependant, la compréhension du quicksort est instructif.
Mon objectif ici est de décomposer le sujet de manière à ce qu'il soit facilement compréhensible et reproductible par le lecteur sans avoir à revenir à des documents de référence.
L'algorithme de quicksort 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 inférieure au pivot - déplacez ceux qui sont supérieurs ou égaux (au-dessus) au pivot vers une position supérieure à celui-ci.
- Appliquer l'algorithme aux zones situées 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 est équivalente à une sélection aléatoire.
Exemple lisible :
Tout d'abord, examinons un exemple lisible qui utilise des commentaires et des noms de variables pour indiquer 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 présentés ici, nous déplaçons les valeurs au-dessus du pivot vers la droite, et les valeurs en dessous du pivot vers la gauche, puis nous transmettons ces partitions à la même fonction pour un tri supplémentaire.
Golfé :
Ce dernier peut être golfé jusqu'à 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, prenons d'abord notre exemple lisible, supprimons les commentaires et les docstrings, et trouvons 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 le dessous et le 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 s'il est 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 cela 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 revenir à notre exemple, raccourcissez les noms de fonctions et de 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 cette lambda, comme la plupart des golfs de code, est plutôt de mauvais style.
Tri rapide sur place, utilisant le schéma de partitionnement de Hoare.
L'implémentation précédente crée beaucoup de listes supplémentaires inutiles. Si nous pouvons faire cela en place, nous éviterons de gaspiller de l'espace.
L'implémentation ci-dessous utilise le schéma de partitionnement de Hoare, que vous pouvez utiliser dans le cadre de votre projet. Pour en savoir plus, consultez wikipedia (mais nous avons apparemment supprimé jusqu'à 4 calculs redondants par partition()
en utilisant la sémantique de la boucle while au lieu de do-while et en déplaçant les étapes de rétrécissement à 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 suis pas sûr de l'avoir testé assez minutieusement :
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. Il nous aide à réfléchir à la récursion et à la méthode "diviser pour régner".
La fonction Quicksort n'est pas très pratique en Python, car notre module intégré timsort L'algorithme est assez efficace, et nous avons des limites de récursion. Nous nous attendrions à trier les listes in-place avec list.sort
ou créer de nouvelles listes triées avec sorted
- qui prennent tous deux un key
y reverse
argument.