récursivité algorithmique et complexité algorithmique et Les algorithmes de tri
1. La récursivité en algorithmique
1
I. Définition
Un algorithme est dit récursif s'il est défini en fonction de lui-même.
La récursion est un principe puissant permettant de définir une entité à l'aide d'une partie de celle-ci.
Chaque appel successif travaille sur un ensemble d'entrées toujours plus précis, en se rapprochant de plus en
plus de la solution d'un problème.
Evolution d’un appel récursif
L'exécution d'un appel récursif passe par deux phases, la phase de descente et la phase de la remontée :
Dans la phase de descente, chaque appel récursif fait à son tour un appel récursif. Cette phase se
termine lorsque l'un des appels atteint une condition terminale.
Condition pour laquelle la fonction doit retourner une valeur au lieu de faire un autre appel récursif.
La phase de la remontée, cette phase se poursuit jusqu'à ce que l'appel initial soit terminé, ce qui
termine le processus récursif.
II. Les types de la récursivité
La récursivité simple
La fonction contient un seul appel récursif dans son corps.
Exemple : la fonction factorielle
Trace d’exécution de la fonction factorielle (calcul de la valeur de 4! )
La récursivité multiple
La fonction contient plus d'un appel récursif dans son corps.
Exemple : le calcul du nombre de combinaisons en se servant de la relation de Pascal :
La trace d’exécution des fonctions récursive
multiple est sous forme d’arbre inversé.
La récursivité mutuelle
Des fonctions sont dites mutuellement récursives si elles dépendent les unes des autres
Par exemple la définition de la parité d'un entier peut être écrite de la manière suivante :
def pair(n) :
if n>=1 :
def imppair(n) :
if n>=1 :
def factorielle(n) :
if n>0 :
return n*factorielle(n-1)
return 1
def combinaison(n,p) :
if n>1 :
return combinaison(n-1,p) + combinaison(n-1,p-1)
return 1
Phase descente
Condition d’arrêt
Phase remontée
2. 2
return impair(n-1)
return True
return pair(n-1)
return False
La récursivité imbriquée
La récursivité imbriquée consiste à faire un appel récursif à l'intérieur d'un autre appel récursif.
Exemple : La fonction d'Ackermann
def ackermann(m, n):
if m == 0:
return n + 1
elif n == 0 and m>0:
return ackermann(m - 1, 1)
else:
return ackermann(m - 1, ackermann(m, n - 1))
III. Transformer une boucle en une procédure récursive
Soit la procédure suivante :
def compter(n) :
s=0
for i in range(1,n+1):
s+=i
return s
Cette procédure peut être traduite en une procédure récursive, qui admet un paramètre ; l'instruction qui
l'appellera sera "compter(1)":
def compter(n) :
if n>1 :
return n + compter(n-1)
else:
return 1
IV. Transformer deux boucles imbriquées en une procédure récursive
Supposons qu'on ait maintenant deux boucles imbriquées. Nous allons traduire progressivement cette
procédure itérative en une procédure récursive avec deux paramètres :
def compter02(n,p) :
if n>0:
P=0
for b in range(p):
P+= Matrice[n-1,b]
return P+compter02(n-1,p)
return 0
print(compter02(3,5))
Pour supprimer les deux boucles, on commence par supprimer la première en suivant l'exemple ci-dessus ;
on obtient la procédure suivante que l'on appelle avec "afficher(0)" :
def compter02(n,p) :
if n>0:
P=0
for b in range(p):
P+= Matrice[n-1,b]
return P+compter02(n-1,p)
return 0
print(compter02(3,5))
3. 3
Il ne nous reste plus qu'à supprimer la deuxième boucle; en sachant que lorsque "b=10" dans la procédure
initiale, le programme revient à la boucle sur "a" et remet "b" à zéro, alors on a 2 appels récursifs :
le premier dans le cas où "b" est inférieur à 10 et alors on appelle la procédure avec "a" inchangé et
"b" incrémenté de 1
le deuxième où "b=10" et alors on appelle la procédure avec "a" incrémenté de 1 et "b" initialisé à 0.
L'appel sera "affiche(0,0)".
def compter03(n,p) :
if n>0 :
if p>0 :
return Matrice[n-1,m-1]+compter03(n,m-1)
return compter03(n-1,p)
return 0
4. 4
Initiation à la complexité algorithmique
I. Introduction
Un algorithme est un ensemble d’instructions permettant de transformer un ensemble de données en un
ensemble de résultats avec un nombre fini d’étapes. Pour atteindre cet objectif, un algorithme utilise deux
ressources machine :
le temps
l’espace mémoire.
L’optimisation de l’efficacité en termes d’exécution va à l’encontre (en opposition) de l’optimisation en
espace.
Il n’y a pas de méthode ou d’échelle de mesure permettant d’évaluer la fiabilité ou la robustesse d’un
algorithme. Par contre, il existe des méthodes rationnelles et rigoureuses pour évaluer l’efficacité en espace
et en temps d’un algorithme, Analyse de la "complexité des algorithmes".
La complexité en temps d’un algorithme se mesure essentiellement en calculant le nombre d’opérations
élémentaires pour traiter une donnée de taille n.
On note Dn l’ensemble des données de taille n et T(n) le coût de l’algorithme sur la
donnée de taille n.
II. Les types de complexités
On définit 3 types de complexités :
A. Complexité au meilleur
Tmin(n) = mindϵDnT(d)
C’est le plus petit nombre d’opérations qu’aura à exécuter l’algorithme sur un jeu de données de taille fixée
(ici n). C’est une borne inférieure de la complexité de l’algorithme sur un jeu de données de taille n.
B. Complexité au pire
Tmax(n) = maxdϵDnT(d)
C’est le plus grand nombre d’opérations qu’aura à exécuter l’algorithme sur un jeu de données de taille fixée
(ici n).
Avantage : il s’agit d’un maximum et l’algorithme finira donc toujours avant d’avoir effectué Tmax(n)
opérations.
Inconvénient : cette complexité peut ne pas refléter le comportement usuel de l’algorithme, le pire cas
pouvant ne se produire que très rarement.
C. Complexité en moyenne
Tmoy(n) = ∑dϵDnT(d)/Dn
C’est la moyenne des complexités de l’algorithme sur des jeux de données de taille n.
Avantage : elle reflète le comportement général de l’algorithme si les cas extrêmes sont rares ou si la
complexité varie peu en fonction des données.
Inconvénient : la complexité sur un jeu de donnes particulier peut être nettement plus importante que la
complexité en moyenne, dans ce cas la complexité moyenne ne donnera pas une bonne indication du
comportement de l’algorithme.
Un algorithme est dit optimal si sa complexité est la complexité minimale parmi les algorithmes de sa
classe.
III. Définitions
A. La complexité asymptotique
La complexité asymptotique d’un algorithme décrit le comportement de celui-ci quand la taille n des
données du problème traité devient de plus en plus grande, plutôt qu’une mesure exacte du temps
d’exécution.
La complexité asymptotique de l’algorithme donne une mesure approximative du temps pris pour
l’exécution d’un algorithme étudié.
T : Dn → IN
n → T(n)
5. 5
Exemple
On considère que un algorithme (dans le pire des cas) a pris le temps suivant :
Tmax(n) = (n+1)*a + (n-1)*b + (2n+1)*c,
Alors on dira que la complexité de cet algorithme est tout simplement en n.
On élimine toute constante, et on suppose que les opérations d’affectation, de test et d’addition ont des
temps constants.
B. Notation de Grand-O
Soit f(n) une fonction non négative. f(n) est en O(g(n)) (f est dominée par g) s’il existe deux constante
positives c et n0 telles que: f(n) <= c*g(n) pour tout n >= n0.
Utilité : Le temps d’exécution est borné
Signification : Pour toutes les grandes entrées (i.e, n >= n0), on est assuré que l’algorithme ne prend pas plus
de c(g(n)) étapes : borne supérieure.
Exemple 1
Initialiser une liste d’entiers de taille n
Il y a n itérations dont chacune nécessite un temps <= c où c’est une constante (test logique + accès à la liste
+ affectation + incrémentation).
Le temps est donc T(n) <= c*n
Donc T(n) = O(n) (grand-O de n)
Exemple 2
T(n) = c
On écrit T(n) = O(1).
Exemple 3
T(n) = c1n2
+ c2n
c1n2
+ c2n <= c1n2
+ c2n2
= (c1 + c2)n2
pour tout n >= 1.
T(n) <= c.n2
où c = c1 + c2 et n0 = 1.
Donc : T(n) est en O(n2
).
Remarques
Si f(n) = O(g(n)) et g(n) = O(h(n)) alors f(n) = O(h(n)).
Si f(n) = O(kg(n)) où k > 0 est une constante alors f(n) = O(g(n)).
Si f1(n) = O(g1(n)) et f2(n) = O(g2(n)) alors (f1 + f2)(n) = O(max(g1(n),
g2(n)))
Si f1(n) = O(g1(n)) et f2(n) = O(g2(n)) alors f1(n)f2(n) = O(g1(n) g2(n))
IV. Quelques règles de calcul de complexité
Règle 1
La complexité d’un ensemble d’instructions est la somme des complexités de
chacune d’elles.
Règle 2 : instructions élémentaires
Les opérations élémentaires telles que l’affectation, test, accès à une liste (ou
tableau), opérations logiques et arithmétiques, lecture ou écriture d’une variable simple ... etc. Sont en O(1)
Règle 3 : instructions conditionnelles
Complexité de l’instruction Si: maximum entre le Alors et le
Sinon : T(n) = max(T(Bloc1), T(Bloc2))
Complexité de l’instruction de Sélection (switch) : maximum
parmi les différents cas.
Règle 4 : Instructions de répétition
La complexité de la boucle for est calculée par la complexité du
corps de cette boucle multipliée par le nombre de fois qu’elle est
répétée.
En règle générale, pour déterminer la complexité d’une boucle
while, il faudra avant tout déterminer le nombre de fois que cette boucle est répétée, ensuite le multiplier par
la complexité du corps de cette boucle.
Exemple 1
i = 0
while (i < n) :
L[i] = 0
i += 1
Exemple 2
Print(“Message”)
i=100*2
Exemple 3
S=0
for i in range(n)
for j in range(i+1,n)
S+=i*j
Print(“message”)
for i in range(n)
print(“intération:”,i)
6. 6
Règle 5 : Procédure et fonction
Leur complexité est déterminée par celle de leur corps. L’appel à une fonction est supposé prendre un temps
constant en O(1).
On fait la distinction entre les fonctions récursives et celles qui ne le sont pas. Dans le cas de la récursivité,
le temps de calcul est exprimé comme une relation de récurrence.
V. Efficacité d’un algorithme
A. Définitions
Un algorithme est dit efficace si sa complexité (temporelle) asymptotique est dans O(P(n)) où P(n) est un
polynôme et n la taille des données du problème considéré.
On dit qu’un algorithme A est meilleur qu’un algorithme B si et seulement si : O(TA(n))<O(TB(n)).
B. Classification
Complexité constante O(1) : On rencontre cette complexité quand toutes les instructions du problème sont
exécutées une seule fois quel que soit la taille du problème.
Complexité linéaire O(n) : C’est le cas d’une boucle de 1 à n et le corps de la boucle effectue un travail de
durée constante et indépendante de n.
Exemple : calcul du produit scalaire de deux vecteurs.
Complexité logarithmique O(log(n)) : La durée d’exécution croit légèrement avec n.
Ce cas se rencontre quand la taille du problème est divisée par une constante à chaque itération.
Exemple : recherche dichotomique dans une liste triée L[0,…,n-1].
Complexité quasi-linéaire ou n-logarithmique O(nlog(n)): se rencontre dans les algorithme où à chaque
itération la taille du problème est divisée par une constante avec à chaque fois un parcours linéaire des
données.
Exemple : tri par fusion.
Complexité quadratique O(n2) : C’est le cas des algorithmes avec deux boucles imbriquées chacune allant
de 1 à n et avec le corps de la boucle interne qui est constant.
Exemple : multiplication de deux matrices carrées d'ordre n
Complexité cubique O(n3) : Quadratique mais avec trois boucles imbriquées.
Complexité exponentielle O(2n) : Les algorithmes de ce genre sont dit naïfs car ils sont inefficaces et
inutilisables dès que n dépasse 50. On rencontre typiquement ces algorithmes dans les parcours arborescents.
Exemple : tours de Hanoï.
VI. Exercices d’application
Calculer la complexité des programmes suivants :
Programme 1
def prog1(n):
for i in range(n):
print(i)
T(n)=c*n=n
La complexité est : O(n)
Programme 2
def prog2(n):
for i in range(n):
for j in range(n):
print(i)
T(n)=c*n*n=cn²=n²
La complexité est :O(n)
Programme 3
def prog3(n):
s=1
i=1
while s<=n:
i+=1
s=s+i
S 1 3 6 10 15 ………… n
i 1 2 3 4 5 ………… k
K*(K+1)/2=n , k²=n , k=√ donc t(n)= √
Normalement c’est 2n mais les valeurs constantes
doivent être éliminer.
7. 7
print(s)
La complexité est : O(√ )
Programme 4
import math
def prog4(n):
n=int(math.sqrt(n))
for i in range(1,n+1):
print(i)
t(n)= √
La complexité est : O(√ )
Programme 5
for i in range(1,n+1):
for j in range(1,i+1):
for k in range(1,101):
Print(“message”)
i 1 2 3 … n
j 1 fois 2 fois 3 fois … n fois
k 100
fois
2*100
fois
3*100
fois
… n*100
fois
100+2*100+3*100+….+n*100
100*(1+2+…+n)
t(n)=n²
La complexité est :O(n²)
Programme 6
for i in range(1,n+1):
for j in range(1,pow(i,2)):
for k in range(1,(n//2)+1):
Print(“message”)
i 1 2 3 … n
j 1 fois 4 fois 9 fois … n² fois
k n/2
fois
n/2*4
fois
n/2*9
fois
… n/2*n²
fois
n/2+n/2*4+n/2*9+…+n/2*n²
n/2(1+2+9+…+n²)
n/2((n(n+1)(2n+1))/6)
t(n)=n4
La complexité est : O(n4
)
Programme 7
i,k=1,0
while i<n:
i*=2
k+=1
print(k)
i 1 2 3 … n
j 20
21
22
… 2k
n=2k
t(n)=log2(n)
La complexité est : O(log2(n))
Programme 8
def prog8(n):
l=0
for i in range(n//2,n+1):
for j in range(1,n//2+1):
k=1
while k<n: k*=2; l+=1
i = n/2 fois
j= n/2 fois
k=log2(n)
T(n)=n²log2(n)
La complexité est : O(n²log2(n))
Programme 9
for i in range(n//2,n+1):
j=1
while j<=n:
j*=2;k=1
while k<=n: k*=2
La complexité est : O(n*log2(n)²)
8. 8
Programme 10
while n>1:
n/=2
n 2 2² 23
… n
ité 1 2 3 … 2k
La complexité est : O(log2(n))
Programme 11
for i in range(1,n+1):
for j in range(1,n+1,j+i):
Print(“message”)
i 1 2 3 … k n
J n n/2 n/3 … 2/k n/n
t(n)=n*(1+1/2+1/3+…+1/n)
t(n)=n*logn
La complexité est :O(n*log(n))
Programme 12
n=2**2**k
for i in range(1,n+1):
j=2
while j<=n:
j=j**2;print(“message”)
K 1 2 3 … 2^2^k
N 4 16 2^2^3=2^8 2^2^k=n
J 2,4 2,4,16 2,2^2,2^4,2^8
ité N*2 N*3 N*4 N*(k+1)
T(n)= N*(k+1)=N*2^2^k=n*log2(log2(n))
La complexité est : O(n*log2(log2(n)))
9. 9
Les algorithmes de tri
Introduction
Le problème auquel nous allons nous confronter est très simple à comprendre : à partir d’une liste de
données numériques, réordonner ces valeurs par ordre croissant.
Tout sous-programme résolvant ce problème sera naturellement une procédure, il n'y a pas lieu ici de
calculer une valeur à retourner. La liste à trier en sera alors un paramètre qui sera lu et modifié.
Pour des raisons évidentes d'optimisation de la mémoire utilisée, nos tris se feront "sur place", c'est-à-dire
que nous n'utiliserons pas de listes intermédiaires pour stocker provisoirement les valeurs.
Il s’agit d’un des plus problèmes les plus classiques de l’algorithmique. De nombreuses résolutions en sont
possibles avec des méthodes radicalement différentes.
Méthodes utilisées
Nous étudierons dans ce chapitre deux types d'algorithmes de tri :
Des algorithmes itératifs
Des algorithmes récursifs, basés sur le paradigme “diviser pour régner”, dans lesquels le tri d’une
liste s’effectuera en la divisant plusieurs fois par deux jusqu’à obtention de sous-listes ne comportant
qu’une seule valeur.
I. Algorithmes itératifs
On va présenter dans cette partie les trois algorithmes de tri itératifs les plus connus.
A. Tri par sélection
Le tri par sélection est assez intuitif du point de vue mathématique, puisqu'il consiste dans un premier temps
à mettre à la première place le plus petit élément de la liste, puis à la seconde place le deuxième plus petit
élément, etc.
En voici sa description précise :
Rechercher dans la liste la plus petite valeur et la permuter avec le premier élément de la liste.
Rechercher ensuite la plus petite valeur à partir de la deuxième case et la permuter avec le second
élément de la liste.
Et ainsi de suite jusqu’à avoir parcouru toute la liste.
Exemple : Déroulement du tri par sélection sur un exemple
On considère la liste suivante de cinq entiers :
Première itération :
On détermine le minimum des éléments de
la liste :
Et on le permute avec le premier élément de
la liste :
Seconde itération :
On détermine le minimum des éléments de
la liste à partir de la deuxième case :
Et on le permute avec le second élément de
la liste :
Troisième itération :
On détermine le minimum des éléments de
la liste à partir de la troisième case :
Et on le permute avec le troisième élément
de la liste :
Quatrième itération :
On détermine le minimum des éléments de
la liste à partir de la quatrième case :
Et on le permute avec le quatrième élément
de la liste :
10. 10
Le cinquième et dernier élément la liste est
de fait à sa place, le tri est terminé :
1. Version itérative
Tri par sélection Complexité
def triSelection(l):
for i in range(len(l)-1):
indMini=i
for j in range(i+1,len(l)):
if l[j]<l[indMini]:
indMini=j
l[i], l[indMini]= l[indMini],
l[i]
2. Version récursive
Tri par sélection Complexité
def RecSelection(list, i, j=1, flag=True):
print("i=",i,"j=",j)
size = len(list)
if (i < size - 1):
if flag:
j = i + 1;
if (j < size):
if (list[i] > list[j]):
list[i],list[j]=list[j],list[i]
RecSelection(list, i, j + 1, False);
else:
RecSelection(list, i + 1, i+2);
list = [6, 2, 3, 7, 1, 6, 2, 3, 7, 1]
print("Liste non triée : ",list)
RecSelection(list, 0)
print("Liste triée: ",list)
B. Tri à bulles
Le tri à bulles consiste à parcourir toute la liste en comparant chaque élément avec son successeur, puis en
les remettants dans le "bon" ordre si nécessaire. Et à recommencer si besoin est.
En voici sa description précise :
Parcourir la liste en comparant chaque élément avec son successeur.
Si ce dernier est le plus petit des deux, les permuter.
Si lors du parcours de la liste une permutation au moins a été effectuée, recommencer un nouveau
parcours.
Exemple : Déroulement du tri à bulles sur un exemple
On considère la liste suivante de cinq entiers :
Première itération :
On compare 5 et 8 :
Ils sont dans l'ordre, donc on ne les permute
pas :
On compare 8 et 2 :
Seconde itération :
On compare 5 et 2 :
On les permute pour les mettre dans l'ordre :
On compare 5 et 8 :
Ils sont dans l'ordre, donc on ne les permute
pas :
11. 11
On les permute pour les mettre dans l'ordre :
On compare 8 et 9 :
Ils sont dans l'ordre, donc on ne les permute
pas :
On compare 9 et 5 :
On les permute pour les mettre dans l'ordre :
On compare 8 et 5 :
On les permute pour les mettre dans l'ordre :
Troisième itération :
On parcourt de nouveau la liste, mais cette fois-ci
on constate qu’il n’y a plus de permutations à
effectuer.
On n'effectue donc pas de quatrième itération,
la liste est triée :
1. Version itérative
Tri à bulles Complexité
def bubbleSort(L):
for dernier in range(len(L)-1,0,-1):
for i in range(dernier):
if L[i]>L[i+1]:
L[i],L[i+1]=L[i+1],L[i]
2. Version récursive
Tri à bulles Complexité
def RecBubble(Array, f, d=0):
if d < f-1:
if Array[d] > Array[d+1]:
Array[d],Array[d+1]=Array[d+1],Array[d]
RecBubble(Array, f, d+1)
elif f>1:
RecBubble(Array, f-1, 0)
list = [6, 4,3,1,5,2, 3, 7]
print("Liste non triée : ",list)
RecBubble(list, len(list)-1)
print("Liste triée : ",list)
C. Tri par insertion
Le tri par insertion est celui naturellement utilisé par les joueurs de cartes pour trier leur jeu. Il consiste à
insérer les éléments un par un en s'assurant que lorsque l'on rajoute un nouvel élément, les éléments déjà
insérés restent triés.
En voici sa description précise :
Considérer le premier élément de la liste. A lui tout seul il constitue une liste triée.
Insérer ensuite le second élément de telle sorte que les deux premiers éléments soient triés.
Continuer ainsi en insérant successivement chaque élément à sa “bonne” place dans la partie déjà
triée de la liste.
Exemple : Déroulement du tri par insertion sur un exemple
On considère la liste suivante de cinq entiers :
Insertion du second élément vis-à-vis du
premier :
On considère la valeur 3 :
Que l’on retire provisoirement de la liste :
Insertion du troisième élément vis-à-vis des
deux premiers :
On considère la valeur 1 :
Que l’on retire provisoirement de la liste :
12. 12
On décale le 5
Et on réinsère la valeur 3 :
On décale le 3 et le 5 :
Et on réinsère la valeur 1 :
Insertion du quatrième élément vis-à-vis des
trois premiers :
On considère la valeur 4 :
Que l’on retire provisoirement de la liste :
On décale le 5 :
Et on réinsère la valeur 4 :
Insertion du cinquième élément vis-à-vis des
quatre premiers :
On considère la valeur 2 :
Que l’on retire provisoirement de la liste :
On décale le 3, le 4 et le 5 :
Et on réinsère la valeur 2 :
Chaque élément a été inséré à sa place, le tri est terminé :
1. Version itérative
Tri par insertion Complexité
def insertionsort(A):
for i in range(len(A)):
for k in range(len(A)-1, i, -1 ):
if (A[k] < A[k-1]):
A[k],A[k-1] = A[k-1],A[k]
2. Version récursive
Tri par insertion Complexité
def insertionSort(array,i=1):
print("array",array)
if i >= len(array):
return array
if array[i-1] > array[i]:
temp = array[i]
for a in range(0, i):
print(array[a])
if temp < array[a]:
array.insert(a,temp)
del array[i+1]
break
return insertionSort(array, i+1)
L=[4,5,2,1] ; print("Liste non triée :
",L)
insertionSort(L); print("Liste triée :
",L)
II. Algorithmes récursifs
On va présenter dans cette partie les deux algorithmes de tri récursifs les plus connus. Ils utilisent tous les
deux le paradigme « diviser pour régner ».
13. 13
A. Tri fusion
Le tri fusion consiste à trier récursivement les deux moitiés de la liste, puis à fusionner ces deux sous-listes
triées en une seule. La condition d’arrêt à la récursivité sera l’obtention d'une liste à un seul élément, car une
telle liste est évidemment déjà triée.
Voici donc les trois étapes (diviser, régner et combiner) de cet algorithme :
1. Diviser la liste en deux sous-listes de même taille (à un élément près) en la "coupant" par la moitié.
2. Trier récursivement chacune de ces deux sous-listes. Arrêter la récursion lorsque les listes n'ont plus
qu'un seul élément.
3. Fusionner les deux sous-listes triées en une seule.
Exemple : Déroulement du tri fusion sur un exemple
On considère la liste suivante de sept entiers :
On la subdivise en deux sous-listes en la coupant par la moitié :
Sous-listes que l’on scinde à leur tour :
Sous-listes que l’on scinde à leur tour :
Ces sous-listes sont triées car elles n’ont qu’un élément. On va maintenant les fusionner deux par deux en
de nouvelles sous-listes triées :
De nouveau une étape de fusionnement :
Une dernière fusion :
On a fusionné toutes les sous-listes obtenues lors des appels récursifs, le tri est terminé :
14. 14
1. Algorithme
Algorithme Tri par fusion
def tri_fusion(liste):
if len(liste)<2:
return liste(:)
else:
milieu = len(liste)//2
listel = tri_fusion(liste[:milieu])
liste2 = tri_fusion(liste[milieu:])
return fusion(listel,liste2)
Fonction de fusion
def fusion(listel,liste2):
liste=[]
i, j=0,0
while i<len(listel)and j<len(liste2):
if listel[i]<=liste2[j]:
liste.append(listel[i]); i+=1
else:
liste.append(liste2[j]); j+=1
while i<len(listel):
liste.append(listel[i]); i+=1
while j<len(liste2):
liste.append(liste2[j]); j+=1
return liste
B. Tri rapide
Il existe plusieurs algorithmes qui ont une complexité optimale en O(n*log(n)). On remarque cependant
qu'en pratique, un de ces tris est souvent plus rapide que les autres. C'est le tri rapide, qui est un des
algorithmes de tri les plus adoptés.
Le tri rapide est un algorithme récursif. Le principe est de choisir un élément de la liste, que l'on nommera
pivot, et de créer deux autres listes : une à gauche du pivot, composée des éléments inférieurs au pivot et une
à droite du pivot, composée des éléments supérieurs au pivot. On applique la même méthode pour ces sous-
listes et ainsi de suite jusqu'à avoir des listes d'un seul élément.
Il existe plusieurs variantes du tri rapide selon la façon de choisir le pivot. Dans notre cas, nous choisirons
comme pivot le premier élément de la liste.
Voici donc les trois étapes (diviser, régner et combiner) de cet algorithme :
1. Considérer le premier élément de la liste et le positionner à sa place définitive, avec à sa gauche une
sous-liste constituée d’éléments qui lui sont inférieurs ou et égaux et à sa droite une sous-liste
constituée d’éléments qui lui sont strictement supérieurs.
2. Appliquer récursivement ce même traitement aux deux sous-listes ainsi obtenues. Arrêter la
récursion lorsque les listes n'ont plus qu'un seul élément.
3. Pas de résultats à combiner.
Exemple : Déroulement du tri rapide sur un exemple
On considère la liste suivante de huit entiers :
Lors du premier appel récursif, on va positionner le premier élément, à savoir 7, à sa place définitive. Pour
faire cela, on va mettre à sa gauche les éléments de la liste qui lui sont inférieurs ou égaux, et à sa droite
ceux qui lui sont strictement supérieurs :
15. 15
Lors du second appel récursif, selon le même principe on va placer les éléments 4 et 10 qui sont les
premiers éléments des sous-listes créées lors de l'appel précédent :
Le placement des éléments 4 et 10 s'est fait à l’intérieur des deux sous-listes délimitées par l'élément 77.
On voit bien ici le côté récursif de cet algorithme, puisque l’on applique le même traitement aux deux
sous-listes qu’à la liste initiale.
Lors du troisième appel récursif, on va placer les éléments 2, 5 et 14 qui sont les premiers éléments des
sous-listes créées lors de l'appel précédent :
Lors du quatrième appel récursif, on va placer les éléments 12 et 16 qui sont les premiers éléments des
sous-listes créées lors de l'appel précédent :
Chaque élément a été positionné à sa place, le tri est terminé :
16. 16
1. Algorithme
def quickSort(alist):
quickSortHelper(alist,0,len(alist)-1)
def quickSortHelper(alist,first,last):
if first<last:
splitpoint = partition(alist,first,last)
quickSortHelper(alist,first,splitpoint-1)
quickSortHelper(alist,splitpoint+1,last)
def partition(alist,first,last):
pivotvalue = alist[first]
leftmark = first+1
rightmark = last
done = False
while not done:
while leftmark <= rightmark and alist[leftmark] <= pivotvalue:
leftmark = leftmark + 1
while alist[rightmark] >= pivotvalue and rightmark >= leftmark:
rightmark = rightmark -1
if rightmark < leftmark:
done = True
else:
temp = alist[leftmark]
alist[leftmark] = alist[rightmark]
alist[rightmark] = temp
temp = alist[first]
alist[first] = alist[rightmark]
alist[rightmark] = temp
return rightmark
alist = [54,26,93,17,77,31,44,55,20]
quickSort(alist)
print(alist)
III. Comparaison des algorithmes de tri
Algorithme de tri Pire cas Cas moyen Meilleur cas (optimal)
Sélection n² n² n²
Bulle n² n² n
Insertion n² n² n
Fusion n logn n logn n logn
Rapide n² n logn n logn