Algorithmes de tri en Python – Real Python

By | avril 15, 2020

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:

  1. Pour un point de vue pratique, vous mesurerez le temps d'exécution des implémentations à l'aide du timeit module.
  2. 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.

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.

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:

Algorithme de tri des bulles
Le processus de tri des bulles

Jetez maintenant un œil étape par étape à ce qui se passe avec le tableau à mesure que l'algorithme progresse:

  1. Le code commence par comparer le premier élément, 8, avec son élément adjacent, 2. Depuis 8> 2, les valeurs sont permutées, résultant dans l'ordre suivant: [2, 8, 6, 4, 5].

  2. L'algorithme compare ensuite le deuxième élément, 8, avec son élément adjacent, 6. Depuis 8> 6, les valeurs sont permutées, résultant dans l'ordre suivant: [2, 6, 8, 4, 5].

  3. Ensuite, l'algorithme compare le troisième élément, 8, avec son élément adjacent, 4. Depuis 8> 4, il permute également les valeurs, résultant dans l'ordre suivant: [2, 6, 4, 8, 5].

  4. 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 valeur 8 bouillonné de son emplacement initial à sa position correcte à la fin de la liste.

  5. 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 valeur 6 trouve sa position correcte. Le troisième passage dans la liste positionne la valeur 5, 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.

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 aux key_item.

  • Ligne 18 compare key_item avec chaque valeur à sa gauche en utilisant un tandis que boucle, en déplaçant les éléments pour faire de la place pour placer key_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]:

Algorithme de tri par insertion
Le processus de tri par insertion

Voici maintenant un résumé des étapes de l'algorithme lors du tri du tableau:

  1. 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].

  2. Depuis 2 <8, l'algorithme décale l'élément 8 une position à sa droite. Le tableau résultant à ce stade est [8, 8, 6, 4, 5].

  3. 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].

  4. La deuxième passe commence par key_item = 6 et passe par le sous-tableau situé à sa gauche, dans ce cas [2, 8].

  5. Depuis 6 <8, l'algorithme décale 8 vers la droite. Le tableau résultant à ce stade est [2, 8, 8, 4, 5].

  6. Depuis 6> 2, l'algorithme n'a pas besoin de continuer à parcourir le sous-tableau, il positionne key_item et termine la deuxième passe. A ce moment, le tableau résultant est [2, 6, 8, 4, 5].

  7. Le troisième passage dans la liste met l'élément 4 dans sa position correcte, et la quatrième passe place l'élément 5 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.

Les algorithmes de division et de conquête suivent généralement la même structure:

  1. L'entrée d'origine est divisée en plusieurs parties, chacune représentant un sous-problème similaire à l'original mais plus simple.
  2. Chaque sous-problème est résolu récursivement.
  3. 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:

  1. Une fonction qui divise récursivement l'entrée en deux
  2. 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]:

Fusionner l'algorithme de tri
Le processus de tri par fusion

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:

  1. Le premier appel à tri par fusion() avec [8, 2, 6, 4, 5] définit point médian comme 2. le point médian est utilisé pour réduire de moitié le tableau d'entrée en tableau[:2] et tableau[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.

  2. L'appel à tri par fusion() avec [8, 2] produit [8] et [2]. Le processus se répète pour chacune de ces moitiés.

  3. 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].

  4. À 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.

  5. 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.

  6. Dans la dernière étape, [2, 8] et [4, 5, 6] sont fusionnés avec fusionner(), 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:

  1. 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).

  2. 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. Depuis fusionner() 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 called faible.

  • Lines 21 and 22 put every element that’s equal to pivot into the list called même.

  • Lines 23 and 24 put every element that’s larger than pivot into the list called haute.

  • Line 28 recursively sorts the faible et haute lists and combines them along with the contents of the même liste.

Here’s an illustration of the steps that quicksort takes to sort the array [8, 2, 6, 4, 5]:

Quick Sort Algorithm
The Quicksort Process

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:

  1. le pivot element is selected randomly. Dans ce cas, pivot est 6.

  2. The first pass partitions the input array so that faible contains [2, 4, 5], même contains [6], et haute contains [8].

  3. quicksort() is then called recursively with faible as its input. This selects a random pivot and breaks the array into [2] comme faible, [4] comme même, et [5] comme haute.

  4. The process continues, but at this point, both faible et haute have fewer than two items each. This ends the recursion, and the function puts the array back together. Adding the sorted faible et haute to either side of the même list produces [2, 4, 5].

  5. On the other side, the haute list containing [8] has fewer than two elements, so the algorithm returns the sorted faible array, which is now [2, 4, 5]. Merging it with même ([6]) et haute ([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.

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 et droite parameters in insertion_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:

  1. Sorting small arrays using insertion sort is very fast, and min_run has a small value to take advantage of this characteristic. Initializing min_run with a value that’s too large will defeat the purpose of using insertion sort and will make the algorithm slower.

  2. 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.

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.