1. Algorithmique et
complexité
Mr. Slim Mesfar
Mail: mesfarslim@yahoo.fr
A.U. 2011-2012
2. 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
3. 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
5. 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
L’algorithmique désigne le processus de
recherche d’algorithme
6. Structures de données
Une structure de données indique la manière
d'organisation des données dans la mémoire.
Le choix d'une structure de données adéquate
dépend généralement du problème à résoudre.
Deux types de structures de données :
Statiques : Les données peuvent être manipulées dans
la mémoire dans un espace statique alloué dès le début
de résolution du problème. Ex : les tableaux
Dynamiques : On peut allouer de la mémoire pour y
stocker des données au fur et à mesure des besoins de
la résolution du problème. Ex: liste chainée, pile, file, …
notion des pointeurs nécessité de la gestion des
liens entre les données d'un problème en mémoire.
7. Qualités d’un bon algorithme
Correct: Il faut que le programme exécute
correctement les tâches pour lesquelles il a été
conçu
Complet: Il faut que le programme considère
tous les cas possibles et donne un résultat dans
chaque cas.
Efficace: Il faut que le programme exécute sa
tâche avec efficacité c’est à dire avec un coût
minimal. Le coût pour un ordinateur se mesure
en termes de temps de calcul et d’espace
mémoire nécessaire.
8. Exemple : Calcul de la valeur d’un polynôme
Soit P(X) un polynôme de degré n
P(X) = anXn + an-1Xn-1 + ... + a1X + a0
Où,
n : entier naturel
an, an-1, ..., a1, a0 : les coefficients du polynôme
1ère variante :
début Coût de l’algorithme :
P=0 - (n+1) additions
Pour i de 0 à n faire - (n+1) multiplications
- (n+1) puissances
P = P+ ai*Xi
finpour
fin
9. Exemple : Calcul de la valeur d’un polynôme
2ème variante :
Coût de l’algorithme :
debut - (n+1) additions
Inter=1 - 2(n+1) multiplications
P =0
Pour i de 0 à N faire
P = P+ Inter *ai
Inter = Inter * X
finpour
Fin
10. Exemple : Calcul de la valeur d’un polynôme
3ème variante : Schéma de Horner
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
Nécessité d’estimer le coût d’un algorithme avant de l’écrire
et l’implémenter
12. Définitions
la complexité d'un algorithme est la mesure du
nombre d'opérations fondamentales qu'il effectue
sur un jeu de données.
La complexité est exprimée comme une fonction
de la taille du jeu de données.
La complexité d'un algorithme est souvent
déterminée à travers une description
mathématique du comportement de cet
algorithme.
13. Définitions
On note Dn l’ensemble des données de taille n et T(d) le
coût de l’algorithme sur la donnée d.
On définit 3 types de complexité :
Complexité au meilleur :
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.
Complexité au pire :
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.
Complexité en moyenne :
C'est la moyenne des complexités de l'algorithme sur des jeux de
données de taille n.
14. Définitions
C'est l'analyse pessimiste ou au pire qui est généralement
adoptée.
En effet, de nombreux algorithmes fonctionnent la plupart
du temps dans la situation la plus mauvaise pour eux.
l'analyse au pire des cas donne une limite supérieure de
la performance et elle garantit qu'un algorithme ne fera
jamais moins bien que ce qu'on a établi.
Un algorithme est dit optimal si sa complexité est la
complexité minimale parmi les algorithmes de sa classe.
Même si on s’intéresse quasi-exclusivement à la complexité
en temps des algorithmes. Il est parfois intéressant de
s’intéresser à d’autres ressources, comme la complexité en
espace (taille de l’espace mémoire utilisé), la largeur de
bande passante requise, etc.
15. Notations mathématiques
La notation O est celle qui est le plus communément
utilisée pour expliquer formellement les performances d'un
algorithme.
Cette notation exprime la limite supérieure d'une fonction
dans un facteur constant.
La notation O reflète la courbe ou l'ordre croissance d'un
algorithme.
Les règles de la notation O sont les suivantes :
Les termes constants : O(c) = O(1)
Les constantes multiplicatives sont omises : O(cT ) = cO(T) = O(T)
L'addition est réalisée en prenant le maximum : O(T1) + O(T2) =
O(T1 + T2) = max(O(T1);O(T2))
La multiplication reste inchangée mais est parfois réécrite d'une façon
plus compacte :O(T1)O(T2) = O(T1T2)
16. Exemple
On suppose qu’on dispose d'un algorithme dont le temps
d'exécution est décrit par la fonction T(n) = 3n2+10n+10.
L'utilisation des règles de la notation O nous permet de
simplifier en :
O(T(n)) = O(3n2 + 10n + 10) = O(3n2) = O(n2)
Pour n = 10 nous avons :
Temps d'exécution de 3n2 : 3(10)2 / 3(10)2+10(10)+10 = 73,2%
Temps d'exécution de 10n : 10(10) / 3(10)2+10(10)+10 = 24,4%
Temps d'exécution de 10 : 10 / 3(10)2+10(10)+10 = 2,4%
Le poids de 3n2 devient encore plus grand quand n = 100,
soit 96,7% on peut négliger les quantités 10n et 10.
Ceci explique les règles de la notation O.
17. Classes de complexité
Les algorithmes usuels peuvent être classés en un certain
nombre de grandes classes de complexité.
Les complexités les plus utilisées sont :
Constante : O(1) Accéder au premier élément d'un ensemble de
données
Logarithmique : O(logn) Couper un ensemble de données en deux
parties égales, puis couper ces moitiés en deux parties égales, etc.
Linéaire : O(n) Parcourir un ensemble de données
Quasi-linéaire : O(nlogn) Couper répétitivement un ensemble de
données en deux et combiner les solutions partielles pour calculer la
solution générale
Quadratique : O(n2) Parcourir un ensemble de données en utilisant
deux boucles imbriquées
Polynomiale : O(nP) Parcourir un ensemble de données en utilisant
P boucles imbriquées
Exponentielle : O(2n) Générer tous les sous-ensembles possibles
d'un ensemble de données
19. Calcul de la complexité
1. Cas d'une instruction simple : écriture, lecture,
affectation
Dans le cas d'uns suite d'instructions simples, on considère la
complexité maximale.
La complexité de cette séquence vaut max(O(1),O(1))=O(1).
20. Calcul de la complexité
2. Cas d'un traitement conditionnel
3. Cas d'un traitement itératif : Boucle Tant Que
21. Calcul de la complexité
4. Cas d'un traitement itératif : Boucle Pour
Pour i de indDeb à indFin faire
Traitement
Fin Pour
indFin
∑ O(T
i =indDeb
Traitement )
22. 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.
23. 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
24. 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.
25. 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
26. 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)
27. 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
28. 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
dernière itération taille tableau = 1
La complexité = log2(n)
30. 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...
31. Tri par sélection
Algorithme :
i, j: entier ;
tmp, small : entier ;
t : tableau entier [n] ;
Début
Pour i de 1 à n-1 faire
small←i;
Pour j de i+1 à n faire
Si t[j] < t[small] alors
small ←j ;
Fin si
Fin pour
tmp←t[small];
T(n) = O(n²)
t[small] ←t[i];
t[i] ← tmp;
Fin pour
Fin
32. 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].
33. 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
35. 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.
36. 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.
37. 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
38. Les types de récursivité
Trace d’exécution de la fonction factorielle (calcul de la
valeur de 4!)
39. 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 :
40. 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 :
41. Les types de récursivité
4/ La récursivité imbriquée:
Exemple : La fonction d'Ackermann
42. 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
43. 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
l’ordre des appels récursifs affecte la terminaison d’une
fonction récursive
45. Elimination de la récursivité
Dérécursiver, c’est transformer un
algorithme récursif en un algorithme
équivalent ne contenant pas des appels
récursifs.
Elimination de la récursivité terminale
simple
Rappel : Un algorithme est dit récursif terminal
s’il ne contient aucun traitement après un appel
récursif.
La récursivité terminale simple peut être
remplacée par une solution itérative.
46. Elimination de la récursivité terminale
simple
Algo. récursif Algo. itératif
Procédure ALGOR(X) Procédure ALGOI(X)
Si (COND) Alors Tant que (COND) faire
TRAIT1 TRAIT1
ALGOR(β (X)) X β (X)
Sinon Fin tant que
TRAIT2 TRAIT2
Fin Si Fin
Fin
•X est la liste des paramètres ;
• COND est une condition portant sur X ;
• TRAIT1 est le traitement de base de l'algorithme (dépendant de X) ;
• β(X) représente la transformation des paramètres ;
• TRAIT2 est le traitement de terminaison (dépendant de X).
47. Application : a est diviseur de b ?
Algo. récursif Algo. itératif
Fonction Diviseur (a,b) : Bool Fonction Diviseur (a,b) : Bool
Si (a <=0) Alors Si (a <=0) Alors
Retourner(Faux) Retourner(Faux)
Sinon Sinon
Si (a>=b) Retourner (a=b) Tant que (a<b) Faire
Sinon b b-a
Retourner (Diviseur (a,b-a)) Fin tant que
Fin Si Retourner (a=b)
Fin Si Fin Si
Fin Fin
48. Elimination de la récursivité non
terminale simple
Dans un algorithme récursif non terminal, l’appel
récursif est suivi d’un traitement il reste un
traitement à reprendre après l’appel récursif
Il va falloir donc sauvegarder, sur une pile, le
contexte de l’appel récursif, typiquement les
paramètres de l’appel engendrant l’appel récursif.
La récursivité non terminale simple peut être
remplacée par une solution itérative utilisant une
pile.
49. Elimination de la récursivité non terminale
simple
Algo. récursif Algo. itératif
Procédure ALGOR(X) Procédure ALGOI(X)
Si (COND) Alors Pile.init()
TRAIT1 Tant que (COND) Faire
ALGOR(β (X)) TRAIT1
TRAIT2 Pile.empiler(X)
Sinon X β (X)
TRAIT3 Fin Tant que
Fin Si TRAIT3
Fin Tant que (Non_vide_pile())
Faire
Pile.dépiler(U)
TRAIT2
Fin Tant que
Fin
50. Elimination de la récursivité non terminale
simple
Algo. récursif Algo. itératif
Procédure Procédure ALGOI(X)
AfficherDroiteGauche ( Tab Pile.init()
: Tableau entier, N : Tant que (i<=N) Faire
entier, i : entier)
/* TRAIT1 */
Si (i<=N) Alors
Pile.empiler(Tab[i])
AfficherDroiteGauche
i i+1
(Tab,N,i+1) ;
Ecrire(Tab[i]) ; Fin Tant que
Fin Si /* TRAIT3 */
Fin Tant que (Non_vide_pile())
Faire
TRAIT1 = Ø
Ecrire(Pile.dépiler() )
TRAIT2 = Ecrire(Tab[i])
/* TRAIT2 */
TRAIT3 = Ø
Fin Tant que
Fin
51. Exemple : Tours de Hanoi
Problème :
Le jeu est constitué d’une plaquette de bois où
sont plantées trois tiges numérotées 1, 2 et 3.
Sur ces tiges sont empilés des disques de
diamètres tous différents. Les seules règles du
jeu sont que l’on ne peut déplacer qu’un seul
disque à la fois, et qu’il est interdit de poser un
disque sur un disque plus petit.
Au début, tous les disques sont sur la tige 1
(celle de gauche), et à la fin ils doivent être sur
celle de droite.
52. Exemple : Tours de Hanoi
Résolution:
On suppose que l’on sait
résoudre le problème pour (n-1)
disques. Pour déplacer n disques
de la tige 1 vers la tige 3, on
déplace les (n-1) plus petits
disques de la tige 1 vers la tige
2, puis on déplace le plus gros
disque de la tige 1 vers la tige 3,
puis on déplace les (n-1) plus
petits disques de la tige 2 vers la
tige 3.
53. Exemple : Tours de Hanoi
Algorithme
Procédure Hanoi (n, départ, intermédiaire, destination)
Si n > 0 Alors
Hanoi (n-1, départ, destination, intermédiaire)
déplacer un disque de départ vers destination
Hanoi (n-1, intermédiaire, départ, destination)
Fin Si
Fin
54. Exemple : Tours de Hanoi
Trace d’exécution pour n=3
L’appel à Hanoi(3,1,2,3) entraîne l’affichage de :
1. Déplace un disque de la tige 1 vers la tige 3
2. Déplace un disque de la tige 1 vers la tige 2
3. Déplace un disque de la tige 3 vers la tige 2
4. Déplace un disque de la tige 1 vers la tige 3
5. Déplace un disque de la tige 2 vers la tige 1
6. Déplace un disque de la tige 2 vers la tige 3
7. Déplace un disque de la tige 1 vers la tige 3
55. Exemple : Tours de Hanoi
Calcul de la complexité :
On compte le nombre de déplacements de
disques effectués par l’algorithme Hanoi
invoqué sur n disques.
On trouve :
T(n) = T(n-1) + 1 + T(n-1)
T(n) = 2T(n-1) + 1
T(n) = 2n – 1
Complexité exponentielle
56. 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.
57. Exemple 1
Recherche 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)
Sinon
retourner (k2) T(n) = 2 T(n/2) + cte
FinSi
FinSi
Fin
58. 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 :
Le développement de cette équation donne :
r = ae+bf ; s = ag+bh; t = ce+df et u = cg+dh
59. Exemple2 : multiplication de matrices carrées
Chacune de ces quatre opérations correspond à :
deux multiplications de matrices carrées de taille n/2
2T(n/2)
et une addition de telles matrices n2/4
A partir de ces équations on peut dériver un
algorithme « diviser pour régner » dont la
complexité est donnée par la récurrence :
T(n) = 8T(n/2)+O(n2)
60. Analyse des algorithmes « Diviser pour Régner »
On peut souvent donner une relation de récurrence qui décrit
le temps d'exécution d’un algorithme « diviser pour régner »
en fonction des temps d'exécution des appels récursifs
résolvant les sous-problèmes associés de taille moindre.
Cette récurrence se décompose suivant les trois étapes du
paradigme de base :
Si la taille du problème est suffisamment réduite, n ≤ c pour
une certaine constante c, la résolution est directe et consomme
un temps constant O(1).
Sinon, on divise le problème en a sous-problèmes chacun de
taille 1/b de la taille du problème initial. Le temps d'exécution
total se décompose alors en trois parties :
D(n) : le temps nécessaire à la division du problème en sous-
problèmes.
aT (n/b) : le temps de résolution des a sous-problèmes.
C(n) : le temps nécessaire pour construire la solution finale à partir
des solutions aux sous-problèmes.
61. Analyse des algorithmes « Diviser pour Régner »
La relation de récurrence prend alors la forme :
Soit la fonction f (n) la fonction qui regroupe D(n)
et C(n). La fonction T(n) est alors définie :
T(n) = a.T(n / b) + f (n)
T (n) = a.T (n / b) + c.nk
62. Résolution des récurrence des algorithmes
« Diviser pour Régner »
Théorème de résolution de la récurrence :
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 )
Résolution de la relation de récurrence pour
l’exemple de la multiplication de matrices :
T(n) = 8 T(n/2) + O(n2)
a = 8 , b = 2, k = 2 a > bk
Logba = 3
T(n) = O(n3)
63. 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)
64. 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
Retourner (Faux)
FinSi
65. 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)
66. 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)
67. 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)
68. 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
69. 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 ) − ... − ak T (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 + c3 r3 + ... + c r
n n n n
m m
Cette solution est en général exponentielle
70. Autres résolutions de récurrence
Exemple : La suite de Fibonacci
T(n) = T(n-1) + T(n-2)
On pose P(x) = X² - X – 1
r = 1 + 5 et r = 1 − 5
1 2
2 2 1 + 5 n 1− 5
n
T (n) = a r1n + b r2n = a
2 +b
2
1 + 5 n
T (n) = Ο
2
72. 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).
73. 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 si
FIN
T(n) = 2 T(n/2) + Tfusion(n)
74. 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; i←1 ;j←1 ;
Tantque (i+j-1 <> N+M) faire
Si (i ≤ N) alors
si (j ≤ M) alors
si (T1[i] < T2[j]) alors
T[i+j-1]←T1[i] ; i←i+1 ;
sinon
T[i+j-1]←T2[j] ; j←j+1 ;
Fin si
sinon
T[i+j-1]←T1[i]; i←i+1;
Fin si
sinon T[i+j-1]←T2[j] ;
j←j+1 ; Fin si
FinTanque Retourner T ;FIN Tfusion(n) = O(n)
75. 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.
76. 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)
77. 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é !
78. 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
79. 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)
81. 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.
82. 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
83. 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.
84. 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.
85. 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.
86. 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.
87. 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
88. 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
89. 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.
90. 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
91. 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
93. 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
95. Parcours préfixé utilisant une pile
Le programme suivant est une version dérécursivée
utilisant une pile explicite et permettant le parcours préfixé
d’un arbre binaire.
Algorithme Préfixe (racine : ^Nœud)
P : pile
Empiler (P, racine)
Tant que (Non_Vide(P))
racine = depiler (P)
Empiler (P,racine^.FilsD)
Empiler (P,racine^.FilsG)
Fin Tant que
Fin
98. Arbre binaire de recherche
Un arbre binaire de recherche est un arbre binaire dans
lequel chaque nœud est supérieur à son fils gauche et
inférieur à son fils droit et il n’y a pas de nœuds égaux.
Un arbre binaire de recherche est intéressant puisqu’il est
toujours possible de connaître dans quelle branche de
l’arbre se trouve un élément et de proche en proche le
localiser dans l’arbre. On peut aussi utiliser un arbre binaire
de recherche pour ordonner une liste d’éléments.
99. Recherche dans un arbre binaire de recherche
Algorithme Chercher (racine : ^Nœud , X : élément)
Si ( racine = Nil) alors
retourner 0
Sinon
Si( racine^.info = X ) alors
retourner 1
Else
Si (X < racine^.info ) alors
retourner Chercher(racine^.FilsG, X)
Else
retourner Chercher (racine^.FilsD, X)
Finsi
FinSi
FinSi
Fin
100. Insertion dans un arbre binaire de recherche
L’élément à ajouter est inséré là où on l’aurait trouvé s’il avait
été présent dans l’arbre. L’algorithme d’insertion recherche
donc l’élément dans l’arbre et, quand il aboutit à la conclusion
que l’élément n’appartient pas à l’arbre (il aboutit à la terre),
il insère l’élément comme fils du dernier nœud visité.
Algorithme Insérer (racine : ^Nœud, X : élément )
Si (racine = nil) alors
« ajouter un nœud pour X à cet endroit»
Sinon
Si (racine^.info = X)
Ecrire ("l’élément à insérer existe déjà dans l’arbre")
Sinon
Si (X> racine^.info)
Insérer (racine^.FD, X)
Sinon
Insérer (racine^.FG, X)
Fin
101. Suppression dans un arbre binaire de recherche
1er cas : l’élément à supprimer n’a pas de
fils il est terminal et il suffit de le
supprimer
102. Suppression dans un arbre binaire de recherche
2ème cas : l’élément a un fils unique on
supprime le nœud et on relie son fils à
son père
103. Suppression dans un arbre binaire de recherche
3ème cas : l’élément à supprimer a deux
fils on le remplace par son successeur
qui est toujours le minimum de ses
descendants droits.