Algorithmiqueavancee
Algorithmiqueavancee
Algorithmiqueavancee
complexité
Plan du
cours
Chap-1: Introduction & motivations
Chap-2: Complexité & optimalité
Chap-3: Algorithmes de tri:
analyse et estimation de la complexité
Chap-4: Récursivité
Différents types de récursivité
Dérécursivation d’algorithmes
Récursivité terminale et non terminale
Paradigme « diviser pour régner »
Chap-5: Graphes et arbres
Chap-6: Arbres binaires de recherche
Objectifs
Elaborer des algorithmes performants et efficaces
Comprendre la notion de complexité d’un algorithme
Maîtriser la récursivité (simple, multiple,
mutuelle, imbriquée)
Savoir dérécursiver des algorithmes simples et multiples
Maîtriser la démarche « diviser pour régner »
Savoir estimer la complexité d’un algorithme itératif ou
récursif pouvant conduire à des récurrences linéaires
d’ordre 1, d’ordre 2 et des récurrences de type « diviser
régner »
Connaître les différents algorithmes de tri et estimer leur
complexité
Elaborer des algorithmes à base de graphes et d’arbres
Réaliser des algorithmes de parcours de graphes et d’arbres
Chapitre 1 – Introduction
et motivations
Introduction
Un algorithme = une suite ordonnée d'opérations
ou d'instruction écrites pour la résolution d'un
problème donné.
Algorithme = une suite d’actions que devra
effectuer un automate pour arriver à partir d’un
état initial, en un temps fini, à un résultat
P(x) = (….(((anx+an-1)x+an-2)x+an-3)…..)x+a0
début
P = an
Pour i de n-1 à 0 (pas = –1) faire
P = P*X + ai
Coût de l’algorithme :
finpour - n additions
Fin - n multiplications
O(T Traitement )
iindDeb
Exemples de calcul de la
complexité
Exemple 1 : Tri par insertion
Principe : Cette méthode de tri s'apparente à
celle utilisée pour trier ses cartes dans un jeu :
on prend une carte, tab[1], puis la deuxième,
tab[2], que l'on place en fonction de la
première, ensuite la troisième tab[3] que l'on
insère à sa place en fonction des deux
premières et ainsi de suite. Le principe général
est donc de considérer que les (i-1) premières
cartes, tab[1],..., tab[i-1] sont triées et de
placer la ie carte, tab[i], à sa place parmi les
(i-1) déjà triées, et ce jusqu'à ce que i = N.
Exemples de calcul de la
complexité
Exemple 1 : Tri par insertion
Procédure tri_Insertion (var tab : tableau entier [N])
i, k :entier ;
tmp : entier ;
Pour i de 2 à N faire
tmp tab[i];
k i;
Tant que k > 1 ET tab[k - 1] > tmp faire
tab[k] tab[k - 1];
k k - 1;
Fin Tant que
tab[k] tmp;
Fin pour
Fin
Exemples de calcul de la
complexité
Exemple 1 : Tri par insertion
Calcul de la complexité:
la taille du tableau à trier est n.
On a deux boucles imbriquées :
La première indique l'élément suivant à insérer dans
la partie triée du tableau.
Elle effectuera n - 1 itérations puisque le premier
élément est déjà trié.
Pour chaque élément donné par la première boucle,
on fait un parcourt dans la partie triée pour
déterminer son emplacement.
Exemples de calcul de la
complexité
Exemple 1 : Tri par insertion
Calcul de la complexité:
Au meilleur des cas : le cas le plus favorable
pour cet algorithme est quand le tableau est
déjà trié (de taille n) O(n)
Au pire des cas : Le cas le plus défavorable
pour cet algorithme est quand le tableau est
inversement trié on fera une itération pour
le 1er élément, deux itérations pour le 2ème et
ainsi de suite pour les autres éléments.
Soit 1+2+3+4+…+(n-1) = n(n 1) n
sa complexité O(n2) 2
Exemples de calcul de la
complexité
Exemple 1 : Tri par insertion
Calcul de la complexité:
En moyenne des cas : En moyenne, la moitié
des éléments du tableau sont triés, et sur
l’autre moitié ils sont inversement triés.
O(n2)
Exemples de calcul de la
complexité
Exemple 2 : Recherche dichotomique
Fonction RechDicho(Tab :Tableau, borneinf :entier, bornesup :entier,
elemcherche :entier) : entier
Trouve = false ;
Tant que ((non trouve) ET (borneinf<=bornesup)) faire
mil = (borneinf+bornesup) DIV 2 ;
Si (Tab[mil]=elemcherche) Alors
trouve=true ;
Sinon
Si (elemcherche < Tab[mil])
Alors
bornesup = mil-1 ;
Sinon
borneinf = mil+1 ;
Fin Si
Fin Si
Exemples de calcul de la
complexité
Exemple 2 : Recherche dichotomique
Cette fonction effectue une recherche dichotomique d'un
élément dans un tableau trié. Supposons que le tableau est
de taille n une puissance de 2 (n = 2q).
Le pire des cas pour la recherche d'un élément est de
continuer les divisions jusqu'à obtenir un tableau de taille 1.
q le nombre d'itérations nécessaires pour aboutir à un
tableau de taille 1
La complexité = log2(n)
Chapitre 3 –
Les algorithmes de Tri
Tri par sélection
Principe :
Le principe est que pour classer n valeurs,
il faut rechercher la plus petite valeur
(resp. la plus grande) et la placer au
début du tableau (resp. à la fin du
tableau), puis la plus petite (resp. plus
grande) valeur dans les valeurs restantes
et la placer à la deuxième position (resp.
en avant dernière position) et ainsi de
suite...
Tri par sélection
Algorithme :
i, j: entier ;
tmp, small : entier ;
t : tableau entier [n] ;
Début
Pour i de 1 à n-1 faire
smalli;
Pour j de i+1 à n faire
Si t[j] <
t[small
] alors
sma
l
l T(n) = O(n²)
j
;
Tri par propagation / à
bulles
Principe :
Il consiste à parcourir le tableau tab en permutant
toute paire d'éléments consécutifs
(tab[k],tab[k+1]) non ordonnés - ce qui est un
échange et nécessite donc encore une variable
intermédiaire de type entier. Après le premier
parcours, le plus grand élément se retrouve dans
la dernière case du tableau, en tab[N], et il reste
donc à appliquer la même procédure sur le tableau
composé des éléments tab[1], ..., tab[N-1].
Tri par propagation / à
bulles
Algorithme :
Procédure tri_Bulle (tab : tableau entier [N] ) i,
k :entier ;tmp : entier ;
Pour i de N à 2 faire
Pour k de 1 à i-1 faire
Si (tab[k] > tab[k+1]) alors
tmp tab[k];
tab[k] tab[k+1];
tab[k+1] tmp;
Fin si
Fin pour T(n) = O(n²)
Fin pour
Fin
Tri par fusion
Principe :
Le principe du tri par fusion est plutôt
simple. On dit que trier un tableau de
taille N, c'est trier deux tableaux de taille
N/2; une fois les deux tableaux triés on
n’a plus qu'à les réunir (les fusionner) de
sorte à ce que le tableau final soit trié. Ce
tri bien sûr utilise une notion de
récursivité (un tableau étant la somme de
deux tableaux).
Tri par fusion
Algorithme :
Fonction tri_fusion (T, deb, fin) : tableau entier
T1, T2 : tableau entier [N/2] ;
Si (deb >= fin) alors
Retourner T ;
Sinon
mil = (deb + fin)
DIV 2;
T1 = tri_fusion (T, deb, mil) ; T2 =
tri_fusion (T, mil+1, fin)) ;
Retourner fusion (T1, T2) ;
Fin
FIN si
T(n) = 2 T(n/2) + Tfusion(n)
Tri par fusion
Fonction fusion (T1, T2) : tableau entier
T1 : tableau entier [1...N] ; T2 : tableau entier [1...M] ;
T: tableau entier [1...M+N] ;
i, j: entier; i1 ;j1 ;
Tantque (i+j-1 <> N+M) faire
Si (i N) alors
si (j M) alors
si
(T1[i]
<
T2[j])
alors
T
Fin si
[
sinon i
T[i+j-1]T1[i];
+ ii+1;
Fin si j
sinon -T[i+j-1]T2[j] ;
jj+1 ; Fin si 1
]
FinTanque Retourner T ;FIN Tfusion(n) = O(n)
Tri par fusion
L’algorithme Tri_Fusion est de type « diviser pour
régner ». Il faut donc étudier ses trois phases:
Diviser : cette étape se réduit au calcul du milieu
de l’intervalle [deb,fin], sa complexité est donc
en O(1).
Régner : l’algorithme résout récursivement deux
sous-problèmes de tailles respectives (n/2) ,
d’où une complexité en 2T(n/2).
Combiner : la complexité de cette étape est celle
de l’algorithme de fusion qui est de O(n) pour la
construction d’un tableau solution de taille n.
Tri par fusion
T(n) = 2 T(n/2) + O(n)
Rappel : Théorème de résolution de la récurrence
T(n) = a T(n/b) + O(nk):
si a > bk T(n) = O(nlog a )
b
si a = bk T(n) = O(nk logbn)
si a < bk T(n) = O( f (n)) = O(nk )
a = 2, b = 2, k = 1 (2ème cas)
T(n) = O(n log2n)
Tri rapide (Quicksort)
Principe :
Le tri rapide est fondé sur le paradigme « diviser pour
régner», tout comme le tri par fusion, il se décompose donc
en trois étapes :
Diviser : Le tableau T[deb..fin] est partitionné (et
réarrangé) en deux sous-tableaux non vides, T[deb..inter] et
T[inter+1..fin] tels que chaque élément de T[deb..fin] soit
inférieur ou égal à chaque élément de T[inter+1..fin].
L’indice inter est calculé pendant la procédure de
partitionnement.
Régner : Les deux sous-tableaux T[deb..inter] et
T[inter+1..fin] sont triés par des appels récursifs.
Combiner : Comme les sous-tableaux sont triés sur place,
aucun travail n’est nécessaire pour les recombiner, le tableau
T[deb..fin] est déjà trié !
Tri rapide
Tri_Rapide (T, deb, fin)
Si (deb < fin ) alors
inter =Partionner (T, deb, fin)
Tri_Rapide (T, deb, inter)
Tri_Rapide (T, inter+1, fin)
Fin si
Partionner (T, deb, fin)
x = T(deb)
i = deb-1 ; j= fin+1
Tant que (1)
Répéter { j=j-1 } Jusqu’à T(j) <= x
Répéter { i =i+1 } Jusqu’à T(i) >= x
si ( i < j )
permuter (T(i), T(j))
sinon retourner j Fin Si
Fin
Tri rapide : calcul de la
complexité
Pire cas
Le cas pire intervient quand le partitionnement produit une région
à n-1 éléments et une à 1 élément.
Comme le partitionnement coûte O(n) et que T(1) = O(1), la
récurrence pour le temps d’exécution est : T(n) = T(n-1) +O(n)
et par sommation on obtient : T(n) = O(n²)
Meilleur cas :
Le meilleur cas intervient quand le partitionnement produit deux
régions de longueur n/2.
La récurrence est alors définie par : T(n) = 2T(n / 2) + O(n)
ce qui donne d’après le théorème de résolution des
récurrences :
T(n) = O(nlog n)
Chapitre 4 – La
récursivité
Définitions
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 affinée, 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.
Ensuite, on commence 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.
Les types de
récursivité
1/ La récursivité simple :
récursivité simple la fonction contient un seul appel
récursif dans son corps.
Exemple : la fonction factorielle
Les types de
récursivité
Trace d’exécution de la fonction factorielle (calcul de la
valeur de 4!)
Les types de
récursivité
2/ La récursivité multiple:
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 :
Les types de
récursivité
3/ 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 :
Les types de
récursivité
4/ La récursivité imbriquée
Exemple : La fonction d'Ackermann
Récursivité terminale vs. non
terminale
Une fonction récursive est récursive
terminale si dite tous ses
terminaux. appels sont récursifs
Un appel récursif est terminal s'il s'agit de la
dernière instruction exécutée dans le corps d'une
fonction et que sa valeur de retour ne fait pas
partie d'une expression.
Les fonctions récursives terminales sont
caractérisées par le fait qu'elles n'ont rien à faire
pendant la phase de remontée
Importance de l’ordre des appels
récursifs
Proc. terminale Proc. non terminale
Procédure AfficherGaucheDroite Procédure AfficherDroiteGauche
( Tab : Tableau entier, N : ( Tab : Tableau entier, N :
entier, i : entier) entier, i : entier)
Si (i<=N) Alors Si (i<=N) Alors
Ecrire(Tab[i]) ; AfficherGaucheDroite AfficherDroiteGauche (Tab,N,i+1) ;
(Tab,N,i+1) ; Ecrire(Tab[i]) ;
Fin Si Fin Si
Fin Fin
T(n) = 2n – 1
Complexité exponentielle
Paradigme « diviser pour régner
» De nombreux algorithmes ont une structure récursive: pour
résoudre un problème donné, ils s’appellent eux-mêmes
récursivement une ou plusieurs fois sur des problèmes très
similaires, mais de tailles moindres, résolvent les sous
problèmes de manière récursive puis combinent les
résultats pour trouver une solution au problème initial.
Le paradigme « diviser pour régner » parcourt
trois étapes à chaque appel récursif à savoir :
Diviser : le problème en un certain nombre de sous-
problèmes de taille moindre ;
Régner : sur les sous-problèmes en les résolvant d'une façon
récursive ou le résoudre directement si la taille d'un sous-
problème est assez réduite ;
Combiner : les solutions des sous-problèmes en une solution
globale pour le problème initial.
Exemple
1Recherche de l’indice du maximum dans un tableau d’entiers
Fonction maximum ( Tab , indDeb, indFin)
Si ( indDeb = indFin) alors
retourner (indDeb)
Sinon
m=(indDeb+indFin)/2 //
division du problème en 2
sous-problèmes
k1 = maximum (Tab, indDeb, m ) // régner sur le 1er sous-problème k2 =
maximum (Tab, m+1, indFin)// régner sur le 2ème sous-problème
Si(Tab[k1] > Tab[k2]) Alors // combiner les solutions
retourner (k1)
Sinonretourner (k2) T(n) = 2 T(n/2) + cte
FinSi
FinSi
Fin
Exemple2 : multiplication de matrices
carrées
Dans cet exemple, on se propose de multiplier 2
matrices carrées A et B de taille n * n chacune,
où n est une puissance exacte de 2. C est la
matrice résultante.
Si on décompose les matrices A, B et C en sous-
matrices de taille n/2 * n/2, l'équation C = AB
peut alors se réécrire :
Logba = 3
T(n) = O(n3)
Analyse des algorithmes « Diviser pour Régner
»
Complexité de l’algorithme de recherche du
maximum: T(n) = 2 T(n/2) + 3
a = 2 , b = 2, k = 0 a > bk
Logba = 1
T(n) = O(n)
Application : algorithme de recherche
dichotomique
Fonction RechDicho(Tab :Tableau, borneinf :entier, bornesup :entier,
elem :entier) : bool
Si (borneinf<=bornesup) alors
mil = (borneinf+bornesup) DIV 2 ;
Si (Tab[mil]=elem) Alors
retourner (vrai)
Sinon
Si (Tab[mil]>elem) Alors
Retourner (RechDicho(Tab, borneinf, mil-1, elem))
Sinon
Retourner(RechDicho(Tab, mil+1, bornesup, elem))
Fin Si
Fin Si
Sinon
R
etour
ner
Application : algorithme de recherche
dichotomique
Analyse de la complexité :
T(n) = T(n/2) + cte
a = 1 , b = 2, k = 0 a = bk
T(n) = O(log2n)
Exemples
d’application
Exemple 1 : n
T (n) 9T
a=9,b=3,k=1 3
n
a > bk
Logba = 2
T(n) = O(n²)
Exemple 2 : 2n
T (n) T
a = 1 , b = 3/2 ,1k = 0 3
a = bk
T(n) = O(nklogn) = O(logn)
Exemples
d’application
Exemple 3 : n
T (n) 3T n log
a = 3 , b = 4 , k = ?? 4
n
or n < nlogn < n² 1 < k < 2
4 < bk < 16 a < bk
T(n) = O(f(n)) = O(n logn)
Autres résolutions de
récurrence
Equations de récurrence linéaires:
n
(0) f (i)
T (n) aTn 1 f n
n
T (n) a T i
a
i1
Exemple : Les Tours de Hanoi
n
1 n
T(n) = 2 T(n-1) + 1 T (n) 2 n 0 2i 2 1
i1
Autres résolutions de
récurrence
Equations de récurrence linéaires sans second
membre (f(n) = cte)
T (n) a1T n 1 a2T n 2... akT n k
cte
P(x) x on
A une telle équation, a1peut
k
x associer
a 2 x ...un k
k 1 k 2
Enregistrement Nœud
{
Info : Type (entier, réel, chaine, …)
FilsG : ^ Nœud
FilsD : ^ Nœud
}
Racine : ^Nœud
Il existe 3 méthodes de parcours
d’un arbre binaire :
parcours préfixe : père, fils gauche,
fils droit
parcours infixe : fils gauche, père, fils
Parcours
préfixé Préfixe(racine : ^Nœud)
Algorithme
AfficheRacine(racine)
infixe
(racine^.FilsD)
FinSi
Fin
Parcours postfixé
Algorithme Postfixe(racine : ^Nœud)