complexité
Mr. Slim Mesfar
Mail: mesfarslim@yahoo.fr
A.U. 2012-2013
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 du cours
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
i indDeb
Traitement )
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
Fin Tant que
Si (trouve) Alors Retourner (mil) ;
Sinon Retourner (-1) ;
Fin Si
Fin
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
small j ;
Fin si
Fin pour
tmpt[small];
T(n) = O(n²)
t[small] t[i];
t[i] tmp;
Fin pour
Fin
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
Fin pour Au pire et au meilleur des cas
Fin T(n) = O(n²) !!!
Tri par propagation / à bulles optimisé
Procédure tri_Bulle (tab : tableau entier [N] ) i,
k :entier ;tmp : entier ;
Trier : Bool; Trier Faux; i N
Tant que (i >= 2) ET (Trier = Faux) faire
Trier Vrai
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;
Trier Faux
Fin si
Fin pour Au pire des cas T(n) = O(n²)
Fin pour Au meilleur des cas T(n) = O(n)
Fin
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 dite récursive
terminale si tous ses appels sont récursifs
terminaux.
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 : entier, Tab : Tableau entier, N : entier,
i : entier) i : entier)
Si (i<=N) Alors Si (i<=N) Alors
Ecrire(Tab[i]) ; AfficherDroiteGauche (Tab,N,i+1) ;
AfficherGaucheDroite (Tab,N,i+1) ; Ecrire(Tab[i]) ;
Fin Si Fin Si
Fin Fin
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
Retourner (Faux)
FinSi
Application : algorithme de recherche dichotomique
Analyse de la complexité :
T(n) = T(n/2) + cte
a = 1 , b = 2, k = 0 a = bk
a = bk T(n) = O(nk logbn)
T(n) = O(log2n)
Exemples d’application
n
Exemple 1 : T (n) 9T n
a = 9 , b = 3 , k = 1
3
a > bk
Logba = 2
T(n) = O(n²)
2n
Exemple 2 : T ( n) T 1
3
a = 1 , b = 3/2 , k = 0
a = bk
T(n) = O(nklogn) = O(logn)
Exemples d’application
n
Exemple 3 : T (n) 3T n log n
a = 3 , b = 4 , k = ??
4
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
f (i)
T (n) aT n 1 f n T (n) a T (0) i
n
i 1 a
Exemple : Les Tours de Hanoi
n
1
T(n) = 2 T(n-1) + 1 T (n) 2 0 i 2 1
n n
i 1 2
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
A une telle équation, on peut associer un
polynôme: P( x) x k a x k 1 a x k 2 ... a
1 2 k
La résolution de ce polynôme nous donne m
racines ri ( avec m<=k).
La solution de l’équation de récurrence est ainsi
donnée par :
T (n) c1r1 c2 r2 c3r3 ... c r
n n n n
m m
sinon
T[i+j-1]T2[j] ; jj+1 ;
Fin si
sinon
T[i+j-1]T1[i]; ii+1;
Fin si
sinon T[i+j-1]T2[j] ;
jj+1 ; Fin si
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(nlogb a )
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
Tant que (i < j) Faire
Tant que ((i < j) et T[i] < x) Faire i := i + 1
Tant que ((i < j) et T[j] > x) Faire j := j - 1
échanger T[i] et T[j]
i := i + 1,
j := j - 1
Fin tant que
Échanger T[deb] et T[j] retourner(j)
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 5 –
Graphes et arbres
Les graphes
Un graphe orienté G est représenté par un couple
(S, A) où S est un ensemble fini et A une relation
binaire sur S. L’ensemble S est l’ensemble des
sommets de G et A est l’ensemble des arcs de G.
Il existe deux types de graphes :
graphe orienté : les relations sont orientées et on parle d’arc. Un
arc est représenté par un couple de sommets ordonnés.
Graphe non orienté : les relations ne sont pas orientées et on
parle alors d’arêtes.
Une arête est représentée par une paire de sommets non
ordonnés.
Les graphes
Une boucle est un arc qui relie un sommet à lui-même. Dans un
graphe non orienté les boucles sont interdites et chaque arête est
donc constituée de deux sommets distincts.
Degré d’un sommet : Dans un graphe non orienté, le degré d’un
sommet est le nombre d’arêtes qui lui sont incidentes. Si un
sommet est de degré 0, il est dit isolé.
Degré sortant d’un sommet : Dans un graphe orienté, le degré
sortant d’un sommet est le nombre d’arcs qui en partent,
Degré rentrant d’un sommet : le degré entrant est le nombre
d’arcs qui y arrivent et le degré est la somme du degré entrant et
du degré sortant.
Chemin : Dans un graphe orienté G = (S,A), un chemin de
longueur k d’un sommet u à un sommet v est une séquence
(u0,u1,…, uk) de sommets telle que u = u0, v = uk et (ui-1, ui)
appartient à A pour tout i.
Un chemin est élémentaire si ses sommets sont tous distincts
Les graphes
Un sous-chemin p0 d’un chemin p = (u0,u1, …. ,uk) est une sous-
séquence contiguë de ses sommets. Autrement dit, il existe i et j,
0<=i<= j <=k, tels que p0 = (ui,ui+1, …. ,uj).
Circuit : Dans un graphe orienté G=(S,A), un chemin (u0,u1, ….
,uk) forme un circuit si u0 =uk et si le chemin contient au moins un
arc. Ce circuit est élémentaire si les sommets u0, ..., uk sont
distincts. Une boucle est un circuit de longueur 1.
Cycle : Dans un graphe non orienté G = (S,A), une chaîne
(u0,u1,…., uk) forme un cycle si k >= 2 et si u0 = uk. Ce cycle est
élémentaire si les sommets u0, ..., uk sont distincts. Un graphe
sans cycle est dit acyclique.
Sous-graphe : On dit qu’un graphe G0 = (S0,A0) est un sous-
graphe de G = (S,A) si S0 est inclus dans S et si A0 est inclus
dans A.
Les arbres
Propriétés des arbres :
Soit G = (S,A) un graphe non orienté. Les affirmations
suivantes sont équivalentes.
1. G est un arbre.
2. Deux sommets quelconques de G sont reliés par un unique
chemin élémentaire.
3. G est acyclique et |A| = |S| - 1
4. G est acyclique, mais si une arête quelconque est ajoutée à
A, le graphe résultant contient un cycle.
Arbres – définitions
Arbre enraciné (ou arborescence): C’est un arbre dans
lequel l’un des sommets se distingue des autres. On appelle
ce sommet la racine. Ce sommet particulier impose en
réalité un sens de parcours de l’arbre
Ancêtre : Soit x un noeud (ou sommet) d’un arbre A de
racine r. Un noeud quelconque y sur l’unique chemin allant
de r à x est appelé ancêtre de x.
Père et fils : Si (y,x) est un arc alors y est le père de x et
x est le fils de y. La racine est le seul noeud qui n’a pas de
père.
Feuille ou noeud externe (ou terminal) : Une feuille est
un noeud sans fils. Un noeud qui n’est pas une feuille est
un noeud interne.
Sous-arbre : Le sous-arbre de racine x est l’arbre composé
des descendants de x, enraciné en x.
Arbres - définitions
Degré d’un nœud : Le nombre de fils du nœud x est
appelé le degré de x.
Profondeur d’un nœud : La longueur du chemin entre la
racine r et le nœud x est la profondeur de x.
Profondeur de l’arbre : c’est la plus grande profondeur
que peut avoir un nœud quelconque de l’arbre. Elle est dite
aussi la hauteur de l’arbre.
Arbre binaire : c’est un arbre dont chaque nœud a au plus
deux fils.
Arbre binaire complet : Dans un arbre binaire complet
chaque nœud est soit une feuille, soit de degré deux. Aucun
nœud n’est donc de degré 1.
Parcours des arbres
Parcours en profondeur
Dans un parcours en profondeur, on descend d’abord le
plus profondément possible dans l’arbre puis, une fois
qu’une feuille a été atteinte, on remonte pour explorer les
autres branches en commençant par la branche « la plus
basse » parmi celles non encore parcourues. Les fils d’un
nœud sont bien évidemment parcourus suivant l’ordre sur
l’arbre.
Algorithme:
Algorithme ParPro(A)
si A n’est pas réduit à une feuille alors
Pour tous les fils u de racine(A) Faire
ParPro(u)
Fin pour
finSi
Fin
Parcours des arbres
Parcours en largeur
Dans un parcours en largeur, tous les nœuds à une
profondeur i doivent avoir été visités avant que le premier
nœud à la profondeur i+1 ne soit visité. Un tel parcours
nécessite l’utilisation d’une file d’attente pour se souvenir
des branches qui restent à visiter.
Algorithme:
Algorithme Parcours_Largeur(A)
F : File d’attente
F.enfiler(racine(A))
Tant que F != vide Faire
u F.défiler()
Afficher (u)
Pour « chaque fils v de » u Faire
F.enfiler (v)
FinPour
Fin Tant que
Fin
Parcours des graphes
Le parcours des graphes est un peu plus compliqué que
celui des arbres. En effet, les graphes peuvent contenir des
cycles et il faut éviter de parcourir indéfiniment ces cycles.
Pour cela, il suffit de colorier les sommets du graphe.
Initialement les sommets sont tous blancs,
lorsqu’un sommet est rencontré pour la première fois il est
peint en gris,
lorsque tous ses successeurs dans l’ordre de parcours ont été
visités, il est repeint en noir.
Parcours des graphes
Algorithme Pacours_Profondeur (G) Algorithme VisiterPP(G, s)
Pour chaque sommet u de G Faire couleur[s] Gris
couleur[u] Blanc Pour chaque voisin v de s Faire
FinPour Si couleur[v] = Blanc alors
Pour chaque sommet u de G Faire VisiterPP(G, v)
si couleur[u] = Blanc alors FinSi
VisiterPP(G, u) FinPour
FinSi couleur[s] Noir
FinPour
Fin Fin
Parcours des graphes
Algorithme Parcours_Largeur(G, s)
F : File d’attente
Pour chaque sommet u de G Faire
couleur[u] Blanc
FinPour
couleur[s] Gris
F.enfiler(s)
Tant que F != Vide Faire
u F.défiler()
Pour chaque voisin v de u Faire
Si couleur(v) = Blanc alors
couleur(v) Gris
F.enfiler(v)
FinPour
Couleur(u) Noir
FinTant que
Chapitre 6 –
Arbres binaires de
recherche
Définitions
Un arbre binaire est un graphe qui admet une racine et
sans cycle et dont chaque nœud admet deux fils : fils droit
et fils gauche.
Structure de données :
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 droit
parcours postfixe : fils gauche, fils droit, père
Parcours préfixé
Algorithme Préfixe(racine : ^Nœud)