Aller au contenu

Tri par insertion

Principe du tri par insertion

Principe du tri par insertion

En résumé :

L'opération, pour chaque position i, consiste à prendre l'éléments d'indice i (la clef) et à l'insérer à la bonne place dans le tableau des éléments d'indices 0 ài - 1. Ce processus assure que les i premiers éléments seront triés.

On pourra donc recommencer avec l'élément suivant (la clef suivante à l'indice i + 1).

Algorithme :

Pour un tableau tab de taille n

pour i allant de 1 à n-1
    clef ← tab[i]
    insérer la clef au bon endroit dans tab 

Activité 1 - Déroulé « à la main »

Considérons la situation suivante (utiliser les onglets afin de passer d'une étape à la suivante)

On débute dans la situation où les premières cartes ont déjà été triées.

Il faut désormais placer le \(2\) convenablement.

Étape 0

La première étape est de mettre cette carte de côté.

On la sort du jeu.

Étape 1

Cette étape réalisée, on va comparer sa valeur à la carte précédente. Ici le \(8\) est strictement supérieur au \(2\).

On décale le \(8\) vers la droite :

Étape 2

On peut ensuite comparer le \(2\) et le \(3\).

Là encore, le \(3\) est strictement supérieur au \(2\) : on le décale vers la droite.

Étape 3

Le \(2\) est remonté au début du tableau, le comparer à la carte précédente n'a pas de sens.

On peut insérer le \(2\) à cette position.

Étape 4


À quels éléments est-il indispensable d'appliquer l'algorithme ?
  • À tous les éléments
  • À tous les éléments sauf le premier
  • À tous les éléments sauf le dernier
  • À un élément sur deux en partant du deuxième
  • ❌ À tous les éléments
  • ✅ À tous les éléments sauf le premier
  • ❌ À tous les éléments sauf le dernier
  • ❌ À un élément sur deux en partant du deuxième

En effet, le premier élément ne peut pas être décalé car au début du tableau. Il faut par contre aborder tous les autres éléments.

Sous quelle condition peut-on décaler des éléments ?
  • On décale un élément lorsque le précédent est strictement inférieur au suivant
  • On décale un élément lorsque le précédent est strictement supérieur au suivant
  • On ne peut pas décaler l'élément s'il est en deuxième position
  • On ne peut pas décaler l'élément s'il est en première position (à gauche du tableau)
  • ❌ On décale deux éléments lorsque le précédent est strictement inférieur au suivant
  • ✅ On décale deux éléments lorsque le précédent est strictement supérieur au suivant
  • ❌ On ne peut pas décaler l'élément si l'on est en deuxième position
  • ✅ On ne peut pas décaler l'élément si l'on est en première position

La condition de décalage est double. Il faut ainsi que :

  • l'élément étudié ne soit pas au début du tableau,
  • l'élément précédent lui soit strictement supérieur.
Activité 2 - Algorithme du tri par insertion en Python

Avant d'écrire l'ensemble du tri, attardons nous sur les décalages de valeurs.

Contrairement à l'exemple du tri de cartes, il est impossible de « sortir un élément » du tableau. Il est néanmoins possible de stocker sa valeur dans une variable temporaire.

Les décalages vers la droite vont, quant à eux, alors dupliquer des valeurs ! Toutefois, chaque valeur dupliquée sera « écrasée » de proche en proche lors des décalages. En dernier lieu, la valeur à insérer viendra « écraser » la dernière valeur dupliquée.

L'illustration ci-dessous présente ce fonctionnement dans le cadre d'un tableau. La valeur à insérer est stockée sur la gauche.

Tableau aléatoire Tableau croissant Tableau décroissant

Trier votre tableau

Avant de transcrire en Python l'algorithme, gardons à l'esprit que :

  • il n'est pas indispensable d'aborder tous les éléments du tableau,
  • l'élément abordé dans une itération doit être stocké dans une variable temporaire,
  • la condition pour savoir s'il est possible de décaler des éléments est double,
  • lors de chaque décalage, on duplique un élément.

Compléter la fonction tri_insertion prenant en argument un tableau et le triant en place à l'aide du tri par insertion.

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier
Évaluations restantes : 5/5

.128013rt=7flb)1 2e/vygj:,-]8swnuph5aS(o3k_i46P;[cdm0050S0m0c0E0L0g0x0k0R0g0E0x0x0d010c0L0B010406050x0A0T0T0E0b0p040F0H0g0A0/0H0z050n0_0{0}0 0@0B04051f181i0n1f0@0S0L0o0%0)0+0-0C0L0q0C0g1w0C0c0=050Y0h0g0m1r0*0,011v1x1z1x0c1F1H1D0c0b1g0c0C0%120x0B0E0z0-0l011J1t010f0!0m0z0E0T0m1D1$1(1-1L1:1H1?1^0=0a0k0O0b0H0B0H0x0L150z0k0W1!0b0b0m0R2d181{0z1g0n1Y2q1V1X1W1E0S1}0-1z0z1=2a1D1o1q0(1K2A0L2C0z0H2G1D0B2j1g2o2q2U0^1%2e2I1.2N0b0|0g0=0j2n2Y0?2X1|2!1L2$2(0=0l2,1(2.2o2z012?0E2)040I2`2p0@2}2;0-30320M352|2Y2~3b0=0D3e373g392 0H2%310=0N3l2/2Z1s2=3q2@040e3v383y3a3A3s040w3e1j2S182G2t0S1X2y3o0R2O1_1g3Q1h3O2W192-053W0W2T3n3G010J0=0W0f3e0k3w3h0f0=1V0L0K2L0x0m0b2m3(2{3`3o0;040G3M3F2J2 3}0E1G0m0E0A4d3.4f4a0i0s3l0k4u3_4e1.3;040f3q3^483/0z0=0L4D4x1L0H0y4H17462p4w4o2#0h0=0b1(0q0m4n2:3/4a4c4Q3-4$4f0T0L2*4#3x4p0=0t4J4T2=4V04204;2~4(4 3o4G041S4k4m4*4E4?040i4r4t4v5g5a2#0=0o310m0A0b0K0E400z422j0b4_4,1.0H0=0d5x4=5j554i1H4l524%0=0Q5K4f544I594K0-4a0v5f5g4u5i2=0=0r5D2~5A045C4*4S5y5#045R2U065Y4v5!0-4z0y1v1H5(535$603/5*020q0c0P634-4/040U6a5z4N041(0S6f5:565J5S4`5U5M5O5F5%5-5`015*0u6l0-4.4:6p5/6r045W6w5T6y0=66686B4g045l1H5o5q5s5u436t1L4a4s4*5@5^5Y6x546n582W6L4a5N6F5E5:6v6:6q015V6Q5*5,2U5.6^3a4h4j6o6{6G6}6s6@3h626K6|6z6Q6D042+7d490=6J5?6*6+6L546`2-735)5B6Q7v6 0=6A7g7a7k7m7r6*6,765I6/3)6;7c79746R7w477R6I7D5+7B5k5m6V5r41436Z6(183+0m2q2R7;3P1p3R2t2w2r5H7@0n3Q0@800X0Z0#04.
tester le tri par insertion

Tester ci-dessous. Que s'est-il passé ?

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

Solution

La fonction n'a pas de return, c'est une procédure. Elle renvoie donc None

Que fait la fonction de tri par insertion ?

Tester ci-dessous. Que s'est-il passé ?

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

Solution

La liste de départ a été modifiée ...

C'est ce qu'on appelle un effet de bord. La fonction a modifié "en place" la liste.

Activité 3 - Complexité du tri par insertion

Déterminons le coût de cet algorithme.

Tri par insertion « à la main »

On considère le tableau \([6,\,1,\,4,\,5,\,2,\,3]\).

  • Lors de la première itération, le \(6\) reste à sa place
  • Après deux itérations, le tableau est \([1,\,4,\,6,\,5,\,2,\,3]\)
  • Après trois itérations, le tableau est \([1,\,2,\,4,\,6,\,5,\,3]\)
  • Au total, la valeur \(6\) sera déplacée 5 fois
  • ❌ La première itération étudie la deuxième valeur, le \(1\). Le \(6\) sera donc décalé
  • ✅ Oui car on a traité les valeurs \(1\) et \(4\)
  • ❌ La troisième itération décale le \(5\). On obtient donc \([1,\,4,\,5,\,6,\,2,\,3]\)
  • ✅ Oui car cette valeur doit être décalée à la fin du tableau

La performance du tri par insertion peut s'évaluer en nombre d'itérations élémentaires. Comme dans le cas du tri par sélection, il y a deux boucles imbriquées :

  • la boucle principale est une boucle « Pour ». Elle parcourt les valeurs de la deuxième à la dernière afin de les insérer à la bonne position à leur gauche,
  • chaque insertion est menée dans une seconde boucle « Tant que » qui décale la valeur vers la gauche.

À l'issue de chaque tour de la boucle principale, on a inséré un élément sur la gauche et on est certain que tous les éléments du début du tableau jusqu'à celui-ci inclus sont triés dans l'ordre croissant.

On présente ci-dessous les différentes étapes du tri. Chaque onglet correspond à une itération de la boucle principale.

Étape 0

Étape 1

Étape 2

Étape 3

Étape 4


Dans un tableau de \(5\) valeurs, on effectue donc \(4\) itérations de la boucle principale.

Les différentes étapes de la boucle secondaire correspondent aux illustrations présentées en haut de la page.

Combien d'itérations ? de lectures ?

On considère un tableau de \(20\) valeurs initialement trié dans l'ordre décroissant.

  • La boucle principale va s'exécuter \(19\) fois
  • Lors de la première itération de la boucle principale on doit lire \(1\) valeur
  • La troisième itération de la boucle principale va effectuer \(3\) décalages de valeurs
  • Lors de la dernière itération de la boucle principale on doit effectuer \(1\) seul décalage
  • ✅ Oui car la première valeur n'est pas traitée par cette boucle
  • ❌ On étudie la deuxième valeur et on la compare à la première. Il faut lire \(2\) valeurs
  • ✅ On place correctement la quatrième valeur : elle remonte de trois positions. On effectue donc \(3\) décalages
  • ❌ Lors de la dernière itération, la dernière valeur remonte en première position. Il faut effectuer \(19\) décalages
Complexité dans le "pire des cas?"
Solution

Le pire des cas est atteint lorsque le tableau est trié dans l'ordre décroissant. Dans ce cas, pour un tableau de \(N\) valeurs :

Dans le pire des cas :

  • la boucle principale effectue \(N-1\) itérations,
  • la première boucle secondaire effectue \(1\) décalage,
  • la deuxième effectue \(2\) décalages,
  • la troisième \(3\) décalages,
  • ...
  • la dernière boucle \(N - 1\) décalages.

On effectue donc au total \(1 + 2 + 3 + \dots+(N-1)\) décalages. On retrouve la somme étudiée dans cette page. Le coût de cet algorithme est donc quadratique.

Complexité dans le "meilleur des cas?"
Solution

Dans le cas où le tableau est initialement trié dans l'ordre croissant, l'algorithme n'effectuera qu'une seule comparaison et aucun échange à chaque itération de la boucle principale.

Le coût sera alors linéaire.

Activité 4 - Mesures du temps de calcul
On se place dans le pire des cas

Le pire des cas est celui où la liste est triée par ordre décroissant.

Vérifions sur une liste de taille 100000 ...

Recopier le script ci-dessous dans votre éditeur Python (pas en ligne)

Il faut attendre un peu, on trie une "grosse" liste ...

from timeit import timeit

def tri_insertion(tableau):
    for i in range(1, len(tableau)):
        valeur_a_inserer = tableau[i]
        j = i
        while j > 0 and tableau[j - 1] > valeur_a_inserer:
            tableau[j] = tableau[j - 1]
            j = j - 1
        tableau[j] = valeur_a_inserer


liste_croiss = [i for i in range(10000)]
liste_decroiss = [i for i in range(10000,0,-1)]
t_croiss = timeit("tri_insertion(liste_croiss)", number = 1, globals = globals())
t_decroi = timeit("tri_insertion(liste_decroiss)", number = 1, globals = globals())
print("t_croiss = ",t_croiss)
print("t_decroi = ",t_decroi)

Remarque

En nous inspirant de ce qui a été fait sur les tri par sélection, nous allons comparer le temps de tri pour différentes tailles de listes
💣 Attention, il y a une difficulté supplémentaire. Le tri par insertion est beaucoup plus lent si la liste n'est pas triée.
Nous avons vu que nous mesurons le pire des cas. Or le tri par insertion est à effet de bord. Donc n'oubliez pas que si vous avez chronométré le tri d'une liste une première fois , et que vous voulez recommencer une deuxième fois, il faut le faire sur la liste originale (indication : ou une copie de celle-ci...)

regardons plus précisément

Recopier le script ci-dessous dans votre éditeur Python (pas en ligne). Que se passe-t-il ?

from timeit import timeit

def tri_insertion(tableau):
    for i in range(1, len(tableau)):
        valeur_a_inserer = tableau[i]
        j = i
        while j > 0 and tableau[j - 1] > valeur_a_inserer:
            tableau[j] = tableau[j - 1]
            j = j - 1
        tableau[j] = valeur_a_inserer

liste_tailles = [300, 900, 2700]
liste_ref = [i for i in range(100,0,-1)] # liste [100, 99, 98, ..., 1]
# pour chronométrer les 20 fois, on reprend à chaque fois une copie de la liste de départ list(liste_ref)
tref = timeit("tri_insertion(list(liste_ref))", number = 20, globals = globals())
print("n = 100 : tref = ",tref)
for n in liste_tailles :
    print("n =", n, end = '')
    # Création de la liste triée par ordre décroissant de taille n : [n, n-1, ..., 1]
    liste_decr = [i for i in range(n, 0, -1)]
    # Temps d'éxécution du tri : il faut trier list(liste_decr) pour ne pas trier la liste déjà triée !
    temps = timeit("tri_insertion(list(liste_decr))", number = 20, globals = globals())
    # Affichage
    print('\t-> temps = ', round(temps, 2), '\t x', round(temps/tref, 2) )
    # Nouveau temps de référence avant le tour de boucle suivant
    tref = temps
Solution

Lorsque la taille de la liste et multipliée par 3, la temps est multiplié par 9. Or \(3^2 = 9\)

Cela correspond bien à une complexité quadratique

Visulalisation du temps de calcul

Notebook

Activité 5 - Correction de l'algorithme de tri par insertion

Prouver la correction de l'algorithme à l'aide d'un invariant de boucle.

Réponse

Après la ième itération de la boucle for (boucle en i de l’algorithme fourni) les i premiers éléments sont triés.

Cette propriété est un invariant de boucle pour le tri par insertion.
Cela se comprend aisément car dans chaque tour de boucle nous avions utilisé la fonction insere dont le rôle est de ranger les i premiers éléments de la liste par ordre croissant.

Cet invariant de boucle prouve la correction du tri par insertion.

Activité 6 - Terminaison de l'algorithme de tri par insertion

Justifier la terminaison à l'aide d'un variant de boucle.

Réponse

La procédure de tri par insertion contient une boucle while ⚠ : est-on sûr que cette boucle va se terminer ?

Cette boucle s'arrête quand l'une des 2 conditions est fausse, donc quand :

  • j <= 0
  • ou tableau[j - 1] <= valeur_a_inserer

Avant la boucle, j vaut i et 0 <= i <n.

Notons que si i = 0 on n'entre pas dans la boucle, elle se termine d'emblée sans commencer. La terminaison est alors évidente.

Sinon, on démarre avec j > 0, on entre donc dans la boucle.

Dans le corps de la boucle, j est décrémenté de 1.

On a donc un entier positif qui décroît strictement à chaque itération.

Donc j finira par devenir négatif si on n'a pas été stoppé avant par l'autre condition.

Ceci prouve que la boucle se termine.

j est un variant de boucle qui nous a permis de démontrer la terminaison de l'algorithme.

Ici, ce variant de boucle est une quantité qui décroît strictement et finit donc inévitablement par atteindre une valeur "plancher" qui assure la terminaison.

À retenir
  • Le tri par insertion a une complexité quadratique, c'est à dire de l'ordre du carré de la taille de la liste à trier \(N^2\) : \(O(n^2)\)
Algorithme de tri par insertion
def tri_insertion(tableau):
    for i in range(1, len(tableau)):
        valeur_a_inserer = tableau[i]
        j = i
        while j > 0 and tableau[j - 1] > valeur_a_inserer:
            tableau[j] = tableau[j - 1]
            j = j - 1
        tableau[j] = valeur_a_inserer