python pour débutant
Tri est un bloc de construction de base sur lequel de nombreux autres algorithmes sont construits. C'est lié à plusieurs idées passionnantes que vous verrez tout au long de votre carrière en programmation. Comprendre comment les algorithmes de tri en Python fonctionnent en arrière-plan est une étape fondamentale vers la mise en œuvre d'algorithmes corrects et efficaces qui résolvent les problèmes du monde réel.
Dans ce didacticiel, vous apprendrez:
- Quelle différence algorithmes de tri en Python travailler et comment ils se comparent dans différentes circonstances
- Comment Fonctionnalité de tri intégrée de Python travaille dans les coulisses
- Comment différents concepts informatiques comme récursivité et diviser et conquérir appliquer au tri
- Comment mesurer l'efficacité d'un algorithme en utilisant Notation Big O et Python
timeit
module
À la fin de ce didacticiel, vous comprendrez les algorithmes de tri d'un point de vue théorique et pratique. Plus important encore, vous aurez une meilleure compréhension des différentes techniques de conception d'algorithmes que vous pouvez appliquer à d'autres domaines de votre travail. Commençons!
L'importance des algorithmes de tri en Python
Le tri est l'un des algorithmes les plus étudiés en informatique. Il existe des dizaines d'implémentations et d'applications de tri différentes que vous pouvez utiliser pour rendre votre code plus efficace et plus efficace.
Vous pouvez utiliser le tri pour résoudre un large éventail de problèmes:
-
Recherche: La recherche d'un élément sur une liste fonctionne beaucoup plus rapidement si la liste est triée.
-
Sélection: La sélection d'éléments dans une liste en fonction de leur relation avec le reste des éléments est plus facile avec les données triées. Par exemple, trouver le ke-la plus grande ou la plus petite valeur, ou trouver la valeur médiane de la liste, est beaucoup plus facile lorsque les valeurs sont dans l'ordre croissant ou décroissant.
-
Doublons: La recherche de valeurs en double sur une liste peut être effectuée très rapidement lorsque la liste est triée.
-
Distribution: L'analyse de la distribution de fréquence des éléments d'une liste est très rapide si la liste est triée. Par exemple, trouver l'élément qui apparaît le plus ou le moins souvent est relativement simple avec une liste triée.
Des applications commerciales à la recherche universitaire et partout ailleurs, il existe d'innombrables façons d'utiliser le tri pour vous faire gagner du temps et des efforts.
Algorithme de tri intégré de Python
Le langage Python, comme de nombreux autres langages de programmation de haut niveau, offre la possibilité de trier les données hors de la boîte en utilisant trié ()
. Voici un exemple de tri d'un tableau d'entiers:
>>> tableau = [[[[8, 2, 6, 4, 5]
>>> trié(tableau)
[2, 4, 5, 6, 8]
Vous pouvez utiliser trié ()
pour trier n'importe quelle liste tant que les valeurs à l'intérieur sont comparables.
L'importance de la complexité temporelle
Ce didacticiel couvre deux façons différentes de mesurer la Durée d'algorithmes de tri:
- Pour un point de vue pratique, vous mesurerez le temps d'exécution des implémentations à l'aide du
timeit
module. - Pour une perspective plus théorique, vous mesurerez complexité d'exécution des algorithmes utilisant Notation Big O.
Timing votre code
Lorsque vous comparez deux algorithmes de tri en Python, il est toujours instructif de regarder combien de temps chacun prend pour s'exécuter. Le temps spécifique pris par chaque algorithme sera en partie déterminé par votre matériel, mais vous pouvez toujours utiliser le temps proportionnel entre les exécutions pour vous aider à décider quelle implémentation est plus efficace en temps.
Dans cette section, vous vous concentrerez sur un moyen pratique de mesurer le temps réel nécessaire pour exécuter vos algorithmes de tri à l'aide du timeit
module. Pour plus d'informations sur les différentes façons dont vous pouvez chronométrer l'exécution de code en Python, consultez Fonctions de minuterie Python: trois façons de surveiller votre code.
Voici une fonction que vous pouvez utiliser pour chronométrer vos algorithmes:
1 de Aléatoire importation randint
2 de timeit importation répéter
3
4 def run_sorting_algorithm(algorithme, tableau):
5 # Configurer le contexte et préparer l'appel vers le spécifié
6 # algorithme utilisant le tableau fourni. Importez uniquement
7 # fonction d'algorithme si ce n'est pas le `sorted ()` intégré.
8 setup_code = F"de __main__ import algorithme"
9 si algorithme ! = "trié" autre ""
dix
11 stmt = F"algorithme(array) "
12
13 # Exécutez le code dix fois et retournez l'heure
14 # en secondes que chaque exécution a pris
15 fois = répéter(installer=setup_code, stmt=stmt, répéter=3, nombre=dix)
16
17 # Enfin, affichez le nom de l'algorithme et le
18 # temps minimum nécessaire pour s'exécuter
19 impression(F"Algorithme: algorithme. Temps d'exécution minimum: min (times) ")
Dans cet exemple, run_sorting_algorithm ()
reçoit le nom de l'algorithme et le tableau d'entrée qui doit être trié. Voici une explication ligne par ligne de son fonctionnement:
-
Ligne 8 importe le nom de l'algorithme en utilisant la magie des chaînes f de Python. C'est ainsi que
timeit.repeat ()
sait d'où appeler l'algorithme. Notez que cela n'est nécessaire que pour les implémentations personnalisées utilisées dans ce didacticiel. Si l'algorithme spécifié est le intégrétrié ()
, alors rien ne sera importé. -
Ligne 11 prépare l'appel à l'algorithme avec le tableau fourni. Il s'agit de l'instruction qui sera exécutée et chronométrée.
-
Ligne 15 appels
timeit.repeat ()
avec le code d'installation et l'instruction. Cela appellera l'algorithme de tri spécifié dix fois, renvoyant le nombre de secondes que chacune de ces exécutions a prises. -
Ligne 19 identifie le temps le plus court retourné et l'imprime avec le nom de l'algorithme.
Remarque: Une idée fausse courante est que vous devriez trouver le temps moyen de chaque exécution de l'algorithme au lieu de sélectionner le temps le plus court. Les mesures de temps sont bruyantes car le système exécute simultanément d'autres processus. Le temps le plus court est toujours le moins bruyant, ce qui en fait la meilleure représentation du véritable temps d'exécution de l'algorithme.
Voici un exemple d'utilisation run_sorting_algorithm ()
pour déterminer le temps nécessaire pour trier un tableau de dix mille valeurs entières à l'aide trié ()
:
21 ARRAY_LENGTH = 10000
22
23 si __Nom__ == "__principale__":
24 # Générer un tableau d'éléments `ARRAY_LENGTH` consistant
25 Nombre de valeurs entières aléatoires entre 0 et 999
26 tableau = [[[[randint(0, 1000) pour je dans gamme(ARRAY_LENGTH)]
27
28 # Appeler la fonction en utilisant le nom de l'algorithme de tri
29 # et le tableau que vous venez de créer
30 run_sorting_algorithm(algorithme="trié", tableau=tableau)
Si vous enregistrez le code ci-dessus dans un sorting.py
fichier, alors vous pouvez l'exécuter à partir du terminal et voir sa sortie:
$ python sorting.py
Algorithme: trié. Temps d'exécution minimum: 0,010945824000000007
N'oubliez pas que le temps en secondes de chaque expérience dépend en partie du matériel que vous utilisez, vous verrez donc probablement des résultats légèrement différents lors de l'exécution du code.
Mesurer l'efficacité avec la notation Big O
Le temps spécifique nécessaire à un algorithme pour s'exécuter n'est pas suffisant pour obtenir une image complète de son complexité temporelle. Pour résoudre ce problème, vous pouvez utiliser la notation Big O (prononcée «big oh»). Big O est souvent utilisé pour comparer différentes implémentations et décider laquelle est la plus efficace, en ignorant les détails inutiles et en se concentrant sur ce qui est le plus important dans l'exécution d'un algorithme.
Le temps en secondes requis pour exécuter différents algorithmes peut être influencé par plusieurs facteurs indépendants, notamment la vitesse du processeur ou la mémoire disponible. Big O, d'autre part, fournit une plate-forme pour exprimer la complexité de l'exécution en termes indépendants du matériel. Avec Big O, vous exprimez la complexité en termes de rapidité d'exécution de votre algorithme par rapport à la taille de l'entrée, d'autant plus que l'entrée augmente arbitrairement.
En admettant que n est la taille de l'entrée d'un algorithme, la notation Big O représente la relation entre n et le nombre d'étapes que l'algorithme prend pour trouver une solution. Big O utilise une lettre majuscule «O» suivie de cette relation entre parenthèses. Par exemple, Sur) représente des algorithmes qui exécutent un certain nombre d'étapes proportionnelles à la taille de leur entrée.
Bien que ce didacticiel ne va pas approfondir les détails de la notation Big O, voici cinq exemples de la complexité d'exécution de différents algorithmes:
Big O | Complexité | La description |
---|---|---|
O (1) | constant | Le temps d'exécution est constant quelle que soit la taille de l'entrée. La recherche d'un élément dans une table de hachage est un exemple d'une opération qui peut être effectuée dans temps constant. |
Sur) | linéaire | Le runtime croît linéairement avec la taille de l'entrée. Une fonction qui vérifie une condition sur chaque élément d'une liste est un exemple de Sur) algorithme. |
Sur2) | quadratique | Le runtime est une fonction quadratique de la taille de l'entrée. Une implémentation naïve de la recherche de valeurs en double dans une liste, dans laquelle chaque élément doit être vérifié deux fois, est un exemple d'algorithme quadratique. |
O (2n) | exponentiel | Le runtime croît de façon exponentielle avec la taille de l'entrée. Ces algorithmes sont considérés comme extrêmement inefficaces. Un exemple d'algorithme exponentiel est le problème des trois couleurs. |
O (log n) | logarithmique | Le runtime croît de façon linéaire tandis que la taille de l'entrée croît de façon exponentielle. Par exemple, s'il faut une seconde pour traiter mille éléments, alors il faudra deux secondes pour traiter dix mille, trois secondes pour traiter cent mille, et ainsi de suite. La recherche binaire est un exemple d'algorithme d'exécution logarithmique. |
Ce didacticiel couvre la complexité d'exécution Big O de chacun des algorithmes de tri discutés. Il comprend également une brève explication de la façon de déterminer le temps d'exécution sur chaque cas particulier. Cela vous permettra de mieux comprendre comment commencer à utiliser Big O pour classer d'autres algorithmes.
L'algorithme de tri des bulles en Python
Tri des bulles est l'un des algorithmes de tri les plus simples. Son nom vient de la façon dont l'algorithme fonctionne: à chaque nouvelle passe, le plus grand élément de la liste «bouillonne» vers sa position correcte.
Le tri à bulles consiste à effectuer plusieurs passages dans une liste, à comparer les éléments un par un et à permuter les éléments adjacents qui ne fonctionnent pas.
Implémentation de Bubble Sort en Python
Voici une implémentation d'un algorithme de tri à bulles en Python:
1 def bubble_sort(tableau):
2 n = len(tableau)
3
4 pour je dans gamme(n):
5 # Commencez à regarder chaque élément de la liste un par un,
6 # le comparer avec sa valeur adjacente. Avec chaque
7 # itération, la partie du tableau que vous regardez
8 # rétrécit car les éléments restants ont déjà été
9 # trié.
dix pour j dans gamme(n - je - 1):
11 # Créez un drapeau qui permettra à la fonction de
12 # terminer tôt s'il n'y a plus rien à trier
13 déjà_ trié = Vrai
14
15 si tableau[[[[j] > tableau[[[[j + 1]:
16 # Si l'élément que vous regardez est supérieur à son
17 # valeur adjacente, puis échangez-les
18 tableau[[[[j], tableau[[[[j + 1] = tableau[[[[j + 1], tableau[[[[j]
19
20 # Puisque vous avez dû échanger deux éléments,
21 # définissez le drapeau `already_sorted` sur` False` pour que le
22 # l'algorithme ne se termine pas prématurément
23 déjà_ trié = Faux
24
25 # S'il n'y a pas eu de swaps lors de la dernière itération,
26 # le tableau est déjà trié et vous pouvez terminer
27 si déjà_ trié:
28 Pause
29
30 revenir tableau
Étant donné que cette implémentation trie le tableau par ordre croissant, chaque étape «bouillonne» le plus grand élément à la fin du tableau. Cela signifie que chaque itération prend moins d'étapes que l'itération précédente car une partie continuellement plus grande du tableau est triée.
Les boucles dans lignes 4 et 10 déterminer la façon dont l'algorithme parcourt la liste. Remarquez comment j
passe initialement du premier élément de la liste à l'élément immédiatement avant le dernier. Lors de la deuxième itération, j
s'exécute jusqu'à deux éléments du dernier, puis trois éléments du dernier, etc. À la fin de chaque itération, la partie finale de la liste sera triée.
À mesure que les boucles progressent, ligne 15 compare chaque élément avec sa valeur adjacente, et ligne 18 les échange s'ils sont dans le mauvais ordre. Cela garantit une liste triée à la fin de la fonction.
Remarque: le déjà_ trié
drapeau dans lignes 13, 23 et 27 du code ci-dessus est une optimisation de l'algorithme, et il n'est pas nécessaire dans une implémentation de tri à bulles entièrement fonctionnelle. Cependant, il permet à la fonction d'enregistrer les étapes inutiles si la liste finit entièrement triée avant la fin des boucles.
En tant qu'exercice, vous pouvez supprimer l'utilisation de cet indicateur et comparer les temps d'exécution des deux implémentations.
Pour analyser correctement le fonctionnement de l'algorithme, considérez une liste avec des valeurs [8, 2, 6, 4, 5]
. Supposons que vous utilisez bubble_sort ()
d'en haut. Voici une figure illustrant à quoi ressemble le tableau à chaque itération de l'algorithme:
Jetez maintenant un œil étape par étape à ce qui se passe avec le tableau à mesure que l'algorithme progresse:
-
Le code commence par comparer le premier élément,
8
, avec son élément adjacent,2
. Depuis8> 2
, les valeurs sont permutées, résultant dans l'ordre suivant:[2, 8, 6, 4, 5]
. -
L'algorithme compare ensuite le deuxième élément,
8
, avec son élément adjacent,6
. Depuis8> 6
, les valeurs sont permutées, résultant dans l'ordre suivant:[2, 6, 8, 4, 5]
. -
Ensuite, l'algorithme compare le troisième élément,
8
, avec son élément adjacent,4
. Depuis8> 4
, il permute également les valeurs, résultant dans l'ordre suivant:[2, 6, 4, 8, 5]
. -
Enfin, l'algorithme compare le quatrième élément,
8
, avec son élément adjacent,5
, et les échange également, résultant en[2, 6, 4, 5, 8]
. À ce stade, l'algorithme a terminé le premier passage dans la liste (i = 0
). Remarquez comment la valeur8
bouillonné de son emplacement initial à sa position correcte à la fin de la liste. -
La deuxième passe (
i = 1
) tient compte du fait que le dernier élément de la liste est déjà positionné et se concentre sur les quatre autres éléments,[2, 6, 4, 5]
. À la fin de ce passage, la valeur6
trouve sa position correcte. Le troisième passage dans la liste positionne la valeur5
, et ainsi de suite jusqu'à ce que la liste soit triée.
Mesurer la complexité du Big O Runtime de Bubble Sort
Votre implémentation du tri à bulles se compose de deux pour
boucles dans lesquelles l'algorithme fonctionne n – 1 comparaisons, puis n – 2 comparaisons, et ainsi de suite jusqu'à ce que la comparaison finale soit effectuée. Cela vient à un total de (n – 1) + (n – 2) + (n – 3) +… + 2 + 1 = n (n-1) / 2 des comparaisons, qui peuvent aussi s'écrire ½n2 – ½n.
Vous avez appris plus tôt que Big O se concentre sur la croissance du runtime par rapport à la taille de l'entrée. Cela signifie que, pour transformer l'équation ci-dessus en complexité Big O de l'algorithme, vous devez supprimer les constantes car elles ne changent pas avec la taille d'entrée.
Cela simplifie la notation pour n2 – n. Depuis n2 croît beaucoup plus vite que n, ce dernier terme peut également être abandonné, laissant le type de bulle avec une complexité moyenne et pire Sur2).
Dans les cas où l'algorithme reçoit un tableau déjà trié – et en supposant que l'implémentation inclut le déjà_ trié
l'optimisation des drapeaux expliquée précédemment – la complexité de l'exécution se résumera à une bien meilleure Sur) car l'algorithme n'aura pas besoin de visiter un élément plus d'une fois.
Sur)est donc la meilleure complexité d'exécution du tri à bulles. Mais gardez à l'esprit que les meilleurs cas sont une exception, et vous devez vous concentrer sur le cas moyen lorsque vous comparez différents algorithmes.
Timing Your Bubble Sort Implementation
En utilisant votre run_sorting_algorithm ()
plus tôt dans ce didacticiel, voici le temps que prend le tri à bulles pour traiter un tableau avec dix mille éléments. Ligne 8 remplace le nom de l'algorithme et tout le reste reste le même:
1 si __Nom__ == "__principale__":
2 # Générer un tableau d'éléments `ARRAY_LENGTH` consistant
3 Nombre de valeurs entières aléatoires entre 0 et 999
4 tableau = [[[[randint(0, 1000) pour je dans gamme(ARRAY_LENGTH)]
5
6 # Appeler la fonction en utilisant le nom de l'algorithme de tri
7 # et le tableau que vous venez de créer
8 run_sorting_algorithm(algorithme="bubble_sort", tableau=tableau)
Vous pouvez maintenant exécuter le script pour obtenir le temps d'exécution de bubble_sort
:
$ python sorting.py
Algorithme: bubble_sort. Temps d'exécution minimum: 73,21720498399998
Ça a pris 73
secondes pour trier le tableau avec dix mille éléments. Cela représente l'exécution la plus rapide des dix répétitions run_sorting_algorithm ()
s'exécute. L'exécution de ce script plusieurs fois produira des résultats similaires.
Remarque: Une seule exécution de type bulle a 73
secondes, mais l'algorithme a fonctionné dix fois en utilisant timeit.repeat ()
. Cela signifie que vous devez vous attendre à ce que votre code 73 * 10 = 730
secondes à exécuter, en supposant que vous avez des caractéristiques matérielles similaires. Les machines plus lentes peuvent prendre beaucoup plus de temps à terminer.
Analyse des forces et des faiblesses du tri à bulles
Le principal avantage de l'algorithme de tri à bulles est son simplicité. Il est simple à mettre en œuvre et à comprendre. C'est probablement la principale raison pour laquelle la plupart des cours d'informatique introduisent le thème du tri à l'aide du tri à bulles.
Comme vous l'avez vu précédemment, l'inconvénient du tri à bulles est qu'il est lent, avec une complexité d'exécution de Sur2). Malheureusement, cela l'exclut comme un candidat pratique pour trier les grands tableaux.
L'algorithme de tri par insertion en Python
Comme le tri à bulles, le tri par insertion algorithme est simple à mettre en œuvre et à comprendre. Mais contrairement au tri à bulles, il construit la liste triée un élément à la fois en comparant chaque élément avec le reste de la liste et en l'insérant dans sa position correcte. Cette procédure «d'insertion» donne son nom à l'algorithme.
Une excellente analogie pour expliquer le tri par insertion est la façon dont vous trieriez un jeu de cartes. Imaginez que vous tenez un groupe de cartes entre vos mains et que vous souhaitez les organiser dans l’ordre. Vous commencerez par comparer une seule carte étape par étape avec le reste des cartes jusqu'à ce que vous trouviez sa position correcte. À ce stade, vous devez insérer la carte au bon endroit et recommencer avec une nouvelle carte, en répétant jusqu'à ce que toutes les cartes dans votre main soient triées.
Implémentation du tri par insertion en Python
L'algorithme de tri par insertion fonctionne exactement comme l'exemple avec le jeu de cartes. Voici l'implémentation en Python:
1 def tri par insertion(tableau):
2 # Boucle du deuxième élément du tableau jusqu'à
3 # le dernier élément
4 pour je dans gamme(1, len(tableau)):
5 # C'est l'élément que nous voulons positionner dans son
6 # endroit approprié
7 key_item = tableau[[[[je]
8
9 # Initialise la variable qui sera utilisée pour
dix # trouver la position correcte de l'élément référencé
11 # par `key_item`
12 j = je - 1
13
14 # Parcourez la liste des éléments (la gauche
15 # portion du tableau) et trouver la position correcte
16 # de l'élément référencé par `key_item`. Faites cela seulement
17 # si `key_item` est plus petit que ses valeurs adjacentes.
18 tandis que j > = 0 et tableau[[[[j] > key_item:
19 # Décaler la valeur d'une position vers la gauche
20 # et repositionnez j pour pointer vers l'élément suivant
21 # (de droite à gauche)
22 tableau[[[[j + 1] = tableau[[[[j]
23 j - = 1
24
25 # Lorsque vous avez terminé de déplacer les éléments, vous pouvez positionner
26 # `key_item` à son emplacement correct
27 tableau[[[[j + 1] = key_item
28
29 revenir tableau
Contrairement au tri à bulles, cette implémentation du tri par insertion construit la liste triée en poussant les petits éléments vers la gauche. Décomposons tri par insertion()
ligne par ligne:
-
Ligne 4 met en place la boucle qui détermine la
key_item
que la fonction positionnera à chaque itération. Notez que la boucle commence avec le deuxième élément de la liste et va jusqu'au dernier élément. -
Ligne 7 initialise
key_item
avec l'élément que la fonction tente de placer. -
Ligne 12 initialise une variable qui pointera consécutivement vers chaque élément à gauche de
élément clé
. Ce sont les éléments qui seront consécutivement comparés auxkey_item
. -
Ligne 18 compare
key_item
avec chaque valeur à sa gauche en utilisant untandis que
boucle, en déplaçant les éléments pour faire de la place pour placerkey_item
. -
Ligne 27 postes
key_item
à sa place correcte après que l'algorithme décale toutes les plus grandes valeurs vers la droite.
Voici une figure illustrant les différentes itérations de l'algorithme lors du tri du tableau [8, 2, 6, 4, 5]
:
Voici maintenant un résumé des étapes de l'algorithme lors du tri du tableau:
-
L'algorithme commence par
key_item = 2
et passe par le sous-tableau à sa gauche pour trouver la bonne position pour lui. Dans ce cas, le sous-tableau est[8]
. -
Depuis
2 <8
, l'algorithme décale l'élément8
une position à sa droite. Le tableau résultant à ce stade est[8, 8, 6, 4, 5]
. -
Puisqu'il n'y a plus d'éléments dans le sous-tableau, le
key_item
est maintenant placé dans sa nouvelle position, et le tableau final est[2, 8, 6, 4, 5]
. -
La deuxième passe commence par
key_item = 6
et passe par le sous-tableau situé à sa gauche, dans ce cas[2, 8]
. -
Depuis
6 <8
, l'algorithme décale 8 vers la droite. Le tableau résultant à ce stade est[2, 8, 8, 4, 5]
. -
Depuis
6> 2
, l'algorithme n'a pas besoin de continuer à parcourir le sous-tableau, il positionnekey_item
et termine la deuxième passe. A ce moment, le tableau résultant est[2, 6, 8, 4, 5]
. -
Le troisième passage dans la liste met l'élément
4
dans sa position correcte, et la quatrième passe place l'élément5
au bon endroit, en laissant le tableau trié.
Mesurer la complexité de Big O Runtime du tri par insertion
Semblable à votre implémentation de tri à bulles, l'algorithme de tri par insertion possède quelques boucles imbriquées qui parcourent la liste. La boucle interne est assez efficace car elle ne parcourt la liste que jusqu'à ce qu'elle trouve la position correcte d'un élément. Cela dit, l'algorithme a toujours un Sur2) complexité d'exécution sur le cas moyen.
Le pire des cas se produit lorsque le tableau fourni est trié dans l'ordre inverse. Dans ce cas, la boucle interne doit exécuter chaque comparaison pour mettre chaque élément dans sa position correcte. Cela vous donne toujours une Sur2) complexité d'exécution.
Le meilleur cas se produit lorsque le tableau fourni est déjà trié. Ici, la boucle interne n'est jamais exécutée, ce qui entraîne un Sur) complexité d'exécution, tout comme le meilleur cas de tri à bulles.
Bien que le tri par bulles et le tri par insertion aient la même complexité d'exécution Big O, en pratique, le tri par insertion est considérablement plus efficace que le tri par bulles. Si vous regardez l'implémentation des deux algorithmes, vous pouvez voir comment le tri par insertion doit faire moins de comparaisons pour trier la liste.
Planification de la mise en œuvre du tri par insertion
Pour prouver que le tri par insertion est plus efficace que le tri par bulles, vous pouvez chronométrer l'algorithme de tri par insertion et le comparer avec les résultats du tri par bulles. Pour ce faire, il vous suffit de remplacer l'appel à run_sorting_algorithm ()
avec le nom de votre implémentation de tri par insertion:
1 si __Nom__ == "__principale__":
2 # Générer un tableau d'éléments `ARRAY_LENGTH` consistant
3 Nombre de valeurs entières aléatoires entre 0 et 999
4 tableau = [[[[randint(0, 1000) pour je dans gamme(ARRAY_LENGTH)]
5
6 # Appeler la fonction en utilisant le nom de l'algorithme de tri
7 # et le tableau que nous venons de créer
8 run_sorting_algorithm(algorithme="tri par insertion", tableau=tableau)
Vous pouvez exécuter le script comme précédemment:
$ python sorting.py
Algorithme: insertion_sort. Temps d'exécution minimum: 56,71029764299999
Remarquez comment l'implémentation du tri par insertion s'est déroulée 17
moins de secondes que l'implémentation du tri à bulles pour trier le même tableau. Même s'ils sont tous les deux Sur2) algorithmes, le tri par insertion est plus efficace.
Analyse des forces et des faiblesses du tri par insertion
Tout comme le tri à bulles, l'algorithme de tri par insertion est très simple à implémenter. Même si le tri par insertion est un Sur2) algorithme, il est également beaucoup plus efficace dans la pratique que d'autres implémentations quadratiques telles que le tri à bulles.
Il existe des algorithmes plus puissants, notamment le tri par fusion et le tri rapide, mais ces implémentations sont récursives et ne parviennent généralement pas à battre le tri par insertion lorsque vous travaillez sur de petites listes. Certaines implémentations de tri rapide utilisent même le tri par insertion en interne si la liste est suffisamment petite pour fournir une implémentation globale plus rapide. Timsort utilise également le tri par insertion en interne pour trier de petites parties du tableau d'entrée.
Cela dit, le tri par insertion n'est pas pratique pour les grands tableaux, ouvrant la porte à des algorithmes pouvant évoluer de manière plus efficace.
L'algorithme de tri par fusion en Python
Tri par fusion est un algorithme de tri très efficace. Il est basé sur l'approche diviser pour mieux régner, une puissante technique algorithmique utilisée pour résoudre des problèmes complexes.
Pour bien comprendre la division et la conquête, vous devez d'abord comprendre le concept de récursivité. La récursivité consiste à décomposer un problème en sous-problèmes plus petits jusqu'à ce qu'ils soient suffisamment petits pour être gérés. En programmation, la récursivité est généralement exprimée par une fonction qui s'appelle elle-même.
Remarque: Ce didacticiel n'explore pas la récursivité en profondeur. Pour mieux comprendre le fonctionnement de la récursivité et la voir en action à l'aide de Python, consultez Penser récursivement en Python.
Les algorithmes de division et de conquête suivent généralement la même structure:
- L'entrée d'origine est divisée en plusieurs parties, chacune représentant un sous-problème similaire à l'original mais plus simple.
- Chaque sous-problème est résolu récursivement.
- Les solutions à tous les sous-problèmes sont combinées en une seule solution globale.
Dans le cas du tri par fusion, l'approche diviser pour régner divise l'ensemble des valeurs d'entrée en deux parties de taille égale, trie chaque moitié de manière récursive et finalement fusionne ces deux parties triées en une seule liste triée.
Implémentation du tri par fusion en Python
L'implémentation de l'algorithme de tri par fusion nécessite deux éléments différents:
- Une fonction qui divise récursivement l'entrée en deux
- Une fonction qui fusionne les deux moitiés, produisant un tableau trié
Voici le code pour fusionner deux tableaux différents:
1 def fusionner(la gauche, droite):
2 # Si le premier tableau est vide, alors rien n'a besoin
3 # à fusionner, et vous pouvez renvoyer le deuxième tableau comme résultat
4 si len(la gauche) == 0:
5 revenir droite
6
7 # Si le deuxième tableau est vide, alors rien n'a besoin
8 # à fusionner, et vous pouvez renvoyer le premier tableau comme résultat
9 si len(droite) == 0:
dix revenir la gauche
11
12 résultat = []
13 index_left = index_right = 0
14
15 # Maintenant, parcourez les deux tableaux jusqu'à ce que tous les éléments
16 # en faire le tableau résultant
17 tandis que len(résultat) < len(la gauche) + len(droite):
18 # Les éléments doivent être triés pour les ajouter à la
19 # tableau résultant, vous devez donc décider si vous souhaitez obtenir
20 # l'élément suivant du premier ou du deuxième tableau
21 si la gauche[[[[index_left] <= droite[[[[index_right]:
22 résultat.ajouter(la gauche[[[[index_left])
23 index_left + = 1
24 autre:
25 résultat.ajouter(droite[[[[index_right])
26 index_right + = 1
27
28 # Si vous atteignez la fin de l'un ou l'autre tableau, vous pouvez
29 # ajouter les éléments restants de l'autre tableau à
30 # le résultat et rompre la boucle
31 si index_right == len(droite):
32 résultat + = la gauche[[[[index_left:]
33 Pause
34
35 si index_left == len(la gauche):
36 résultat + = droite[[[[index_right:]
37 Pause
38
39 revenir résultat
fusionner()
reçoit deux tableaux triés différents qui doivent être fusionnés. Le processus pour y parvenir est simple:
-
Lignes 4 et 9 vérifiez si l'un des tableaux est vide. Si l'un d'eux l'est, alors il n'y a rien à fusionner, donc la fonction renvoie l'autre tableau.
-
Ligne 17 commence un
tandis que
boucle qui se termine chaque fois que le résultat contient tous les éléments des deux tableaux fournis. L'objectif est d'examiner les deux tableaux et de combiner leurs éléments pour produire une liste triée. -
Ligne 21 compare les éléments en tête des deux tableaux, sélectionne la valeur la plus petite et l'ajoute à la fin du tableau résultant.
-
Lignes 31 et 35 ajoutez tous les éléments restants au résultat si tous les éléments de l'un ou l'autre des tableaux ont déjà été utilisés.
Avec la fonction ci-dessus en place, la seule pièce manquante est une fonction qui divise récursivement le tableau d'entrée en deux et utilise fusionner()
pour produire le résultat final:
41 def tri par fusion(tableau):
42 # Si le tableau d'entrée contient moins de deux éléments,
43 # puis le retourner comme résultat de la fonction
44 si len(tableau) < 2:
45 revenir tableau
46
47 point médian = len(tableau) // 2
48
49 # Trier le tableau en séparant récursivement l'entrée
50 # en deux moitiés égales, en triant chaque moitié et en les fusionnant
51 # ensemble dans le résultat final
52 revenir fusionner(
53 la gauche=tri par fusion(tableau[:[:[:[:point médian]),
54 droite=tri par fusion(tableau[[[[point médian:]))
Voici un bref résumé du code:
-
Ligne 44 agit comme condition d'arrêt pour la récursivité. Si le tableau d'entrée contient moins de deux éléments, la fonction renvoie le tableau. Notez que cette condition peut être déclenchée en recevant un seul élément ou un tableau vide. Dans les deux cas, il ne reste plus rien à trier, donc la fonction devrait revenir.
-
Ligne 47 calcule le point central du tableau.
-
Ligne 52 appels
fusionner()
, passant les deux moitiés triées comme les tableaux.
Remarquez comment cette fonction s'appelle récursivement, réduisant de moitié le tableau à chaque fois. Chaque itération traite d'un tableau toujours plus petit jusqu'à ce qu'il reste moins de deux éléments, ce qui signifie qu'il ne reste plus rien à trier. À ce point, fusionner()
prend le relais, fusionnant les deux moitiés et produisant une liste triée.
Jetez un œil à une représentation des étapes que le tri par fusion prendra pour trier le tableau [8, 2, 6, 4, 5]
:
La figure utilise des flèches jaunes pour représenter la réduction de moitié du tableau à chaque niveau de récursivité. Les flèches vertes représentent la fusion de chaque sous-ensemble. Les étapes peuvent être résumées comme suit:
-
Le premier appel à
tri par fusion()
avec[8, 2, 6, 4, 5]
définitpoint médian
comme2
. lepoint médian
est utilisé pour réduire de moitié le tableau d'entrée entableau[:2]
ettableau[2:]
, produisant[8, 2]
et[6, 4, 5]
, respectivement.tri par fusion()
est ensuite récursivement appelé pour chaque moitié afin de les trier séparément. -
L'appel à
tri par fusion()
avec[8, 2]
produit[8]
et[2]
. Le processus se répète pour chacune de ces moitiés. -
L'appel à
tri par fusion()
avec[8]
Retour[8]
puisque c'est le seul élément. La même chose se produit avec l'appel àtri par fusion()
avec[2]
. -
À ce stade, la fonction commence à fusionner les sous-réseaux en utilisant
fusionner()
, commençant par[8]
et[2]
comme tableaux d'entrée, produisant[2, 8]
comme résultat. -
D'un autre côté,
[6, 4, 5]
est récursivement décomposé et fusionné en utilisant la même procédure, produisant[4, 5, 6]
comme résultat. -
Dans la dernière étape,
[2, 8]
et[4, 5, 6]
sont fusionnés avecfusionner()
, produisant le résultat final:[2, 4, 5, 6, 8]
.
Mesurer la complexité Big O du tri par fusion
Pour analyser la complexité du tri par fusion, vous pouvez examiner ses deux étapes séparément:
-
fusionner()
a un temps d'exécution linéaire. Il reçoit deux tableaux dont la longueur combinée est au plus n (la longueur du tableau d'entrée d'origine), et il combine les deux tableaux en regardant chaque élément au plus une fois. Cela conduit à une complexité d'exécution de Sur). -
La deuxième étape divise le tableau d'entrée récursivement et appelle
fusionner()
pour chaque moitié. Étant donné que le tableau est divisé par deux jusqu'à ce qu'il ne reste qu'un seul élément, le nombre total d'opérations de division par deux effectuées par cette fonction est Journal2n. Depuisfusionner()
est appelé pour chaque moitié, nous obtenons un temps d'exécution total de O (n log2n).
De façon intéressante, O (n log2n) est le meilleur temps d'exécution le plus défavorable possible pouvant être obtenu par un algorithme de tri.
Planification de la mise en œuvre du tri de fusion
Pour comparer la vitesse de tri de fusion avec les deux implémentations précédentes, vous pouvez utiliser le même mécanisme que précédemment et remplacer le nom de l'algorithme dans ligne 8:
1 si __Nom__ == "__principale__":
2 # Générer un tableau d'éléments `ARRAY_LENGTH` consistant
3 Nombre de valeurs entières aléatoires entre 0 et 999
4 tableau = [[[[randint(0, 1000) pour je dans gamme(ARRAY_LENGTH)]
5
6 # Appeler la fonction en utilisant le nom de l'algorithme de tri
7 # et le tableau que vous venez de créer
8 run_sorting_algorithm(algorithme="tri par fusion", tableau=tableau)
Vous pouvez exécuter le script pour obtenir le temps d'exécution de tri par fusion
:
$ python sorting.py
Algorithme: merge_sort. Temps d'exécution minimum: 0,6195857160000173
Par rapport au tri à bulles et au tri par insertion, l'implémentation du tri par fusion est extrêmement rapide, triant le tableau de dix mille éléments en moins d'une seconde!
Analyse des forces et des faiblesses du tri par fusion
Grâce à sa complexité d'exécution de O (n log2n), le tri par fusion est un algorithme très efficace qui évolue bien à mesure que la taille du tableau d'entrée augmente. Il est également simple de paralléliser car il divise le tableau d'entrée en morceaux qui peuvent être distribués et traités en parallèle si nécessaire.
Cela dit, pour les petites listes, le coût en temps de la récursivité permet aux algorithmes tels que le tri à bulles et le tri par insertion d'être plus rapides. Par exemple, l'exécution d'une expérience avec une liste de dix éléments entraîne les délais suivants:
Algorithme: bubble_sort. Temps d'exécution minimum: 0,000018774999999998654
Algorithme: insertion_sort. Temps d'exécution minimum: 0,000029786000000000395
Algorithme: merge_sort. Temps d'exécution minimum: 0,00016983000000000276
Le tri par bulles et le tri par insertion battent le tri par fusion lors du tri d'une liste à dix éléments.
Un autre inconvénient du tri par fusion est qu'il crée des copies du tableau lors de son appel récursif. Il crée également une nouvelle liste à l'intérieur fusionner()
pour trier et renvoyer les deux moitiés d'entrée. Cela fait que le tri par fusion utilise beaucoup plus de mémoire que le tri par bulles et le tri par insertion, qui sont tous deux capables de trier la liste en place.
En raison de cette limitation, vous ne souhaiterez peut-être pas utiliser le tri par fusion pour trier les grandes listes dans le matériel contraint en mémoire.
L'algorithme Quicksort en Python
Tout comme le tri par fusion, le tri rapide L'algorithme applique le principe de division et de conquête pour diviser le tableau d'entrée en deux listes, la première avec de petits éléments et la seconde avec de grands éléments. The algorithm then sorts both lists recursively until the resultant list is completely sorted.
Dividing the input list is referred to as partitioning the list. Quicksort first selects a pivot
element and partitions the list around the pivot
, putting every smaller element into a faible
array and every larger element into a haute
array.
Putting every element from the faible
list to the left of the pivot
and every element from the haute
list to the right positions the pivot
precisely where it needs to be in the final sorted list. This means that the function can now recursively apply the same procedure to faible
and then haute
until the entire list is sorted.
Implementing Quicksort in Python
Here’s a fairly compact implementation of quicksort:
1 de Aléatoire importation randint
2
3 def quicksort(tableau):
4 # If the input array contains fewer than two elements,
5 # then return it as the result of the function
6 si len(tableau) < 2:
7 revenir tableau
8
9 faible, même, haute = [], [], []
dix
11 # Select your `pivot` element randomly
12 pivot = tableau[[[[randint(0, len(tableau) - 1)]
13
14 pour article dans tableau:
15 # Elements that are smaller than the `pivot` go to
16 # the `low` list. Elements that are larger than
17 # `pivot` go to the `high` list. Elements that are
18 # equal to `pivot` go to the `same` list.
19 si article < pivot:
20 faible.append(article)
21 elif article == pivot:
22 même.append(article)
23 elif article > pivot:
24 haute.append(article)
25
26 # The final result combines the sorted `low` list
27 # with the `same` list and the sorted `high` list
28 revenir quicksort(faible) + même + quicksort(haute)
Here’s a summary of the code:
-
Line 6 stops the recursive function if the array contains fewer than two elements.
-
Line 12 selects the
pivot
element randomly from the list and proceeds to partition the list. -
Lines 19 and 20 put every element that’s smaller than
pivot
into the list calledfaible
. -
Lines 21 and 22 put every element that’s equal to
pivot
into the list calledmême
. -
Lines 23 and 24 put every element that’s larger than
pivot
into the list calledhaute
. -
Line 28 recursively sorts the
faible
ethaute
lists and combines them along with the contents of themême
liste.
Here’s an illustration of the steps that quicksort takes to sort the array [8, 2, 6, 4, 5]
:
The yellow lines represent the partitioning of the array into three lists: faible
, même
, et haute
. The green lines represent sorting and putting these lists back together. Here’s a brief explanation of the steps:
-
le
pivot
element is selected randomly. Dans ce cas,pivot
est6
. -
The first pass partitions the input array so that
faible
contains[2, 4, 5]
,même
contains[6]
, ethaute
contains[8]
. -
quicksort()
is then called recursively withfaible
as its input. This selects a randompivot
and breaks the array into[2]
commefaible
,[4]
commemême
, et[5]
commehaute
. -
The process continues, but at this point, both
faible
ethaute
have fewer than two items each. This ends the recursion, and the function puts the array back together. Adding the sortedfaible
ethaute
to either side of themême
list produces[2, 4, 5]
. -
On the other side, the
haute
list containing[8]
has fewer than two elements, so the algorithm returns the sortedfaible
array, which is now[2, 4, 5]
. Merging it withmême
([6]
) ethaute
([8]
) produces the final sorted list.
Selecting the pivot
Élément
Why does the implementation above select the pivot
element randomly? Wouldn’t it be the same to consistently select the first or last element of the input list?
Because of how the quicksort algorithm works, the number of recursion levels depends on where pivot
ends up in each partition. In the best-case scenario, the algorithm consistently picks the median element as the pivot
. That would make each generated subproblem exactly half the size of the previous problem, leading to at most Journal2n niveaux.
On the other hand, if the algorithm consistently picks either the smallest or largest element of the array as the pivot
, then the generated partitions will be as unequal as possible, leading to n-1 recursion levels. That would be the worst-case scenario for quicksort.
As you can see, quicksort’s efficiency often depends on the pivot
selection. If the input array is unsorted, then using the first or last element as the pivot
will work the same as a random element. But if the input array is sorted or almost sorted, using the first or last element as the pivot
could lead to a worst-case scenario. Selecting the pivot
at random makes it more likely quicksort will select a value closer to the median and finish faster.
Another option for selecting the pivot
is to find the median value of the array and force the algorithm to use it as the pivot
. This can be done in O(n) time. Although the process is little bit more involved, using the median value as the pivot
for quicksort guarantees you will have the best-case Big O scenario.
Measuring Quicksort’s Big O Complexity
With quicksort, the input list is partitioned in linear time, O(n), and this process repeats recursively an average of Journal2n times. This leads to a final complexity of O(n log2n).
That said, remember the discussion about how the selection of the pivot
affects the runtime of the algorithm. le O(n) best-case scenario happens when the selected pivot
is close to the median of the array, and an O(n2) scenario happens when the pivot
is the smallest or largest value of the array.
Theoretically, if the algorithm focuses first on finding the median value and then uses it as the pivot
element, then the worst-case complexity will come down to O(n log2n). The median of an array can be found in linear time, and using it as the pivot
guarantees the quicksort portion of the code will perform in O(n log2n).
By using the median value as the pivot
, you end up with a final runtime of O(n) + O(n log2n). You can simplify this down to O(n log2n) because the logarithmic portion grows much faster than the linear portion.
Remarque: Although achieving O(n log2n) is possible in quicksort’s worst-case scenario, this approach is seldom used in practice. Lists have to be quite large for the implementation to be faster than a simple randomized selection of the pivot
.
Randomly selecting the pivot
makes the worst case very unlikely. That makes random pivot
selection good enough for most implementations of the algorithm.
Timing Your Quicksort Implementation
By now, you’re familiar with the process for timing the runtime of the algorithm. Just change the name of the algorithm in line 8:
1 si __Nom__ == "__main__":
2 # Generate an array of `ARRAY_LENGTH` items consisting
3 # of random integer values between 0 and 999
4 tableau = [[[[randint(0, 1000) pour je dans gamme(ARRAY_LENGTH)]
5
6 # Call the function using the name of the sorting algorithm
7 # and the array you just created
8 run_sorting_algorithm(algorithme="quicksort", tableau=tableau)
You can execute the script as you have before:
$ python sorting.py
Algorithm: quicksort. Minimum execution time: 0.11675417600002902
Not only does quicksort finish in less than one second, but it’s also much faster than merge sort (0.11
seconds versus 0.61
seconds). Increasing the number of elements specified by ARRAY_LENGTH
de 10 000
à 1,000,000
and running the script again ends up with merge sort finishing in 97
seconds, whereas quicksort sorts the list in a mere dix
seconds.
Analyzing the Strengths and Weaknesses of Quicksort
True to its name, quicksort is very fast. Although its worst-case scenario is theoretically O(n2), in practice, a good implementation of quicksort beats most other sorting implementations. Also, just like merge sort, quicksort is straightforward to parallelize.
One of quicksort’s main disadvantages is the lack of a guarantee that it will achieve the average runtime complexity. Although worst-case scenarios are rare, certain applications can’t afford to risk poor performance, so they opt for algorithms that stay within O(n log2n) regardless of the input.
Just like merge sort, quicksort also trades off memory space for speed. This may become a limitation for sorting larger lists.
A quick experiment sorting a list of ten elements leads to the following results:
Algorithm: bubble_sort. Minimum execution time: 0.0000909000000000014
Algorithm: insertion_sort. Minimum execution time: 0.00006681900000000268
Algorithm: quicksort. Minimum execution time: 0.0001319930000000004
The results show that quicksort also pays the price of recursion when the list is sufficiently small, taking longer to complete than both insertion sort and bubble sort.
The Timsort Algorithm in Python
le Timsort algorithm is considered a hybrid sorting algorithm because it employs a best-of-both-worlds combination of insertion sort and merge sort. Timsort is near and dear to the Python community because it was created by Tim Peters in 2002 to be used as the standard sorting algorithm of the Python language.
The main characteristic of Timsort is that it takes advantage of already-sorted elements that exist in most real-world datasets. These are called natural runs. The algorithm then iterates over the list, collecting the elements into runs and merging them into a single sorted list.
Implementing Timsort in Python
In this section, you’ll create a barebones Python implementation that illustrates all the pieces of the Timsort algorithm. If you’re interested, you can also check out the original C implementation of Timsort.
The first step in implementing Timsort is modifying the implementation of insertion_sort()
from before:
1 def insertion_sort(tableau, la gauche=0, droite=Aucun):
2 si droite est Aucun:
3 droite = len(tableau) - 1
4
5 # Loop from the element indicated by
6 # `left` until the element indicated by `right`
7 pour je dans gamme(la gauche + 1, droite + 1):
8 # This is the element we want to position in its
9 # correct place
dix key_item = tableau[[[[je]
11
12 # Initialize the variable that will be used to
13 # find the correct position of the element referenced
14 # by `key_item`
15 j = je - 1
16
17 # Run through the list of items (the left
18 # portion of the array) and find the correct position
19 # of the element referenced by `key_item`. Do this only
20 # if the `key_item` is smaller than its adjacent values.
21 tandis que j >= la gauche et tableau[[[[j] > key_item:
22 # Shift the value one position to the left
23 # and reposition `j` to point to the next element
24 # (from right to left)
25 tableau[[[[j + 1] = tableau[[[[j]
26 j -= 1
27
28 # When you finish shifting the elements, position
29 # the `key_item` in its correct location
30 tableau[[[[j + 1] = key_item
31
32 revenir tableau
This modified implementation adds a couple of parameters, la gauche
et droite
, that indicate which portion of the array should be sorted. This allows the Timsort algorithm to sort a portion of the array in place. Modifying the function instead of creating a new one means that it can be reused for both insertion sort and Timsort.
Now take a look at the implementation of Timsort:
1 def timsort(tableau):
2 min_run = 32
3 n = len(tableau)
4
5 # Start by slicing and sorting small portions of the
6 # input array. The size of these slices is defined by
7 # your `min_run` size.
8 pour je dans gamme(0, n, min_run):
9 insertion_sort(tableau, je, min((je + min_run - 1), n - 1))
dix
11 # Now you can start merging the sorted slices.
12 # Start from `min_run`, doubling the size on
13 # each iteration until you surpass the length of
14 # the array.
15 Taille = min_run
16 tandis que Taille < n:
17 # Determine the arrays that will
18 # be merged together
19 pour début dans gamme(0, n, Taille * 2):
20 # Compute the `midpoint` (where the first array ends
21 # and the second starts) and the `endpoint` (where
22 # the second array ends)
23 midpoint = début + Taille - 1
24 fin = min((début + Taille * 2 - 1), (n-1))
25
26 # Merge the two subarrays.
27 # The `left` array should go from `start` to
28 # `midpoint + 1`, while the `right` array should
29 # go from `midpoint + 1` to `end + 1`.
30 merged_array = fusionner(
31 la gauche=tableau[[[[début:midpoint + 1],
32 droite=tableau[[[[midpoint + 1:fin + 1])
33
34 # Finally, put the merged array back into
35 # your array
36 tableau[[[[début:début + len(merged_array)] = merged_array
37
38 # Each iteration should double the size of your arrays
39 Taille *= 2
40
41 revenir tableau
Although the implementation is a bit more complex than the previous algorithms, we can summarize it quickly in the following way:
-
Lines 8 and 9 create small slices, or runs, of the array and sort them using insertion sort. You learned previously that insertion sort is speedy on small lists, and Timsort takes advantage of this. Timsort uses the newly introduced
la gauche
etdroite
parameters ininsertion_sort()
to sort the list in place without having to create new arrays like merge sort and quicksort do. -
Line 16 merges these smaller runs, with each run being of size
32
initially. With each iteration, the size of the runs is doubled, and the algorithm continues merging these larger runs until a single sorted run remains.
Notice how, unlike merge sort, Timsort merges subarrays that were previously sorted. Doing so decreases the total number of comparisons required to produce a sorted list. This advantage over merge sort will become apparent when running experiments using different arrays.
Finalement, line 2 définit min_run = 32
. There are two reasons for using 32
as the value here:
-
Sorting small arrays using insertion sort is very fast, and
min_run
has a small value to take advantage of this characteristic. Initializingmin_run
with a value that’s too large will defeat the purpose of using insertion sort and will make the algorithm slower. -
Merging two balanced lists is much more efficient than merging lists of disproportionate size. Picking a
min_run
value that’s a power of two ensures better performance when merging all the different runs that the algorithm creates.
Combining both conditions above offers several options for min_run
. The implementation in this tutorial uses min_run = 32
as one of the possibilities.
Remarque: In practice, Timsort does something a little more complicated to compute min_run
. It picks a value between 32 and 64 inclusive, such that the length of the list divided by min_run
is exactly a power of 2. If that’s not possible, it chooses a value that’s close to, but strictly less than, a power of 2.
If you’re curious, you can read the complete analysis on how to pick min_run
under the Computing minrun section.
Measuring Timsort’s Big O Complexity
On average, the complexity of Timsort is O(n log2n), just like merge sort and quicksort. The logarithmic part comes from doubling the size of the run to perform each linear merge operation.
However, Timsort performs exceptionally well on already-sorted or close-to-sorted lists, leading to a best-case scenario of O(n). In this case, Timsort clearly beats merge sort and matches the best-case scenario for quicksort. But the worst case for Timsort is also O(n log2n), which surpasses quicksort’s O(n2).
Timing Your Timsort Implementation
Vous pouvez utiliser run_sorting_algorithm()
to see how Timsort performs sorting the ten-thousand-element array:
1 si __Nom__ == "__main__":
2 # Generate an array of `ARRAY_LENGTH` items consisting
3 # of random integer values between 0 and 999
4 tableau = [[[[randint(0, 1000) pour je dans gamme(ARRAY_LENGTH)]
5
6 # Call the function using the name of the sorting algorithm
7 # and the array you just created
8 run_sorting_algorithm(algorithme="timsort", tableau=tableau)
Now execute the script to get the execution time of timsort
:
$ python sorting.py
Algorithm: timsort. Minimum execution time: 0.5121690789999998
À 0.51
seconds, this Timsort implementation is a full 0,1
seconds, or 17 percent, faster than merge sort, though it doesn’t match the 0.11
of quicksort. It’s also a ridiculous 11,000 percent faster than insertion sort!
Now try to sort an already-sorted list using these four algorithms and see what happens. You can modify your __main__
section as follows:
1 si __Nom__ == "__main__":
2 # Generate a sorted array of ARRAY_LENGTH items
3 tableau = [[[[je pour je dans gamme(ARRAY_LENGTH)]
4
5 # Call each of the functions
6 run_sorting_algorithm(algorithme="insertion_sort", tableau=tableau)
7 run_sorting_algorithm(algorithme="merge_sort", tableau=tableau)
8 run_sorting_algorithm(algorithme="quicksort", tableau=tableau)
9 run_sorting_algorithm(algorithme="timsort", tableau=tableau)
If you execute the script now, then all the algorithms will run and output their corresponding execution time:
Algorithm: insertion_sort. Minimum execution time: 53.5485634999991
Algorithm: merge_sort. Minimum execution time: 0.372304601
Algorithm: quicksort. Minimum execution time: 0.24626494199999982
Algorithm: timsort. Minimum execution time: 0.23350277099999994
This time, Timsort comes in at a whopping thirty-seven percent faster than merge sort and five percent faster than quicksort, flexing its ability to take advantage of the already-sorted runs.
Notice how Timsort benefits from two algorithms that are much slower when used by themselves. The genius of Timsort is in combining these algorithms and playing to their strengths to achieve impressive results.
Analyzing the Strengths and Weaknesses of Timsort
The main disadvantage of Timsort is its complexity. Despite implementing a very simplified version of the original algorithm, it still requires much more code because it relies on both insertion_sort()
et merge()
.
One of Timsort’s advantages is its ability to predictably perform in O(n log2n) regardless of the structure of the input array. Contrast that with quicksort, which can degrade down to O(n2). Timsort is also very fast for small arrays because the algorithm turns into a single insertion sort.
For real-world usage, in which it’s common to sort arrays that already have some preexisting order, Timsort is a great option. Its adaptability makes it an excellent choice for sorting arrays of any length.
Conclusion
Sorting is an essential tool in any Pythonista’s toolkit. With knowledge of the different sorting algorithms in Python and how to maximize their potential, you’re ready to implement faster, more efficient apps and programs!
In this tutorial, you learned:
- How Python’s built-in
sort()
works behind the scenes - Quoi Big O notation is and how to use it to compare the efficiency of different algorithms
- How to measure the actual time spent running your code
- How to implement five different sorting algorithms in Python
- What the pros and cons are of using different algorithms
You also learned about different techniques such as récursivité, divide and conquer, et randomisation. These are fundamental building blocks for solving a long list of different algorithms, and they’ll come up again and again as you keep researching.
Take the code presented in this tutorial, create new experiments, and explore these algorithms further. Better yet, try implementing other sorting algorithms in Python. The list is vast, but selection sort, heapsort, et tree sort are three excellent options to start with.
[ad_2]