Tri I- Introduction
On parle de tri lorsque l’on veut classer des structures de données avec une relation d’ordre.
Le tri de séquences de données est courant :
- Trier par valeurs croissantes, décroissantes (pour ensuite faire un calcul de médiane)
- Trier par ordre lexicographique (exemple du dictionnaire…ce qui facilite la recherche)
- Trier des grandeurs qui ont une relation d’ordre entre elles (par exemple dans les algorithmes de compression de données, tri des tuples par clé primaire dans une base de données afin de gagner du temps lors d’une phase de recherche…)
Il existe la méthode sort() permettant de trier une liste.
liste= [0,1,2,3,7,8,9,4,5,6]
liste.sort()
print(liste)#[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
liste=["alban","thomas","andré","willy"]
liste.sort()
print(liste)#['alban', 'andré', 'thomas', 'willy']
Cependant, nous allons voir qu’il est instructif de connaître différentes méthodes de tri afin de pouvoir effectuer des opérations de tri avec une relation d’ordre quelconque.
L’algorithme est dit en place s’il modifie directement la structure de données sans en créer une nouvelle (typiquement une liste sous python) : cette technique limite la complexité spatiale (coût constant).
Un algorithme de tri d’une liste est dit stable s’il conserve la position relative dans la liste des quantités qui sont égales pour la relation d’ordre.
II- Tri par sélection
a) Principe
Comme pour une photo de classe on va chercher le minimum de la partie de la liste non triée.
Exemple :
5 7 3 1 9
1 7 3 5 9
1 3 7 5 9
1 3 5 7 9
b) Algorithme itératif
def tri_selection(L):
"""algorithme en place"""
for i in range(len(L)-1):
index_min = i
for j in range(i+1, len(L)):
if L[index_min] > L[j]:
index_min = j
L[i],L[index_min] = L[index_min],L[i]
return L c) Complexité
La complexité temporelle (dans le meilleur ou le pire des cas) mesurée par le nombre de comparaisons est :
𝑇(𝑛) = (𝑛 − 1) + (𝑛 − 2) + ⋯ + 2 + 1 =𝑛(𝑛 − 1) 2 ≈ 𝑂(𝑛2) d) Terminaison
La terminaison est évidente car on utilise deux boucles for (si on a bien vérifié l’absence de dépassement d’indice)
e) Correction
La propriété 𝐿[: 𝑖] est une liste triée est l’invariant de boucles : - Initialisation : évident en entrée de boucle lorsque 𝑖 = 0
- Continuité : Si 𝐿[: 𝑖] est bien triée alors la prochaine recherche conduit nécessairement à rajouter 𝐿[𝑖] ≥ 𝐿[𝑖 − 1]
- La dernière itération conduit à la comparaison des deux derniers éléments (inévitablement les plus grands de 𝐿)
Commenté [AM1]: Dans toute la suite, nous allons écrire des fonctions qui vont chercher à trier des listes.
Lors du passage d’une liste en paramètres ou arguments d’une fonction, ce passage se fait par référence, : il n’y a pas de variable locale associée et la liste est modifiée en place (les listes sont muables, ce qui n’est pas le cas avec une entier par exemple).
Cependant, nous utiliserons des return afin d’obtenir directement la liste modifiée
Commenté [AM2]:
Commenté [AM3]: https://www.youtube.com/watch?v
=jKEoj-sKvVQ
https://www.youtube.com/watch?v=ROalU379l3U Rappels :
1+2+3+4+5+6+7+8+9+10 = 11 *5 =>n(n+1)/2 1+2+3+4+5+6+7+8+9 = 11*5-10 =>n(n+1)/2-n=n(n-1)/2
Commenté [AM4]: Cette inégalité stricte assure un algorithme de tri stable
III- Tri par insertion a) Principe
C’est le principe du jeu de carte : le joueur classe chaque carte à fur et à mesure en les comparant à celles déjà triées. Exemple :
5 7 3 1 9
5 7 3 1 9
3 5 7 1 9
1 3 5 7 9
Ici le placement de 3 de fait en 2 déplacements unitaires, le déplacement de 1 en 3.
b) Algorithme itératif
def tri_insertion(L):
for i in range(1,len(L)):
cle=L[i]
j=i-1
while j>=0 and cle<L[j]:
j=j-1
L[j+2:i+1]=L[j+1:i]
L[j+1]=cle return L
c) Complexité
Si on compte le nombre de comparaisons, on peut distinguer 2 cas - le pire des cas (celui d’une liste triée en ordre décroissant) :
𝑇(𝑛) = 1 + 2 + 3 + ⋯ + 𝑛 − 1 =𝑛
2(𝑛 + 1) − 𝑛 =𝑛
2(𝑛 − 1) = 𝑂(𝑛2) - le meilleur cas (celui d’une liste déjà trié) :
𝑇(𝑛) = 𝑛 − 1 = 𝑂(𝑛) d) Terminaison
La terminaison est évidente pour la boucle for. Pour la boucle while, 𝑗 est le variant de boucle amenant à un arrêt inévitable (dans le pire cas).
e) Correction
La propriété 𝐿[: 𝑖] est une liste triée est l’invariant de boucles : - Initialisation : évident en entrée de boucle lorsque 𝑖 = 1 - Continuité : Si 𝐿[: 𝑖] est bien triée alors 2 cas :
→ 𝐿[𝑖] ≥ 𝐿[𝑖 − 1] la clé reste à la même place : 𝐿[: 𝑖 + 1] est triée
→ 𝐿[𝑖] < 𝐿[𝑖 − 1] et la clé est insérée à la bonne place : : 𝐿[: 𝑖 + 1]
est triée
- Terminaison : Le dernier élément est comparé aux éléments déjà triés
IV- Tri par fusion a) Principe
C’est le principe de diviser pour régner : une liste initiale de longueur 𝑛 = 2𝑝 (avec 𝑝 entier) est alors divisée 𝑘 = 𝑙𝑜𝑔2(𝑛) fois jusqu’à obtenir des listes de longueur unité alors très faciles à comparer entre elles. Cet algorithme repose ensuite sur une fonction 𝑓𝑢𝑠𝑖𝑜𝑛1() qui fusionne deux listes triées.
5 7 3 1 9
3 1 9
5 7
1 9
5 7 3 1 9
5 7 1 9
1 3 9
5 7
1
𝑑𝑖𝑣𝑖𝑠𝑒𝑟
𝑓𝑢𝑠𝑖𝑜𝑛𝑛𝑒𝑟
Principe de l’algorithme de fusion lors de la dernière étape : on part d’une liste
vide et on parcourt les deux
listes en plaçant l’élément le plus
petit
1 3
1 3 5
1 3 5 7
1 3 5 7 9
3
3
Commenté [AM5]: https://www.youtube.com/watch?v
=VHsxXqlzhd4
https://www.youtube.com/watch?v=ROalU379l3U
Commenté [AM6]: https://www.youtube.com/watch?v
=OEmlVnH3aUg&t=662s
b) Algorithme def fusion1(L1,L2):
i1,i2=0,0
n1,n2=len(L1)-1,len(L2)-1 L=[]
while i1<=n1 and i2<=n2:
if L1[i1]<L2[i2]:
L.append(L1[i1]) i1=i1+1
else :
L.append(L2[i2]) i2=i2+1
if i1 == n1+1 : return L+L2[i2:]
else :
return L+L1[i1:]
def fusion2(L):
if len(L)==1:
return L else :
L1=fusion2(L[:len(L)//2]) L2=fusion2(L[len(L)//2:]) return fusion1(L1,L2)
c) Complexité
On peut remarquer que l’on a 𝑙𝑜𝑔2(𝑛) niveaux et sur chaque niveau, on va comparer typiquement 𝑛 valeurs : 𝑇(𝑛) = 𝑛𝑙𝑜𝑔2(𝑛) la complexité est quasi- linéaire.
d) Terminaison
La boucle while de la fonction 𝑓𝑢𝑠𝑖𝑜𝑛1 se termine car elle présente un variant de boucle. La fonction 𝑓𝑢𝑠𝑖𝑜𝑛2 se termine aussi car les différents appels aboutissent à la condition triviale d’une liste à un élément.
e) Correction
Le cas trivial conduit d’une liste à un élément renvoie cet élément : ce qui est juste.
Supposons cet algorithme vrai pour toute liste de longueur 𝑛. Si on considère deux listes 𝐿1 et 𝐿2 de longueur 𝑛 et donc triables et une liste 𝐿 = [𝐿1] + [𝐿2] . Tous les éléments de 𝐿1 sont plus petits que tous les éléments de 𝐿2
𝑓𝑢𝑠𝑖𝑜𝑛2(𝐿) => 𝑓𝑢𝑠𝑖𝑜𝑛1(𝐿1, 𝐿2) Ce qui conduit effectivement à une liste triée.
V- Tri rapide a) Principe
C’est un tri récursif basé également sur la méthode « diviser pour régner ». Le principe est le suivant :
- On choisit un pivot 𝑝 dans le tableau (par exemple le 1e élément) - On enlève le pivot et on segmente le tableau en deux sous tableaux. Un
tableau contenant les éléments strictement plus petits que 𝑝 et les autres éléments supérieurs à 𝑝 dans un second tableau
- On recommence récursivement sur les tableaux obtenus jusqu’au cas trivial et on recombine
b) Algorithme def quicksort(L):
if L == []:
return []
else:
pivot = L[0]
L1 = []
L2 = []
for x in L[1:]:
if x<pivot:
L1.append(x) else:
L2.append(x)
return quicksort(L1)+[pivot]+quicksort(L2)
5 7 3 1 9
Pivot « 5 »
3 1 7 9
Pivots « 3 » et « 7 »
5
3 7
1 9
Cas trivial : on dépile
3 7
1 3 5 7 9
Réassemblage
1 9
Commenté [AM7]: L’algorithme proposé n’est pas en place car la liste triée est une nouvelle liste
Commenté [AM8]: Cet algorithme est stable car si 𝐿1[𝑖1] < 𝐿2[𝑖2] et que 𝐿2[𝑖2] = 𝐿2[𝑖2+ 1] alors 𝐿2[𝑖2] puis 𝐿2[𝑖2+ 1] seront placés successivement
Commenté [AM9]: https://www.youtube.com/watch?v
=ROalU379l3U
c) Complexité
Le « coût » temporel de l’algorithme de tri est principalement donné par des opérations de comparaisons sur les éléments à trier.
- Dans le pire des cas, un des deux segments est vide à chaque appel de la fonction de tri. Cela arrive lorsque le tableau est déjà trié.
𝐿 = [0,1,2,3,4,5,6,7] → 𝑛 = 8 → 7𝑐𝑜𝑚𝑝𝑎𝑟𝑎𝑖𝑠𝑜𝑛𝑠 𝑛 = 7 → 6𝑐𝑜𝑚𝑝𝑎𝑟𝑎𝑖𝑠𝑜𝑛𝑠 𝑛 = 6 → 5 𝑐𝑜𝑚𝑝𝑎𝑟𝑎𝑖𝑠𝑜𝑛𝑠
:
𝑛 = 2 → 1 𝑐𝑜𝑚𝑝𝑎𝑟𝑎𝑖𝑠𝑜𝑛 Dans ce cas, on a 1 + 2 + 3 + ⋯ + 𝑛 − 1 =𝑛(𝑛−1)
2 comparaisons et donc : 𝑇(𝑛) = 𝑂(𝑛2)
Donc 𝑇(𝑛) = 𝑂(𝑛2)
▪ Dans le meilleur des cas, à chaque appel, la division donne deux sous listes de même taille :
𝑇(𝑛) = (𝑛 − 1) + 2𝑇 (𝑛 − 1 2 )
De manière analogue au tri fusion, on a approximativement log2(𝑛) appels et 𝑛 comparaisons à chaque étape : 𝑇(𝑛) ≈ 𝑂(𝑛𝑙𝑜𝑔2(𝑛))
Enfin, on pourra noter l’importance du choix du pivot, par exemple dans une liste déjà triée, un pivot choisi au centre de la liste ne conduit pas au pire cas.
d) Terminaison
Cet algorithme récursif se termine car la variable de contrôle est la longueur de la liste et elle diminue strictement à chaque appel (car le pivot est enlevé à chaque appel) aboutissant au cas de base d’une liste vide
e) Correction Par récurrence :
- Si 𝑙𝑒𝑛(𝐿) = 𝑛 = 0,1 alors la liste est bien triée
- Supposons cet algorithme vrai pour toute liste de longueur 𝑛.
- Si on considère deux listes 𝐿1 et 𝐿2 de longueur 𝑛 et donc triables et une une liste 𝐿 = [𝐿1] + [𝑝] + [𝐿2] où 𝑝 est une liste à un élément pour lequel tous les éléments de 𝐿1 sont plus petit et tous les éléments de 𝐿2 sont plus grands. Alors l’algorithme de tri rapide de pivot 𝑝 conduit à :
𝑡𝑟𝑖(𝐿) = 𝑡𝑟𝑖(𝐿1) + [𝑝] + 𝑡𝑟𝑖(𝐿2) Ce qui conduit effectivement à une liste triée.
VI- Bilan
Tri par sélection Tri par
insertion Tri par fusion Tri rapide Avantage : il n’y
a que 𝑛 − 1 permutations
Avantage : Très efficace lorsque le tableau est déjà presque trié
Avantage : toujours la même complexité
Avantage : expérimentalement meilleur que le tri fusion
coût quadratique dans tous les cas
Meilleur cas 𝑂(𝑛)
Pire cas 𝑂(𝑛2)
𝑇(𝑛)
= 𝑂(𝑛𝑙𝑜𝑔2(𝑛)) Meilleur cas ; 𝑇(𝑛) = 𝑂(𝑛𝑙𝑜𝑔2(𝑛)) Pire cas : 𝑂(𝑛2)
Commenté [AM10]: Certains algorithmes de tri rapide prennent pour « pivot » le dernier élément, la valeur moyenne du premier et du dernier, ou un positionnement aléatoire dans le tableau. Pour se placer dans le meilleur des cas pour chaque segment de tableau, il faut prendre pour pivot la valeur médiane du tableau de valeurs. Le problème est que cette recherche de pivot idéal a aussi un « coût ».
Commenté [AM11]: On pourrait penser que le tri par fusion est la meilleure méthode de tri. Cependant, la situation du pire cas arrive rarement et c’est l’algorithme du tri rapide qui actuellement utilisé pour des grandes listes. Le tri par insertion est quant à lui très utilisé pour des listes de petites tailles (presque déjà triées). C’est le cas de la méthode sort sur python qui utilise les deux méthodes en fonction de la taille de la liste.