Cours Algorithmique Et Complexite Avancee
Cours Algorithmique Et Complexite Avancee
Cours Algorithmique Et Complexite Avancee
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.
begin
Coût de l’algorithme:
P=0
- (n+1) additions
for k=0 to n do
- (n+1)
P = P+ ak*XK
multiplications
endfor
- (n+1) puissances
end
begin
XP=1 ; P =0
for K=0 to N do
P = P+ XP*ak
XP = XP * X
endfor
end Coût de l’algorithme:
- (n+1) additions
- 2(n+1)
multiplications
P(x) = (….(((anx+an-1)x+an-2)x+an-3)…..)x+a0
l’algorithme
begin
P = an
for k = n-1 to 0 step –1 do
P = P*X + ak
endfor
end
Coût de l’algorithme:
- (n) additions …. Peut-on faire mieux ?
- (n)
multiplications
i 0 1 2 3 4 5 6 7 8 9
A 31 -41 59 26 -53 58 97 -93 -23 84
L'idée la plus simple est de calculer toutes les sommes et de rechercher la plus
grande, par un algorithme du style:
Une meilleure idée consiste à ne pas recalculer systématiquement les sommes mais à
utiliser la relation:
j 1 j
Algorithme_2 (int n , int a[ ])
k i
a k
k i
a k a j 1
Maximum=0;
For i=0 to n-1 do
D'où le nouveau programme: Somme=0
For j=i to n-1 do
Somme= Somme + a[j]
If (somme > maximum) then
Maximum = Somme
Endfor
Endfor
Return Maximum
End_algorithme
3.3 - Algorithme_3 :
Complexité de l’algorithme :
Cet algorithme est de type « diviser pour régner ». Le nombre de comparaisons
effectuées à l’intérieur de la fonction récursive est égal à (d-g) fois. Il est donc O(n).
L’équation récurrente de sa complexité est donc:
On démontrera plus loin en utilisant le théorème qui sera présenté au chapitre suivant
que :
T(n) = O(n log(n)).
Car :
T(n) = 2T(n/2) + O(n)
Mais on peut encore mieux faire. L'idée cette fois est de parcourir une seule fois le
tableau et de maintenir simultanément deux maximums : la plus grande somme
« max1 » et un « max2 » qui est remis à zéro chaque fois qu’il devient négatif. La plus
grande somme recherchée est égale soit à « max1 » soit à « max2 » d'où l'algorithme:
Algorithme 1 2 3 4
Temps 41000 ans 1,7 semaines 11 secondes 0,48 seconde
Algorithme
y=x
For i =2 to n do
y=y*x
End_for
Historique :
Cette méthode a été présentée 200 avant J.C. en Inde (ou en Chine), mais il
semblerait qu’il ait fallu attendre un millénaire avant que cette méthode ne soit connue
en dehors de l’Inde [Knuth].
Algorithme Illustration avec x23
1. Écrire n sous forme binaire 1. n = 23 = 10111
2. Remplacer chaque : 1 0 1 1 1
– « 1 » par la paire de lettres « 2. SX S SX SX SX
SX » ;
– « 0 » par la lettre « S ».
3. Éliminer la paire « SX » 3. S SX SX SX
la plus à gauche.
4. Résultat : Une chaîne de caractères 4. Résultat : SSXSXSX
contenant les lettres S et X.
5. Calcul de xn : 5. Nous partons de x et nous obtenons
- partir de x et parcourir la chaîne successivement :
Explication de la méthode
p
– Écriture binaire de n : n ai 2 i
i 0
a
Dans tous les cas nous avons : y j 1 y 2j x j 1
ai 2 i
y 0 x i 0 xn
Complexité (coût)
Notes :
- les nombres dont la représentation binaire ont exactement p chiffres forment
exactement l’intervalle : [2 p 1 ,2 p 1]
- Nombres de chiffres dans l’écriture binaire de n = 1+[log2 n]. Notons (n) le
nombre de « 1 » dans l’écriture binaire de n.
Nombre d’opérations effectuées =
Autre schéma de calcul : x2, x3, x6, x12 et faisons x15 = x12 x x3
Nous obtenons ainsi x15 en 5 multiplications
la méthode binaire n’est donc pas optimale (c’est-à-dire que l’on peut faire mieux).
Algorithme
x si n 1
x x n 1.x
n
si n premier
( x p ) n ' si n p.n' avec p le ppdp de n
ppdp : le plus petit diviseur premier
Illustration avec n = 15
1. 15 = 3x5, 3 étant le plus petit diviseur (facteur) premier de 15. Donc x15 = (x3)5.
Nous avons affaire à un problème simple, que tout le monde sait résoudre, mais
qu’il est très difficile de résoudre efficacement...
N 1 N 1 2jux / N
f(x) F(u)e
2 jux / N
F (u ) 1 / N f ( x )e et
x 0 u 0
pin= 6.28*u/n;
Fu=0
For k = 0 to n do
Fu = Fu + f(k)*exp(-j*pin*k) // opérations complexes
Endfor
Coût de l’algorithme:
- (n+1) additions complexes
- (n+1) multiplications complexes
- (n+1) multiplications réelle
- (n+1) calcul d’une exponentielle
M 1 M 1
u(2x1)
F(u )1/ 2
1/ M
x0
u(2x)
f(2x)W2M 1/ M
x0
f(2x1)W2M
TF de 2
points
TF de 4
points
TF de 8 points
Coût de l’algorithme:
6- Conclusion
- Existence de plusieurs solutions pour un problème donné
- Ces solutions peuvent être correctes mais n’ont pas la même efficacité (coût)
- Avant de proposer un algorithme, il faut estimer son coût
- Et se poser la question de l’existence d’une autre solution moins coûteuse
7- Définition de la complexité
7.1 - Définition (Complexité):
8- Définition de l’optimalité :
Un algorithme est dit optimal si sa complexité est la complexité minimale parmi les
algorithmes de sa classe.
9.3 - Algorithme
9.4 - Complexité
t
j 2
j
A[i+1]=A[i] c5 n
(t
j 2
j 1)
i=i-1 c6 n
End_while (t
j 2
j 1)
A[i+1]=clé c7
Endfor n-1
Le temps d’exécution total de l’algorithme est alors la somme des coûts élémentaires:
n n n
T (n) c1 n c 2 (n 1) c 3 (n 1) c 4 t j c 5 (t j 1) c 6 (t j 1) c 7 (n 1)
j 2 j 2 j 2
Complexité au meilleur :
le cas le plus favorable pour l’algorithme TRI-INSERTION est quand le tableau est déjà
trié. Dans ce cas tj = 1 pour tout j.
Complexité au pire :
Le cas le plus défavorable pour l’algorithme TRI-INSERTION est quand le tableau est déjà trié
dans l’ordre inverse. Dans ce cas tj = j pour tout j.
n(n 1) n(n 1) n( n 1)
n n n
Rappel : j 1
j donc j 2
j 1 et j 2
( j 1)
2 2 2
Supposons que l’on applique l’algorithme de tri par insertion à n nombres choisis au
hasard. Quelle sera la valeur de tj ? C’est-à-dire, où devra-t-on insérer A[ j] dans le
sous-tableau A[1.. j-1] ?
En moyenne, la moitié des éléments de A[1.. j-1] sont inférieurs à A[ j], et l’autre moitié
sont supérieurs. Donc t j = j/2. Si l’on reporte cette valeur dans l’équation définissant
T(n), on obtient, comme dans le cas pire, une fonction quadratique en n.
Ordre de grandeur :
Ce qui nous intéresse vraiment, c’est l’ordre de grandeur du temps d’exécution. Seul le
terme dominant de la formule exprimant la complexité nous importe, les termes
d’ordres inférieurs n’étant pas significatifs quand n devient grand. On ignore également
le coefficient multiplicateur constant du terme dominant. On écrira donc, à propos de la
complexité du tri par insertion :
Complexité au meilleur = (n).
Complexité au pire = (n2).
Complexité en moyenne = (n2).
Classes de complexité
Les algorithmes usuels peuvent être classés en un certain nombre de grandes classes
de complexité :
1. O(logn) : Les algorithmes sub-linéaires dont la complexité est en général en
O(logn).
2. O(n) : Les algorithmes linéaires en complexité O(n)
3. O(nlogn) : et ceux en complexité en O(nlogn)
4. O(nk) : Les algorithmes polynomiaux en O(nk) pour k > 3
5. Exp(n) : Les algorithmes exponentiels
Les trois premières classes sont considérées rapides alors que la quatrième est
considérée lente et la cinquième classe est considérée impraticable.
n.Log(n)
Log(n)
10- Récursivité
De l’art et la manière d’élaborer des algorithmes pour résoudre des problèmes qu’on
ne sait pas résoudre soi-même !
10.1 -Définition
Une définition récursive est une définition dans laquelle intervient ce que l’on veut
définir.
1 si n0
x n n 1
x.x si n 1
Puissance (x, n)
Begin
If (n = 0) then
return 1
Else
return (x*Puissance (x, n-1))
End
Fibonacci ( n)
If ( n=0 or n =1 )
Return 1
Else
Return (Fibonacci (n-2) + Fibonacci (n-1) )
End_Fibonacci
Des définitions sont dites mutuellement récursives si elles dépendent les unes des
autres. Ça peut être le cas pour la définition de la parité :
vrai si n0 faux si n0
pair ( n) et impair (n)
impair (n 1) sin on pair (n 1) sin on
Principe et intérêt :
Difficultés :
Diviseur (a,b)
If (a <=0) then Erreur
Else
If (a>=b) return (a=b)
Else
Return (Diviseur (a,b-a) )
End_Diviseur
La suite des valeurs b, b-a, b-2a, etc. est strictement décroissante, car a est
strictement positif, et on finit toujours par aboutir à un couple d’arguments (a,b) tel que
b-a est négatif, cas défini explicitement.
Le 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.
Résolution :
Algorithme
Complexité
Un algorithme est dit récursif terminal s’il ne contient aucun traitement après un appel
récursif.
Exemple :
Algorithme P(U) – U est la liste des paramètres ;
If ( Condition(U) ) Then – C est une condition portant sur U ;
Traitement_base (U); – (U) représente la transformation
P((U)) ; des paramètres ;
Else
Traitement_terminaison(U)
;
Endif
End_Algorithme
Algorithme P’(U)
While ( Condition(U) ) do
Traitement_base(U);
U (U)
End_while
Traitement_terminaison;
End_Algorithme
?
If (a <=0) then Erreur
Else
If (a>=b) return (a=b)
Else
Return (Diviseur (a,b-a) )
End_Diviseur
?
If ( N = 0)
Return 1 ;
Else
Return N*Factoriel (N-1) ;
End_Factoriel
Dans l’algorithme suivant la récursivité n’est pas terminale puisque l’appel récursif est
suivi d’un traitement. Cela implique qu’il reste un traitement à reprendre
ultérieurement. 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.
Algorithme récursif :
Algorithme Q(U)
If ( Condition(U) ) Then
Traitement_A(U);
Q((U));
Traitement_B(U)
Else
Traitement_terminaison(U)
Endif
End_Algorithme
Endwhile
End_Algo
Les programmes itératifs sont souvent plus efficaces, mais les programmes récursifs
sont plus faciles à écrire.
Il est toujours possible de dérécursiver un algorithme récursif.
Algorithme naïf
Dans la suite nous supposerons que n est une puissance de 2. Décomposons les
matrices A, B et C en sous-matrices de taille n/2 x n/2. L’équation C = AB peut alors
se récrire :
r s a b e g
*
t u c d f h
En développant cette équation, nous obtenons :
r = ae+bf ; s = ag+bh; t = ce+df et u = cg+dh:
A partir de ces équations on peut aisément dériver un algorithme « diviser pour régner
» dont la complexité est donnée par la récurrence :
T(n) = 8T(n/2)+(n2)
: f ( g ) g ( f )
Cela signifie que g « est majorée par f »
Remarques :
On peut considérer que :
(g ) est une borne supérieure
(g ) est une borne inférieure
(g ) est une borne exacte. Celle-ci est donc plus précise que les précédentes
Fibonacci ( n)
If ( n=0 or n =1 )
Return 1
Else
Return (Fibonacci (n-2) + Fibonacci (n-1))
End_Fibonacci
Soit T(n) le nombre d’additions effectuées par cet algorithme. T(n) vérifie les équations
suivantes :
Si on reprend la formule :
n
f (i )
u n a (u 0
n
) et on remplace a par 2 et f(i) par 1 on trouve le même résultat (car
i 1 ai
la somme d’une suite géométrique=u0(qn-1)/(q-1) )
n n
1 1
u n 2 n (u 0 i
) 2 n
( i
1) 2 n 1
i 1 2 i 0 2
Donc :
1 5 n
S (n) T (n)
2
Exemple-2 :
Soit la suite définie par :
un 3un1 4un2 si n 2
u0 0 et u1 1
Son polynôme caractéristique est :
P( x) x 2 3 x 4
Ses racines sont : r1 4 et r2 1
La solution de l’équation de récurrence est donc :
u( )0 a b 0 1 1
a et b
u(1) 4a b 1 5 5
1 n
d’où : u ( n) (4 (1) n ) (4 n )
5
Soient a >=1 et b > 1 deux constantes, soit f (n) une fonction et soit T(n) une
fonction définie pour les entiers positifs par la récurrence :
T ( n) a.T (n / b) f (n)
T(n) peut alors être bornée asymptotiquement comme suit :
Si la forme de f(n) est : Alors le coût est :
(log a )
1 f ( n) ( n b
) pour un > 0 T (n) (n log a ) b
Donc : a = 8,
b = 2 logb a = 3
On est donc dans le cas 1 du théorème (avec = 1), l’algorithme a donc une
complexité en (n3)
Exemple-2 :
n
T (n) 9T n
3
On est dans le cas : a=9 , b=3 et f(n)=n
9
n log a n log 9 n 2 (n 2 ) f (n) n n 21 n log
b 3 3
avec 1
On applique le cas 1 : T (n) (n 2 )
Nb-additions Nb-Secondes
Non Non
n Récursif Récursif Récursif Récursif Récursif
20 19 10945 0,0000019 0,0010945 2*10-5 mn
30 29 1346260 0,0000029 0,134626 2*10-3 mn
40 39 165580140 0,0000039 16,558014 3*10-1 mn
50 49 3185141889 0,0000049 318,514189 5 mn
100 500 000 années
..\Exemples\Fibonacci.cpp
Algorithme :
Repeat
No_permutation := True
For I := 1 To N-1 Do
If (A(I) > A (I+1)) Then
X :=A(I) ; A(I) := A(I+1) ; A(I+1) := X
No_Permutation := False
Endif
Endfor
Until ( No_permutation)
Quelle est la complexité de cet algorithme ?
44 12 18 6
44 55 12 42 94 18 6 67
L’algorithme de tri par fusion est construit suivant le paradigme « diviser pour régner
»:
La principale action de l’algorithme du tri par fusion est justement la fusion des deux
listes triées.
La fusion
Le principe de cette fusion est simple: à chaque étape, on compare les éléments
minimaux des deux sous-listes triées, le plus petit des deux étant l’élément minimal de
l’ensemble on le met de côté et on recommence. On conçoit ainsi un algorithme
« Fusionner » qui prend en entrée un tableau A et trois entiers, p, q et r, tels que p<=q
< r et tels que les tableaux A[p..q] et A[q+1..r] sont triés.
Le tri
Fusionner (A, p, q, r) // Fusionner les 2 sous-tableaux A(pq) et A(q+1r)
i=p // les 2 sous tableaux sont supposés triés
j=q+1 ; k=1
while (i<=q et j<=r ) do
if ( A[i] < A[ j] ) then
C[k] = A[i] ; i=i+1
else
C[k]=A[ j] ; j=j+1
endif
Tri_Fusion(A, p, r)
If (p<r ) then
q= (p+r)/2
Tri_Fusion(A, p, q)
Tri_Fusion(A,q+1,r)
Fusionner(A,p,q,r)
Endif
Complexité de la fusion
Étudions les différentes étapes de l’algorithme :
– les initialisations ont un coût constant (1) ;
– la boucle While de fusion s’exécute au plus r-p fois, chacune de ses itérations étant
de coût constant, d’où un coût total en O(r-p) ;
– les deux boucles While complétant C ont une complexité respective au pire de q-
p+1 et de r-q, ces deux complexités étant en O(r-p) ;
– la recopie finale coûte (r-p+1).
Par conséquent, l’algorithme de fusion a une complexité en (r-p).
Le tri rapide est fondé sur le paradigme « diviser pour régner », tout comme le tri
fusion, il se décompose donc en trois étapes :
Diviser : Le tableau A[p..r] est partitionné (et réarrangé) en deux sous-tableaux non
vides, A[p..q] et A[q+1..r] tels que chaque élément de A[p..q] soit inférieur ou égal à
chaque élément de A[q+1..r].
L’indice q est calculé pendant la procédure de partitionnement.
Régner : Les deux sous-tableaux A[p..q] et A[q+1..r] 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 A[p..r] est déjà trié !
20.2 -Algorithme
Tri_Rapide (A, p, r)
If (p < r ) then
Partionner (A, p, r)
x = A(p)
i = p-1
j= r+1
while (1)
repeat { j=j-1 } until A(j) <= x
repeat { i =i+1 } until A(i) >= x
if ( i < j )
permuter (A(i), A(j))
else return j
End_Partionner
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 (n) et que T(1) = (1), la récurrence pour le temps
d’exécution est :
T (n) T (n 1) (n)
et par sommation on obtient :
n n
T ( n ) ( k ) ( k ) ( n 2 )
k 1 k 1
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) (n)
ce qui donne d’après le théorème de résolution des récurrences :
T ( n) ( n log n)
21- 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.
1 2 1 2
5 5
3 4 3 4
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,
comme le sommet 4 de l’exemple, 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.
Cycle : Dans un graphe non orienté G = (S,A), une chaîne (u0,u1,…., uk) forme
un cycle si k >= 3 et si u0 = uk. Ce cycle est élémentaire si les sommets
u1, ..., uk sont distincts. Un graphe sans cycle est dit acyclique.
Graphe connexe : Un graphe non orienté est connexe si chaque paire de sommets
est reliée par une chaîne. Les composantes connexes d’un graphe
sont les classes d’équivalence de sommets induites par la relation « est
accessible à partir de ». (figure-5.1b)
Graphe fortement connexe : Un graphe orienté est dit fortement connexe si chaque
sommet est accessible à partir de n’importe quel autre. Les composantes
22- Arbres
Un graphe non orienté acyclique est une forêt et un graphe non orienté connexe
acyclique est un arbre. La figure-5.2 présente 3 graphes : un graphe qui n’est ni un
arbre ni une forêt car contenant un cycle (a), un graphe qui est une forêt mais pas un
arbre (b) et un arbre (c).
1 2 1 2
6 5 6 5
3 4 3 4
7 7
a-6. G est acyclique, mais si une arête quelconque est ajoutée à A, le graphe
résultant contient un cycle.
75
3 75 90
30 80
30
12 60 3 90
12 60
Ancêtre : Soit x un noeud (ou sommet) d’un arbre T de racine r. Un noeud quelconque
y sur l’unique chemin allant de r à x est appelé ancêtre de x.
Feuille ou nœud externe (ou terminal) : Un noeud sans fils est un noeud terminal ou
une feuille. Un noeud qui n’est pas une feuille est un nœud interne. Si y est un
ancêtre de x, alors x est un descendant de y.
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.
75 75
30 80 30 80
12 60 60 12 90
90
3 3
Figure-5.4 : Exemple d’arbres (enracinés) qui ne diffèrent que s’ils sont ordonnés
Arbre binaire complet : Dans un arbre binaire complet chaque noeud est soit une
feuille, soit de degré deux. Aucun noeud n’est donc de degré un.
Un arbre k-aire est une généralisation de la notion d’arbre binaire où chaque noeud
est de degré au plus k et non plus simplement de degré au plus 2.
Nous ne considérons ici que des arbres ordonnés. Les parcours permettent
d’effectuer tout un ensemble de traitement sur les arbres.
Algorithme ParPro(A)
If A n’est pas réduit à une feuille then
for tous les fils u de racine(A) do
ParPro(u)
End_PP
Dans un parcours en largeur, tous les noeuds à une profondeur i doivent avoir été
visités avant que le premier noeud à 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 Parcours_Largeur(A)
F : File d’attente
F.Put (racine(A))
While F != vide Do
u=F.Get()
Afficher (u)
For « chaque fils v de » u do
F.Put (v)
End_PL
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.
Algorithme PP(G)
for chaque sommet u de G do
couleur[u]=Blanc
endfor
for chaque sommet u de G do
If couleur[u] = Blanc then
VisiterPP(G, u, couleur)
Endif
Endfor
End_PP
Algorithme VisiterPP(G, s, couleur)
couleur[s]=Gris
for chaque voisin v de s do
if couleur[v] = Blanc then
VisiterPP(G, v, couleur)
couleur[s]=Noir
End_VisiterPP
25- Définition
Un arbre ou arborescence binaire est un graphe qui admet une racine et sans cycle et
dont chaque nœud admet deux fils : fils droit et fils gauche.
On peut représenter un arbre binaire sous la forme d’une liste chaînée mais non
linéaire.
On ne peut réaliser ces différents parcours qu’en utilisant une pile, puisqu’on est obligé
de commencer le parcours par la racine et d’empiler les nœuds dont on n’a pas encore
rencontré le fils gauche. La pile peut être utilisée d’une manière explicite ou bien au
moyen d’une procédure récursive.
Algorithme Programme en C
Algorithme Préfixe(struct node Void PréfixeArbre (struct arbre *a)
A) { Préfixe (a -> racine) ; }
If (A != Nil) then
AfficheRacine(A) void Préfixe ( struct node *pere)
Préfixe (FilsGauche(A)) { if (pere != null)
Préfixe (FilsDroit(A)) { /* traiter la donnée pere -> data */
End_If Préfixe ( pere -> left);
End_Préfixe Préfixe (pere -> right) ;
}
}
Algorithme Programme en C
Algorithme Infixe(A) Void InfixeArbre (struct arbre *a)
If (A != Nil) then { Infixe (a -> racine) ; }
Algorithme Programme en C
Algorithme Postfixe(A) Void PostfixeArbre (struct arbre *a)
If (A != Nil) then { Postfixe (a -> racine) ; }
Postfixe (FilsGauche(A))
Postfixe (FilsDroit(A)) void Postfixe ( struct node *pere)
AfficheRacine(A) { if (pere != null)
End_If { Postfixe ( pere -> left);
End_Préfixe Postfixe (pere -> right) ;
/* traiter la donnée pere -> data */
}
}
Le programme suivant est une version dérécursivée utilisant une pile explicite et
permettant le parcours infixe d’un arbre binaire.
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.
75
30 80
12 60 90
La recherche d’un élément dans un arbre binaire de recherche est très facile si on
profite du caractère récursif d’un arbre binaire.
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 noeud visité.
La suppression d’un élément est plus compliquée que l’insertion, car si cet élément est
un père à qui confier ses fils ?
Il existe trois cas possibles :
15
1er cas : l’élément à supprimer n’a pas de fils 15il est terminal et il suffit de le
supprimer 5 16 5 16
3 12 20 3 12 20
10 13 10
4 18 23 18 23
6 6
Notes de cours : Algorithmique & Complexité 127/136 Hamrouni Kamel
7 7
2ème cas : l’élément a un fils unique on supprime le nœud et on relie son fils à son
père
15 15
5 16 5 20
3 12 3 12
20
18 23
10 13 10 13
4 18 23
6 6
7 7
15
15
5 16 6 16
3 12 20 3 12 20
10 13
18 23 10 13
4 18 23
4
6
7
Si h est la hauteur de l’arbre, on peut aisément montrer que tous les algorithmes
précédents ont une complexité en O(h). Malheureusement, un arbre binaire
quelconque à n noeuds a une hauteur comprise, en ordre de grandeur, entre log2 n et
n. Pour éviter les cas les plus pathologiques, on s’intéresse à des arbres de
recherches équilibrés.
15 6
6
25 12
3 12 17 35 15
17
25
35
Les opérations de mise à jour d’un arbre « rouge et noir » sont un peu plus
compliquées que dans le cas d’un arbre binaire classique car il faut veiller à ne pas
violer les règles que doit vérifier un arbre « rouge et noir ».
Pour cela, on aura besoin de changer la couleur des nœuds et d’effectuer des
déplacements appelés « rotations ».
26.4.2 Rotations
Pour préserver les propriétés d’un arbre binaire de recherche « rouge et noir » lors de
la mise à jour, on aura besoin d’effectuer des rotations :
x C A y
Rotation gauche (y)
(y)
A B B C
26.4.4 Suppression
On peut montrer par induction que le sous-arbre (d’un arbre rouge et noir) enraciné en
un noeud x quelconque contient au moins 2 hn(x) noeuds internes, où hn(x) est la
hauteur noire de x. Sachant que la hauteur est toujours inférieure au double de la
hauteur noire on en déduit la borne donnée par le théorème.
Ce théorème montre bien que les arbres rouge et noir sont relativement équilibrés : la
hauteur d’un arbre « rouge et noir » est au pire le double de celle d’un arbre binaire
parfaitement équilibré.
Toutes les opérations sur les arbres rouge et noir sont de coût O(h), c’est-à-dire
O(logn), ce qui justifie leur utilisation par rapport aux arbres binaires de recherche
classiques.