Cours d`informatique PC

publicité
Chapitre
1
Algorithmes de tri élémentaires
Le tri de données constitue un des problèmes fondamentaux de l’algorithmique. On considère une liste
de données, auxquelles sont attachées des clés. On cherche à trier ces données en fonction des clés (par
exemple, trier une liste d’élèves par ordre alphabétique, ou par ordre de moyennes...).
Nous nous placerons pour simplifier dans le cas où la clé est la donnée elle-même.
1.1
1.1.1
Tri par sélection
Principe du tri par sélection
Considérons une liste ` de longueur n. Supposons que la plage `[0..i[
– soit triée
– contienne les i plus petits éléments de la liste
(on initialise avec i = 0).
On sélectionne, dans la plage `[i..n[, le plus petit élément, que l’on permute avec `i
partie triée
`0
···
`i−1
···
`i
`∗
···
`n−1
permutation
Figure 1.1 – Tri par sélection : les i premiers éléments sont déjà placés
1.1.2
Programmation
Commençons par écrire une fonction qui détermine l’indice du plus petit élément d’une liste entre les
positions i et j :
1
2
3
4
5
6
7
1
2
3
def cherche_ind_min (l ,i , j ):
""" renvoie l ’ indice du plus petit é l é ment de la plage l [ i .. j [ """
imin = i
for k in range ( i +1 , j ):
if l [ k ] < l [ imin ]:
imin = k
return ( imin )
Nous pouvons maintenant écrire la fonction de tri :
def tri_selection ( l ):
""" trie la liste l par l ’ algorithme du tri s é lection .
La fonction modifie la liste l et ne renvoie rien """
1
Chapitre 1. Algorithmes de tri élémentaires
2
4
5
6
7
n = len ( l )
for i in range (n -1):
imin = cherche_ind_min (l ,i , n )
l [ i ] , l [ imin ] = l [ imin ] , l [ i ]
Rappelons que, lors de l’écriture d’un algorithme, on doit être en mesure du justifier sa terminaison
et sa correction.
La terminaison est ici évidente car on utilise une boucle inconditionnelle (boucle for), qui se termine
toujours (contrairement au cas d’une boucle while).
Pour justifier la correction de l’algorithme (il fait bien ce pour quoi il a été conçu), démontrons que
la propriété suivante est vraie :
«À l’issue du passage d’indice i dans la boucle, la plage `[0..i + 1[ est triée et elle contient les i + 1
plus petits éléments.»
– Lors du premier passage dans la boucle, on identifie le plus petit élément du tableau, que l’on place
en tête (par permutation de deux éléments). À l’issue de ce passage d’indice i = 0, la plage `[0..1[
est triée (elle contient un unique élément) et elle contient les i + 1 = 1 plus petits éléments.
– Supposons que la propriété soit vraie après le passage d’indice i. Lors du passage d’indice i + 1, on
détermine le plus petit élément de la plage `[i + 1..n[ (qui est par hypothèse supérieur à tous les `k
pour k 6 i), que l’on échange avec celui d’indice i + 1. À l’issue de ce passage d’indice i + 1, la plage
`[0..i + 2[ est encore triée et elle contient les i + 2 plus petits éléments.
Par récurrence finie, la propriété est encore vérifiée après le dernier passage, i.e. celui d’indice i = n − 2.
À l’issue de ce passage, la plage `[0..n − 1[ est donc triée, et elle contient les n − 1 plus petits éléments de
la liste. Par suite, le dernier élément `n−1 est supérieur à tous les précédents, donc la totalité de la liste
est triée.
Remarque 1. Une telle propriété (que nous avons démontrée par récurrence) est appelée un invariant de
boucle. C’est en général en exhibant un tel invariant que l’on prouve la correction des algorithmes.
1.1.3
Stabilité
Lorsque les données à trier sont distinctes des clés utilisées pour trier la liste (par exemple, un élève
n’est pas égal à sa moyenne), on peut s’intéresser à ce qui arrive à deux éléments ayant la même clé (deux
élèves ayant la même moyenne) : après le tri, ces deux éléments sont-ils dans la même position relative
l’un par rapport à l’autre qu’avant le tri ? Lorsque c’est le cas, on dit que le tri est stable.
La stabilité d’un tri est un élément intéressant lorsque l’on veut trier des données suivant un critère
prioritaire, puis un critère secondaire. Par exemple, si les données sont des couples (nom, prénom), on
peut chercher à trier par nom (critère prioritaire) puis, en cas d’égalité des noms, par prénom (critère
secondaire). Si l’on dispose d’un tri stable, on peut réaliser ce tri de la façon suivante :
– commencer par trier selon le critère secondaire ;
– trier ensuite selon le critère prioritaire.
Les données seront alors triées selon le critère prioritaire. En cas d’égalité des clés associées (le nom dans
notre exemple), les données seront rangées, après ce second tri, dans le même ordre qu’après le premier
tri, i.e. classées selon le critère secondaire (le prénom).
Le tri par sélection n’est pas stable : par exemple, si les données sont A, B et C, et que les clés attachées
sont 1, 1 et 0 (ce que l’on notera pas A1 , B1 , C0 ), le tri par sélection transforme la liste [A1 , B1 , C0 ] en
[C0 , B1 , A1 ] (les éléments A et B, de même clé, ont été permutés).
1.1.4
Complexité
Lorsque l’on cherche à évaluer la complexité d’un algorithme, on distingue en général la complexité
– spatiale (l’espace mémoire qui est nécessaire pour exécuter l’algorithme) ;
– temporelle (le temps nécessaire à son exécution).
Nous n’étudierons ici que la complexité temporelle des algorithmes.
Il est évidemment impossible de dire quelle durée prendra l’exécution d’un algorithme : cela dépend en
particulier de la vitesse de la machine sur lequel il est exécuté. Pour évaluer cette complexité temporelle,
nous chercherons à compter le nombre d’opérations élémentaires effectuées par l’algorithme. Ceci est en
général impossible à faire de façon exacte, aussi simplifions-nous le problème en choisissant de compter
uniquement les opérations «significatives». On cherche par ailleurs à obtenir une estimation asymptotique
1.2. Tri par insertion
3
de ce nombre plutôt qu’un décompte exact. Ainsi, on parlera d’algorithme linéaire (resp. quadratique,
exponentiel) lorsque le nombre de ces opérations significatives varie linéairement (resp. comme le carré,
l’exponentielle) de la taille n de la donnée à traiter.
Dans le cas du tri, on évalue la complexité d’un algorithme en comptant le nombre de comparaisons
effectuées entre les éléments de la liste.
– La fonction cherche_ind_min, appelée avec les arguments i et j, effectue j − i − 1 comparaisons.
Appelée avec les arguments i et n, elle en effectue n − i − 1.
– Chaque passage dans la boucle indexée par i de la fonction tri_selection effectue donc n − i − 1
comparaisons. Comme i varie de 0 à n − 2, il y a au total
C(n) =
n−2
X
(n − i − 1) =
i=0
n−1
X
k=
k=1
n(n − 1)
comparaisons.
2
Nous avons donc démontré le
Théorème
La complexité du tri par sélection est quadratique :
C(n) = O(n2 ).
Un tel algorithme est en général considéré comme peu efficace. Si la taille de la liste est multipliée pas
deux, le temps nécessaire à son tri est lui multiplié par quatre. De plus, si la liste est déjà triée (ce que
l’on peut tester par un simple parcours de liste, qui ne nécessite que n − 1 comparaisons), il n’y a rien de
plus à faire, alors que le coût de cet algorithme est quand même, dans ce cas particulier, quadratique.
Remarque 1. Même si la complexité quadratique est considérée comme mauvaise, il existe des problèmes
pour lesquels il n’existe pas d’algorithme performant. Par exemple, pour un algorithme de complexité
équivalente à 2n (complexité exponentielle), multiplier la taille des données pas 2 revient à élever aux
carré le nombre d’opérations à effectuer ! De tels algorithmes sont totalement inefficaces, sauf pour de
très petites valeurs de n (le temps de calcul nécessaire à traiter un problème dépasse rapidement l’âge de
l’univers...). En ce qui concerne le tri, nous verrons d’autres algorithmes plus performants que celui du
tri par sélection.
1.2
1.2.1
Tri par insertion
Principe du tri par insertion
C’est le tri du joueur de cartes qui reçoit ses cartes une à une. Le premier élément constitue une liste
triée à lui tout seul. On compare ensuite le deuxième élément au premier pour savoir si on l’insère avant
ou après le premier. À la k ème itération, on insère le k + 1ème élément dans la liste, déjà triée, des k
premiers éléments.
`0
`i−1
`i
`k−1
`k
`n−1
Figure 1.2 – Tri par insertion : insérer `k à sa place et décaler les éléments
1.2.2
1
2
Programmation
def tri_insertion ( l ):
""" trie la liste l par l ’ algorithme du tri insertion .
Chapitre 1. Algorithmes de tri élémentaires
4
3
4
5
6
7
8
9
10
11
La fonction modifie la liste l et ne renvoie rien """
n = len ( l )
for k in range (1 , n ):
a_placer = l [ k ]
i = k -1
while i >= 0 and a_placer < l [ i ]:
l [ i +1] = l [ i ]
i = i - 1
l [ i +1] = a_placer
Terminaison du programme :
– la boucle for se termine ;
– la boucle while aussi : même si on ne trouve aucun indice i tel que `i soit strictement supérieur
à l’élément à placer, i diminue strictement à chaque étape donc finira par devenir négatif, ce qui
terminera la boucle. Remarquons d’ailleurs que l’ordre dans lequel les conditions sont écrites n’est
pas anodin : on vérifie d’abord que i > 0, puis on lit l’élément dans la case d’indice i (si on faisait le
contraire, on risquerait de tenter de lire le contenu de la case d’indice −1, ce qui provoquerait une
erreur).
Démontrons maintenant la correction, en démontrant que la propriété «après le passage d’indice k
dans la boucle for, la sous-liste `[0..k + 1[ est triée».
– C’est vrai avant le premier passage (ce qui correspond à «après le passage d’indice k = 0) car la
liste `[0..1[ comporte un seul élément donc est triée.
– Supposons la propriété vraie après le passage d’indice k − 1 et vérifions qu’elle l’est encore après le
passage d’indice k. Pour cela, il faut vérifier que notre mécanisme d’insertion fonctionne bien. Il est basé
sur une boucle while, qui a deux causes possibles d’arrêt :
– soit on trouve un indice i > 0 tel que a_placer < `i . À cet instant, l’élément en position k − 1 a été
reculé à la position k, . . . et l’élément en position i + 1 a été reculé en position i + 2. La position
i + 1 est donc libre, et c’est celle dans laquelle il faut insérer l’élément.
– soit on n’a trouvé aucun tel élément et on s’arrête parce que i = −1. Lors du passage précédent
dans la boucle, l’élément situé en position 0 a été reculé en position 1 ; il faut insérer l’élément en
position 0.
Dans tous les cas, en sortie de la boucle while, c’est bien en position i + 1 qu’il faut insérer l’élément. À
l’issue du passage d’indice k, la propriété est encore vraie.
Par récurrence, elle l’est encore à l’issue du passage d’indice n − 1 : la liste est alors triée.
Remarque 1. À la différence du tri par sélection, le tri par insertion est stable : un élément de clé c ne
peut passer devant un autre élément que si cet autre est de clé c0 > c. Deux éléments de même clé ne
sont jamais permutés.
1.2.3
Complexité
À la différence du tri par sélection, la complexité n’est pas la même pour toutes les listes de taille n.
Nous allons étudier la complexité Cmin (n) dans le meilleur des cas et la complexité Cmax (n) dans le pire
des cas (toujours en nous intéressant au nombre de comparaisons effectuées entre éléments du tableau).
Dans le meilleur des cas, chaque élément `k (k > 1) est comparé à son prédécesseur et reste à sa place
car `k > `k−1 . Ce cas se produit lorsque le tableau est déjà trié ; la complexité est alors linéaire :
Cmin (n) = n − 1.
Dans le pire des cas, chaque élément `k (k > 1) est comparé à ses k prédécesseurs et est finalement
inséré en tête de liste. Ce cas se produit lorsque la liste est initialement triée à l’envers. La complexité
est alors quadratique :
n−1
X
n(n − 1)
.
Cmax (n) =
k=
2
k=1
Retenons :
1.3. Complexité minimale d’un algorithme de tri
5
Théorème
Les complexités du tri par insertion sont, dans le meilleur et dans le pire des cas :
Cmin (n) = O(n) (linéaire)
1.3
Cmax (n) = O(n2 ) (quadratique).
et
Complexité minimale d’un algorithme de tri
Remarque 1. Ce dernier paragraphe n’est pas à votre programme ; il est inséré à titre d’information.
Les deux algorithmes de tri que nous avons étudiés ont une complexité Cmax (n) en O(n2 ). Peut-on faire
mieux ? Oui : nous étudierons dans le prochain chapitre un algorithme dont la complexité maximale vérifie
Cmax (n) ∈ O(n log n). Cet algorithme est optimal au sens suivant :
Théorème
Un algorithme de tri procédant par comparaisons entre les clés des éléments à trier a, au mieux, une
complexité Cmax (n) ∈ O(n log n).
La preuve utilise la notion d’arbre de décision.
1.3.1
Arbres binaires
Un arbre binaire est un ensemble de nœuds, organisés de la façon suivante :
– un nœud et un seul n’est pointé par aucune flèche : c’est la racine de l’arbre ;
– de certains nœuds (appelés nœuds internes) partent une flèche gauche et une flèche droite, pointant
chacune sur un nœud (ce sont les fils du nœud interne) ;
– des autres nœuds (appelés feuilles) ne part aucune flèche.
Cette structure est utilisée pour représenter des données. Elle contient de l’information, stockée dans
les feuilles et les nœuds internes (ce sont les étiquettes de l’arbre) et dans les flèches (ce sont les labels de
l’arbre).
Exemple 1. Voici un exemple d’arbre binaire.
A
1
2
B
C
3
D
4
E
Figure 1.3 – Un exemple d’arbre binaire
Cet arbre est formé de deux nœuds internes, étiquetés A et C, et de trois feuilles (étiquetées B, D
et E). Le nœud A est la racine de l’arbre ; le sous-arbre pointé par la flèche de label 1 est le fils gauche
du nœud A ; le sous-arbre pointé par la flèche de label 2 est son fils droit. Ces deux sous-arbres ont pour
père le nœud A.
La hauteur d’un arbre est la longueur (i.e. nombre de flèches) maximale d’un chemin allant de la
racine à une feuille.
Exemple 2. L’arbre représenté sur la figure 1 a une hauteur égale à 2.
Remarque 1. Un arbre de hauteur n a donc au moins n + 1 feuilles (cas où chaque fils gauche est une
feuille par exemple) et au plus 2n feuilles (cas où tous les nœuds, sauf ceux du « bas », ont 2 fils).
Chapitre 1. Algorithmes de tri élémentaires
6
24 feuilles
4 + 1 feuilles
Figure 1.4 – Nombre de feuilles d’un arbre de hauteur n
1.3.2
Arbres de décision
On désire faire un choix dans un ensemble E de cardinal n 6= 0. Un arbre de décision associé à ce
choix est un arbre binaire tel que :
– la racine est étiquetée par l’ensemble E
– chaque feuille est étiquetée par un singleton inclus dans E
– chaque nœud qui n’est pas une feuille est étiqueté par un sous-ensemble E1 de E et ses deux fils
par des sous ensembles stricts E2 et E3 de E1 tels que E1 = E2 ∪ E3 .
De plus, on adjoint à chaque nœud un test booléen. Les deux flèches issues de ce nœud portent les labels V
et F (pour « vrai » et « faux »).
{a, b, c}
V
F
a < b?
{a, c}
V
{b, c}
F
a < c?
V
{a}
{c}
b < c?
{b}
F
{c}
Figure 1.5 – Arbre de décision pour la recherche de min(a, b, c)
L’instruction définie par un nœud est la suivante : on sait que le choix à effectuer se trouve dans
l’ensemble E1 . On effectue le test correspondant : si le résultat est V , alors le choix à effectuer se trouve
dans le sous-ensemble issu de la flèche V ; sinon, dans l’autre sous-ensemble.
La figure 1.5 représente un arbre de décision associé au choix du plus petit parmi trois nombres a, b
et c.
Remarque 1. Un arbre de décision associé à un ensemble de cardinal n doit posséder n feuilles au moins ;
sa hauteur h doit donc vérifier 2h > n, donc h > dlog2 ne.
Utilisons un arbre de décision pour le problème du tri d’une liste ` de taille n. En supposant les
éléments deux à deux distincts, il y a n! listes différentes contenant ces éléments. Chacune de ces listes
est une permutation de la liste `. Trier la liste revient à trouver, parmi les n! permutations de `, quelle est
celle qui est triée. Notre arbre de décision a pour ensemble de référence l’ensemble E des permutations
de ` ; les tests associés à chaque nœud sont les tests comparant deux éléments
Pn du tableau. La hauteur
minimale de l’arbre de décision est donc supérieure ou égale à dlog2 (n!)e = d k=2 log2 ke. Or la croissance
de la fonction ln permet d’écrire
Z
k
k+1
Z
ln t dt 6 ln k 6
k−1
ln t dt
k
pour tout entier k > 2, d’où, par somme :
Z
n
Z
ln t dt 6 ln(n!) 6
1
n+1
ln t dt,
2
1.3. Complexité minimale d’un algorithme de tri
7
soit encore
n ln n − n + 1 6 ln(n!) 6 (n + 1) ln(n + 1) − 2(ln 2 − 1),
puis l’équivalent ln(n!) ∼ n ln n, d’où enfin dlog2 (n!)e ∼ n log2 n. Comme le nombre de comparaisons dans
le pire des cas est égal à la hauteur de l’arbre de décision, nous avons démontré le théorème.
Téléchargement