Vous êtes sur la page 1sur 25

L'informatique au lyce

Chapitre 8

http://ow.ly/35Jlt

Chapitre 8
Structures de donnes
avances
Une structure de donnes est une organisation logique des donnes permettant de simplifier ou
d'acclrer leur traitement.

8.1.

Pile

En informatique, une pile (en anglais stack) est une structure de donnes fonde sur le principe
dernier arriv, premier sorti (ou LIFO pour Last In, First Out), ce qui veut dire que les derniers
lments ajouts la pile seront les premiers tre rcuprs.
Le fonctionnement est donc celui d'une pile d'assiettes : on ajoute des assiettes sur la pile, et on
les rcupre dans l'ordre inverse, en commenant par la dernire ajoute.

Primitives
Voici les primitives communment utilises pour manipuler des piles :
empiler : ajoute un lment sur la pile. Terme anglais correspondant : Push .
dpiler : enlve un lment de la pile et le renvoie. En anglais : Pop
vide : renvoie vrai si la pile est vide, faux sinon
remplissage : renvoie le nombre d'lments dans la pile.

Applications

Didier Mller

Dans un navigateur web, une pile sert mmoriser les pages Web visites. L'adresse de
chaque nouvelle page visite est empile et l'utilisateur dpile l'adresse de la page
prcdente en cliquant le bouton Afficher la page prcdente .
L'valuation des expressions mathmatiques en notation post-fixe (ou polonaise
inverse) utilise une pile.
La fonction Annuler la frappe (en anglais Undo ) d'un traitement de texte
8-1

janvier 2015

Structures de donnes avances

mmorise les modifications apportes au texte dans une pile.


Un algorithme de recherche en profondeur utilise une pile pour mmoriser les nuds
visits.
Les algorithmes rcursifs admis par certains langages (LISP, Algol, Pascal, C, etc.)
utilisent implicitement une pile d'appel. Dans un langage non rcursif (FORTRAN par
exemple), on peut donc toujours simuler la rcursion en crant les primitives de gestion
d'une pile.

Exercice 8.1
Implmentez en Python une classe pile avec ces quatre mthodes, ainsi qu'une mthode
afficher qui liste tous les lments de la pile, du dernier entr au premier entr.

8.2.

File

Une file (queue en anglais ) est une structure de donnes base sur le principe Premier entr,
premier sorti , en anglais FIFO (First In, First Out), ce qui veut dire que les premiers lments
ajouts la file seront les premiers tre rcuprs. Le fonctionnement ressemble une file
d'attente : les premires personnes arriver sont les premires personnes sortir de la file.

Primitives
Voici les primitives communment utilises pour manipuler des files :
ajouter : ajoute un lment dans la file. Terme anglais correspondant : Enqueue
enlever : renvoie le prochain lment de la file, et le retire de la file. Terme anglais
correspondant : Dequeue
vide : renvoie vrai si la file est vide, faux sinon
remplissage : renvoie le nombre d'lments dans la file.

Applications

En gnral, on utilise des files pour mmoriser temporairement des transactions qui
doivent attendre pour tre traites.
Les serveurs d'impression, qui doivent traiter les requtes dans l'ordre dans lequel elles
arrivent, et les insrent dans une file d'attente (ou une queue).
Certains moteurs multitches, dans un systme d'exploitation, qui doivent accorder du
temps-machine chaque tche, sans en privilgier aucune.
Un algorithme de parcours en largeur utilise une file pour mmoriser les nuds visits.
On utilise aussi des files our crer toutes sortes de mmoires tampons (en anglais
buffers).

Exercice 8.2
Implmentez en Python une classe file avec ces quatre mthodes, ainsi qu'une mthode
afficher permettant de voir tous les lments de la file, du premier entr au dernier entr.

Files priorits
Une file priorits est un type abstrait lmentaire sur laquelle on peut effectuer trois oprations :
insrer un lment
supprimer le plus grand lment
tester si la file priorits est vide ou pas
Les principales implmentations de ces files priorits sont le tas (voir 8.9), le tas binomial et
le tas de Fibonacci.

Didier Mller

8-2

janvier 2015

L'informatique au lyce

8.3.

Donald Knuth,
professeur mrite
Stanford, auteur
de l'ouvrage de
rfrence sur
l'algorithmique, en
plusieurs volumes,
intitul The Art
of Computer
Programming ,
considre les
arbres comme la
structure la plus
fondamentale de
l'informatique .

Chapitre 8

Arbres

Un arbre est un graphe sans cycle, o des nuds sont relis par des artes. On distingue trois
sortes de nuds :
les nuds internes, qui ont des fils ;
les feuilles, qui n'ont pas de fils ;
la racine de l'arbre, qui est l'unique nud ne possdant pas de pre.
Traditionnellement, on dessine toujours la racine en haut et les feuilles en bas.

La profondeur d'un nud est la distance, i.e. le nombre d'artes, de la racine au nud. La hauteur
d'un arbre est la plus grande profondeur d'une feuille de l'arbre. La taille d'un arbre est son nombre de
nuds (en comptant les feuilles ou non).
Les arbres peuvent tre tiquets. Dans ce cas, chaque nud possde une tiquette, qui est en
quelque sorte le contenu du nud. L'tiquette peut tre trs simple (un nombre entier, par
exemple) ou plus complexe : un objet, une instance d'une structure de donnes, etc.
Les arbres sont en fait rarement utiliss en tant que tels, mais de nombreux types d'arbres avec
une structure plus restrictive existent et permettent alors des recherches rapides et efficaces. Nous en
reparlerons bientt.

8.3.1.

Parcours

Parcours en largeur
Le parcours en largeur correspond un parcours par
niveau de nuds de l'arbre. Un niveau est un ensemble de
nuds ou de feuilles situs la mme profondeur.
Ainsi, si l'arbre ci-contre est utilis, le parcours sera A,
B, C, D, E, F, G.

Parcours en profondeur
Le parcours en profondeur est un parcours rcursif sur un arbre. Il existe trois ordres pour cette
mthode de parcours. Le parcours en profondeur prfix est le plus courant.
Parcours en profondeur prfix
Dans ce mode de parcours, le nud courant est trait avant le traitement des nuds gauche et
droit. Ainsi, si l'arbre prcdent est utilis, le parcours sera A, B, D, E, C, F, G.
Parcours en profondeur suffix
Dans ce mode de parcours, le nud courant est trait aprs le traitement des nuds gauche et
droit. Ainsi, si l'arbre prcdent est utilis, le parcours sera D, E, B, F, G, C, A. Ce mode de parcours
correspond une notation polonaise inverse, utilise par les calculatrices HP.

Didier Mller

8-3

janvier 2015

Structures de donnes avances


Parcours en profondeur infix
Dans ce mode de parcours, le nud courant est trait entre le traitement des nuds gauche et
droit. Ainsi, si l'arbre prcdent est utilis, le parcours sera D, B, E, A, F, C puis G.

Exercice 8.3
Parcourez l'arbre ci-dessous selon les quatre mthodes vues prcdemment.

8.4.

Arbres binaires

Dans un arbre binaire, chaque nud possde au plus deux fils, habituellement appels gauche
et droit . Du point de vue des fils, l'lment dont ils sont issus au niveau suprieur est logiquement
appel pre.

8.4.1.

Didier Mller

Types d'arbres binaires


Un arbre binaire entier est un arbre dont tous les nuds possdent zro ou deux fils.
Un arbre binaire parfait est un arbre binaire entier dans lequel toutes les feuilles sont la
mme hauteur.
L'arbre binaire parfait est parfois nomm arbre binaire complet. Cependant certains
dfinissent un arbre binaire complet comme tant un arbre binaire entier dans lequel les
feuilles ont pour profondeur n ou n-1 pour un n donn.

8-4

janvier 2015

L'informatique au lyce

8.4.2.

Chapitre 8

Mthodes pour stocker des arbres binaires

Structure 3 nuds
Les arbres binaires peuvent tre construits de diffrentes manires. Dans un langage avec
structures et pointeurs (ou rfrences), les arbres binaires peuvent tre conus en ayant une structure
trois nuds qui contiennent quelques donnes et des pointeurs vers son fils droit et son fils gauche.
Parfois, il contient galement un pointeur vers son unique parent. Si un nud possde moins de deux
fils, l'un des deux pointeurs peut tre affect de la valeur spciale nulle.

L'arbre conservera un pointeur vers la racine, un pointeur vers le nud courant (qui permet de
simplifier l'ajout et la consultation) et ventuellement le nombre de nuds du graphe.
Avantages
Conu pour contenir un nombre variable de nuds.
Pas de gaspillage de mmoire.
Inconvnients
Implmentation dlicate raliser quand on est dbutant en programmation.
Pas d'accs direct un nud de l'arbre.

Tableau
La fonction
floor(x) retourne
l'entier infrieur
ou gal x.

Les arbres binaires peuvent aussi tre rangs dans des tableaux, et si l'arbre est un arbre binaire
complet, cette mthode ne gaspille pas de place, et la donne structure rsultante est appele un tas.
Dans cet arrangement compact, un nud a un indice i, et ses fils se trouvent aux indices 2i+1 et
2i+2, tandis que son pre se trouve l'indice floor((i1)/2), s'il existe.

La racine (nud 0) a pour fils les nuds 1 et 2. Le nud 1 a pour fils 3 et 4, etc.
Avantages
Implmentation facile raliser.
Possibilit d'accs direct un nud de l'arbre (un seul accs en mmoire).
Inconvnients
Conu pour contenir un nombre fixe de nuds.
Si l'arbre est profond mais contient peu de nuds, il se produit un gaspillage important
de mmoire.

Didier Mller

8-5

janvier 2015

Structures de donnes avances

Exercice 8.4
Programmez en Python les trois algorithmes de parcours en profondeur vus au 8.3.1 puis
excutez-les sur un tableau reprsentant l'arbre binaire ci-dessous :

8.5. La notation O(f(n))


Dans les annes 1960 et au dbut des annes 1970, alors qu'on en tait dcouvrir des
algorithmes fondamentaux, on ne mesurait pas leur efficacit. On se contentait de dire, par exemple :
Cet algorithme se droule en 6 secondes avec un tableau de 50'000 entiers choisis au hasard en
entre, sur un ordinateur IBM 360/91. Le langage de programmation PL/I a t utilis avec les
optimisations standards.
Une telle dmarche rendait difficile la comparaison des algorithmes entre eux. La mesure publie
tait dpendante du processeur utilis, des temps d'accs la mmoire vive et de masse, du langage
de programmation et du compilateur utilis, etc.
Comme il est trs difficile de comparer les performances des ordinateurs, une approche
indpendante des facteurs matriels tait ncessaire pour valuer l'efficacit des algorithmes. Donald
Ervin Knuth (1938-) fut un des premiers l'appliquer systmatiquement ds les premiers volumes
de sa fameuse srie The Art of Computer Programming.
La notation grand O (avec la lettre majuscule O, et non pas le chiffre zro) est un symbole utilis
en mathmatiques, notamment en thorie de la complexit, pour dcrire le comportement
asymptotique des fonctions.
Une fonction g(n) est dite O(f(n)) s'il existe un entier N et une constante relle
positive c tels que pour n > N on ait g(n) cf(n).
En d'autres termes, O(f(n)) est l'ensemble de toutes les fonctions g(n) bornes suprieurement par
un multiple rel positif de f(n), pour autant que n soit suffisamment grand.
Attention, g(n) est O(f(n)) ne veut pas dire que g(n) s'approche asymptotiquement de f(n), mais
seulement qu'au-del de certaines valeurs de son argument, g(n) est domin par un certain multiple
de f(n).

8.5.1.

Exemples

g(n) = n3 + 2n2 + 4n + 2 = O(n3)

g(n) = n3 + 10100n2 = O(n3)

g(n) = nlog(n) + 12n + 888 = O(nlog(n))


g(n) = 1000n10 n7 + 12n4 + 2n = O(2n)

Didier Mller

8-6

janvier 2015

L'informatique au lyce

Chapitre 8

8.5.2.

Quelques classes de complexit

Notation

Type de complexit

O(1)

complexit constante (indpendante de la taille de la donne)

O(log(n))

complexit logarithmique

O(n)

complexit linaire

O(nlog(n))

complexit quasi-linaire

O(n2)

complexit quadratique

O(n3)

complexit cubique

O(np)

complexit polynomiale

O(2n)

complexit exponentielle

O(n!)

complexit factorielle

Les complexits sont donnes de la meilleure, O(1), la pire, O(n!).

Quelques courbes de complexit

Exercice 8.5
Un programme rsout un problme de taille n en n! secondes. Combien de temps est ncessaire
pour rsoudre un problme de taille n = 12 ?

8.6.

Arbres binaires de recherche

Un arbre binaire de recherche (ABR) est un arbre binaire dans lequel chaque nud possde une
tiquette, telle que chaque nud du sous-arbre gauche ait une tiquette infrieure ou gale celle du
nud considr, et que chaque nud du sous-arbre droit possde une tiquette suprieure ou gale
celle-ci (selon la mise en uvre de l'ABR, on pourra interdire ou non des tiquettes de valeur gale).

Didier Mller

8-7

janvier 2015

Structures de donnes avances

8.6.1.

Recherche

La recherche dans un arbre binaire d'un nud ayant une tiquette particulire est un procd
rcursif. On commence par examiner la racine. Si l'tiquette de la racine est l'tiquette recherche,
l'algorithme se termine et renvoie la racine. Si l'tiquette cherche est infrieure, alors elle est dans le
sous-arbre gauche, sur lequel on effectue alors rcursivement la recherche. De mme, si l'tiquette
recherche est strictement suprieure l'tiquette de la racine, la recherche continue sur le sous-arbre
droit. Si on atteint une feuille dont l'tiquette n'est pas celle recherche, on sait alors que cette
tiquette n'est pas dans l'arbre.
Cette opration requiert un temps en O(log(n)) dans le cas moyen, mais O(n) dans le pire des cas
o l'arbre est compltement dsquilibr o chaque pre a un seul fils.

8.6.2.

Insertion

L'insertion d'un nud commence par une recherche : on cherche l'tiquette du nud insrer ;
lorsqu'on arrive une feuille, on ajoute le nud comme fils de la feuille en comparant son tiquette
celle de la feuille : si elle est infrieure, le nouveau nud sera gauche ; sinon il sera droite. Ainsi,
chaque nud ajout sera une feuille.
La complexit de l'insertion est O(log(n)) dans le cas moyen et O(n) dans le pire des cas.

Exercice 8.6
Insrez dans l'arbre binaire de recherche ci-dessous les valeurs 11 et 5.

Didier Mller

8-8

janvier 2015

L'informatique au lyce

8.6.3.

Chapitre 8

Suppression

Plusieurs cas sont considrer, une fois que le nud supprimer a t trouv :

Pour aller le plus


gauche dans le
sous-arbre droit, on
part de la racine, on
se dplace une fois
droite, puis
toujours gauche,
tant qu'on le peut.

Suppression d'une feuille


Il suffit de l'enlever de l'arbre tant donn qu'elle n'a pas de fils.

Suppression d'un nud avec un seul fils


On l'enlve de l'arbre et on le remplace par son fils.

Suppression d'un nud avec deux fils


Supposons que le nud supprimer soit appel N (le nud de valeur 7 dans le schma cidessous). On le remplace alors par son successeur le plus proche, donc le nud le plus
gauche du sous-arbre droit (ci-aprs, le nud de valeur 9) ou son plus proche prdcesseur,
donc le nud le plus droite du sous-arbre gauche (ci-dessous, le nud de valeur 6).

Cela permet de garder une structure d'arbre binaire de recherche. Puis on applique
nouveau la procdure de suppression N, qui est maintenant une feuille ou un nud avec
un seul fils.

Pour une implmentation efficace, il est dconseill d'utiliser uniquement le successeur ou


le prdcesseur car cela contribue dsquilibrer l'arbre.
Didier Mller

8-9

janvier 2015

Structures de donnes avances


Dans tous les cas, une suppression requiert de parcourir l'arbre de la racine jusqu' une feuille : le
temps d'excution est donc proportionnel la profondeur de l'arbre qui vaut n dans le pire des cas,
d'o une complexit maximale en O(n).

8.7.

Arbres AVL

La dnomination arbre AVL provient des noms de ses deux inventeurs russes, Georgy
Maximovich Adelson-Velsky et Evgenii Mikhailovich Landis, qui l'ont publi en 1962 sous le titre
An algorithm for the organization of information.
Les arbres AVL ont t historiquement les premiers arbres binaires de recherche automatiquement quilibrs. Dans un arbre AVL, les hauteurs des deux sous-arbres d'un mme nud diffrent
au plus de un. La recherche, l'insertion et la suppression sont toutes en O(log(n)) dans le pire des cas.

Le facteur d'quilibrage d'un nud est la diffrence entre la hauteur de son sous-arbre droit et
celle de son sous-arbre gauche. Un nud dont le facteur d'quilibrage est +1, 0, ou -1 est considr
comme quilibr.

Un nud avec tout autre facteur est considr comme dsquilibr et requiert un rquilibrage.
Chaque fois qu'un nud est insr ou supprim d'un arbre AVL, le facteur d'quilibrage de chaque
nud le long du chemin depuis la racine jusqu'au nud insr (ou supprim) doit tre recalcul.
Si l'arbre est rest quilibr, il n'y a rien faire. Si ce n'est pas le cas, on effectuera des rotations
d'quilibrage de manire obtenir nouveau un arbre AVL.

8.7.1.

Rotations d'quilibrage

Une rotation est une modification locale d'un arbre binaire. Elle consiste changer un nud
avec l'un de ses fils.
Dans la rotation droite, un nud devient le fils droit du nud qui tait son fils gauche.
Dans la rotation gauche, un nud devient le fils gauche du nud qui tait son fils droit.
Les rotations gauches et droites sont inverses l'une de l'autre.
Les rotations ont la proprit de pouvoir tre implmentes en temps constant, et de prserver
l'ordre infixe. En d'autres termes, si A est un arbre binaire de recherche, tout arbre obtenu partir de
A par une suite de rotations gauche ou droite d'un sous-arbre de A reste un arbre binaire de recherche.
Une rotation simple droite est utilise quand un nud a un facteur d'quilibrage infrieur -1 et
que son fils gauche a un facteur d'quilibrage de -1.

Didier Mller

8-10

janvier 2015

L'informatique au lyce

Chapitre 8

Une rotation double droite est utilise quand un nud a un facteur d'quilibrage infrieur -1 et
que son fils gauche a un facteur d'quilibrage de +1. La double rotation droite est une rotation simple
gauche du sous-arbre gauche, suivi d'une rotation simple droite du nud dsquilibr. Cette
opration est aussi appele parfois une rotation gauche-droite.

Une rotation simple gauche est utilise quand un nud a un facteur d'quilibrage suprieur +1
et que son fils droit a un facteur d'quilibrage de +1.

Une rotation double gauche est utilise quand un nud a un facteur d'quilibrage suprieur +1
et que son fils gauche a un facteur d'quilibrage de -1. La double rotation gauche est une rotation
simple droite du sous-arbre droit, suivi d'une rotation simple gauche du nud dsquilibr. Cette
opration est aussi appele parfois une rotation droite-gauche.

Didier Mller

8-11

janvier 2015

Structures de donnes avances

8.7.2.

Insertion dans un arbre AVL

L'insertion dans un arbre AVL se droule en deux tapes :


1. tout d'abord, on insre le nud exactement de la mme manire que dans un arbre
binaire de recherche ;
2. puis on remonte depuis le nud insr vers la racine en effectuant une rotation sur
chaque sous-arbre dsquilibr.
On peut montrer qu'il suffit d'une rotation simple ou d'une double rotation pour rquilibrer un
arbre AVL aprs une insertion.
La hauteur de l'arbre tant en O(log(n)), et les rotations tant temps constant, l'insertion se fait
finalement en O(log(n)).

8.7.3.

Suppression

La suppression dans un arbre AVL peut se faire par rotations successives du nud supprimer
jusqu' une feuille (en choisissant ces rotations de sorte que l'arbre reste quilibr), et ensuite en
supprimant cette feuille directement. La suppression se fait aussi en O(log(n)).

8.7.4.

Recherche

La recherche dans un arbre AVL se droule exactement comme pour un arbre binaire de
recherche, et comme la hauteur d'un arbre AVL est en O(log(n)), elle se fait donc en O(log(n)).

Exercice 8.7
Insrez dans l'arbre AVL ci-dessous les valeurs 20 et 70.

Exercice 8.8
Observez sur l'applet du site compagnon comment insrer, supprimer et rechercher une valeur
dans un arbre AVL.

8.8.

Tas

On dit qu'un arbre binaire complet est ordonn en tas1 (on dit aussi parfois monceau ) lorsque
la proprit suivante est vrifie :
Pour tous les nuds de l'arbre, tiquette(pre) tiquette(fils).
Cette proprit implique que la plus grande tiquette est situe la racine du tas. Ils sont ainsi
trs utiliss pour implmenter les files priorits2 car ils permettent des insertions en temps
1
2

Heap en anglais
Voir 8.2. Le mot file est mal choisi, car une file de priorit ne ressemble pas une file, ni par son aspect, ni par son comportement.

Didier Mller

8-12

janvier 2015

L'informatique au lyce

Chapitre 8

logarithmique et un accs direct au plus grand lment.


Le fait qu'un tas soit un arbre binaire complet permet de le reprsenter d'une manire efficace par
un tableau unidimensionnel.
En effet, dans un tableau indic partir de 0, le pre d'un nud en position i a pour position
i 1
l'entier infrieur ou gal
; les enfants d'un nud en position i sont situs 2i+1 pour le fils
2
gauche et 2i+2 pour le fils droit (voir 8.4.2).

8.8.1.

Primitives

Une caractristique fondamentale de cette structure de donnes est que la proprit du tas peut
tre restaure efficacement aprs la modification d'un nud. Si la valeur du nud est augmente et
qu'elle devient de ce fait suprieure celle de son pre, il suffit de l'changer avec celle-ci, puis de
continuer ce processus vers le haut jusqu'au rtablissement de la proprit du tas. Nous dirons que la
valeur modifie a t percole jusqu' sa nouvelle position.
Inversement, si la valeur du nud est diminue et qu'elle devient de ce fait infrieure celle d'au
moins un de ses fils, il suffit de l'changer avec la plus grande des valeurs de ses fils, puis de
continuer ce processus vers le bas jusqu'au rtablissement de la proprit du tas. Nous dirons que la
valeur modifie a t tamise jusqu' sa nouvelle position.
Plus formellement, la modification d'un nud se fait par les algorithmes suivants :
PROCEDURE percoler(T[0..n-1], i)
Donnes : tableau T, noeud i
Rsultat : T est de nouveau un tas
k := i
REPETER
j := k
SI j > 0 ET T[(j-1) div 2] < T[k] ALORS
k := (j-1) div 2
echanger T[j] et T[k]
JUSQU'A j = k

Didier Mller

8-13

janvier 2015

Structures de donnes avances

PROCEDURE tamiser(T[0..n-1], i)
Donnes : tableau T, noeud i
Rsultat : T est de nouveau un tas
k := i
REPETER
j := k
(* recherche du plus grand fils du noeud j *)
SI 2j+1 <= n-1 ET T[2j+1] > T[k] ALORS
k := 2j+1
SI 2j+1 < n-1 ET T[2j+2] > T[k] ALORS
k := 2j+2
echanger T[j] et T[k]
JUSQU'A j = k
PROCEDURE modifier_tas(T[0..n-1], i, v)
(* on veut changer la valeur v du noeud i en prservant la proprit
du tas *)
Donnes : tableau T, noeud i, valeur v
Rsultat : T est de nouveau un tas
x := T[i]
T[i] := v
SI v < x ALORS
tamiser(T, i)
SINON
percoler(T, i)

Cette proprit du tas en fait une structure de donnes idale pour trouver le maximum, liminer
la racine, ajouter un nud et modifier un nud. Ce sont prcisment les oprations voulues pour
l'implmentation efficace d'une liste de priorit dynamique : la valeur d'un nud indique la priorit
de l'vnement correspondant. L'vnement le plus prioritaire se trouve toujours la racine et il est
toujours possible de modifier dynamiquement la priorit d'un vnement.
PROCEDURE eliminer_racine(T[0..n-1])
Donnes : tableau T
Rsultat : T[0..n-2] est de nouveau un tas
T[0] := T[n-1]
tamiser(T,0)
supprimer(T[n-1])

PROCEDURE ajouter_noeud(T[0..n-1], v)
Donnes : tableau T, valeur v
Rsultat : T[0..n] est de nouveau un tas
T[n] := v
percoler(T,n)

Exercice 8.9
crivez un programme Python qui gre un tas : reprsentez le tas par un tableau. puis
programmez les procdures ou fonctions suivantes : percoler, tamiser, modifier_tas,
maximum, eliminer_racine, ajouter_noeud.
crivez enfin une procdure qui cre un tas de n valeurs tires alatoirement.

Didier Mller

8-14

janvier 2015

L'informatique au lyce

Chapitre 8

8.9.
Le terme vient de
retrievable memory,
mais l'usage dans la
littrature
francophone est
d'utiliser le
masculin.

Trie

Un trie ou arbre prfixe est un arbre numrique ordonn qui est utilis pour stocker une table
associative o les cls sont gnralement des chanes de caractres. Contrairement un arbre binaire
de recherche, aucun nud dans le trie ne stocke la chane laquelle il est associ. C'est la position du
nud dans l'arbre qui dtermine la chane correspondante.
Pour tout nud, ses descendants ont en commun le mme prfixe. La racine est associe la
chane vide. Des valeurs ne sont pas attribues chaque nud, mais uniquement aux feuilles et
certains nuds internes se trouvant une position qui dsigne l'intgralit d'une chane
correspondant une cl.

Trie construit avec les mots :


abri, abris, abrit, abrite, abrites, abroge, abroger, abroges, as, assaut et assauts.
Les logiciels de traitement de texte prsentent souvent une fonctionnalit d'auto-compltion des
mots au cours de la frappe. Certains diteurs de texte l'tendent aux commandes d'un langage de
programmation. Dans tous les cas, l'auto-compltion est grandement facilite par la reprsentation
des mots sous forme de trie : il suffit en effet d'extraire le sous-arbre correspondant un prfixe
donn pour connatre toutes les suggestions en rapport avec le texte saisi !

8.9.1.

Un trie en Python

class Trie:
def __init__(self, letter=""): # au dbut, l'arbre est vide
self.branches = {}
self.letter = letter
self.is_word_end = False

Didier Mller

8-15

janvier 2015

Structures de donnes avances

def __getitem__(self, letter): # permet d'accder une valeur


return self.branches.get(letter, None)
def isWordEnd(self): # est-ce la fin d'un mot ?
return self.is_word_end
def setWordEnd(self, value=True):
# marquer que c'est la fin d'un mot
self.is_word_end = value
def getLetter(self): # lire une lettre
return self.letter
def addBranch(self, branch): # ajouter une branche l'arbre
self.branches[branch.getLetter()] = branch
def makeTrie(self, liste_mots):
# cre un trie partir d'une liste de mots
for un_mot in liste_mots:
noeud = self
# on se place au dbut
mot = un_mot.rstrip()
if len(mot)>1:
for lettre in mot:
# lit les lettres
if not noeud[lettre]:
# on cre une nouvelle branche
noeud.addBranch(Trie(lettre))
noeud = noeud[lettre]
# on avance
noeud.setWordEnd()
# marque de
return self

de l'arbre
une une

dans l'arbre
fin de mot

def inTrie(self, mot):


# mot est-il dans le Trie ?
noeud = self
for lettre in mot:
if noeud[lettre] != None :
noeud = noeud[lettre]
else:
return False
else:
if noeud.isWordEnd() :
return True
else:
return False

8.9.2.

Utilisation d'un trie comme dictionnaire

Le programme ci-dessous, utilise le trie dfini au 8.10.1 et un fichier dictionnaire.txt . Ce


dictionnaire contient 328'465 mots franais non accentus entre 2 et 16 lettres, sans tiret et sans
apostrophe. On entre simplement un mot franais (sans accent) et le programme dit s'il est franais
ou non.
from trie_fr import Trie
# ---------------------------------------------------def creer_trie(dico):
print("Lecture du dictionnaire")
fichier = open(dico, 'r')
liste_mots = fichier.readlines()
fichier.close()
print("Cration du Trie")
trie = Trie()
trie.makeTrie(liste_mots)

Didier Mller

8-16

janvier 2015

L'informatique au lyce

Chapitre 8
print("Trie termin")
return trie
# -------------- programme principal -------------------trie = creer_trie('dictionnaire.txt')
while True:
donnees = input("Entrez un mot franais : ")
if donnees=="":
break
if trie.inTrie(donnees):
print(donnees,"est dans le dictionnaire")
else:
print(donnees,"n'est pas dans le dictionnaire")

Comme vous pourrez le constater en testant ce programme, la cration du trie prend un peu de
temps (quelques secondes). Par contre, une fois le trie cr, la rponse la question est instantane.

8.10.

Reprsentation des graphes

Un graphe fini G = (V, E) est dfini par l'ensemble fini V = {v1, v2, ..., vn} dont les lments sont
appels sommets, et par l'ensemble fini E = {e1, e2, ..., em} dont les lments sont appels artes. Une
arte e de l'ensemble E est dfinie par une paire non-ordonne de sommets, appels les extrmits de
e. Si l'arte e relie les sommets a et b, on dira que ces sommets sont adjacents.
Prenons un exemple :
Ensemble des sommets :
V = {1, 2, 3, 4, 5, 6}
Ensemble des artes :
E = {(1, 2), (1, 5), (2, 5), (2, 3), (3, 4), (4, 5), (4, 6)}
Ce graphe peut se reprsenter graphiquement comme cicontre (il y a une infinit de reprsentations possibles).

8.10.1.

Listes d'adjacences

On peut aussi reprsenter un graphe en donnant pour chacun de ses sommets la liste des sommets
auxquels il est adjacent. C'est la mthode qui est utilise pour implmenter un graphe sur ordinateur.
Les listes d'adjacences du graphe ci-contre sont :
1:
2:
3:
4:
5:
6:

2, 5
1, 5
2, 4
3, 5, 6
1, 2, 4
4

8.10.2.

Trouver tous les mots d'une grille de Ruzzle

Ruzzle est un jeu de lettres aux rgles simples : une grille de quatre fois quatre lettres, et deux
minutes pour trouver le plus de mots possibles, avec pour seul impratif que les cases se touchent.
On ne peut pas utiliser deux fois la mme case, ce qui fait que la longueur maximale d'un mot sera de
16.

Didier Mller

8-17

janvier 2015

Structures de donnes avances


Pour pimenter le jeu, Ruzzle reprend galement les cases bonus lettre compte double , lettre
compte triple , mot compte double et mot compte triple du Scrabble. Pour gagner, il faudra
obtenir le plus gros total de points !

Dans la version que nous allons voir, on ne tiendra pas compte de ces bonus, ni de la valeur des
lettres ; on se contentera de trouver tous les mots possibles, du plus long au plus court.
Voici le programme complet que nos allons dcortiquer :
from trie_fr import Trie
# --------- case de la grille de Ruzzle ------------class Case:
def __init__(self, value):
self.value = value
self.neighbors = []
def addNeighbor(self, neighbor):
# ajoute un voisin la liste
self.neighbors.append(neighbor)
def getNeighbors(self):
# renvoie la liste des voisins d'une case
return self.neighbors
def getValue(self):
# renvoie la lettre d'une case
return self.value
# ---------------------------------------------------def creer_trie(dico):
print("Lecture du dictionnaire")
fichier = open(dico, 'r')
liste_mots = fichier.readlines()
fichier.close()
print("Cration du Trie")
trie = Trie()
trie.makeTrie(liste_mots)
print("Trie termin")
return trie
def creer_connexions(grille):
# cre les listes d'adjacences du graphe
for m in range(16):
case = grille[m]
i, j = m//4, m%4
for n in range(16):

Didier Mller

8-18

janvier 2015

L'informatique au lyce

Chapitre 8
x, y = n//4, n%4
if abs(i-x)<=1 and abs(j-y)<=1 and m != n:
case.addNeighbor(grille[n])
def chercher(noeud, case, vus=[]):
# cherche les mots franais dans le Trie
trouves = []
if noeud == None:
return trouves
if noeud.isWordEnd():
trouves.append(noeud.getLetter())
vus.append(case)
for voisin in case.getNeighbors():
if voisin not in vus:
results = chercher(noeud[voisin.getValue()], voisin, vus)
trouves.extend([noeud.getLetter()+ending for ending in results])

vus.remove(case)
return trouves
# -------------- programme principal -------------------trie = creer_trie('ruzzle_dictionnaire.txt')
while True:
donnees = input("Entrez la grille ligne par ligne : ")
while len(donnees)!=16 and len(donnees)!=0:
donnees = input("Entrez la grille ligne par ligne : ")
if donnees=="": # taper 'return' pour sortir de la boucle infinie
break
donnees = donnees.lower()
grille = [Case(lettre) for lettre in donnees]
creer_connexions(grille)
resultats = []
for case in grille: # premire lettre du mot chercher
noeud = trie
# on se place au dbut du Trie
mots_trouves = chercher(noeud[case.getValue()], case)
if len(mots_trouves) > 0:
resultats.extend(mots_trouves)
resultats = list(set(resultats)) # limine les doublons
output = sorted(resultats, key=lambda mot: [len(mot)], reverse=True)

print(len(output),"mots trouvs")
print(output)
print()

On voit sur la premire ligne que le programme importe la classe Trie du 8.9.1. La procdure
creer_trie a dj t utilise au 8.9.2
On a cr une classe case. Une case contient une lettre et la liste des lettres qui sont sur des
cases voisines (pas voisin, on entend qui jouxte horizontalement, verticalement ou en diagonale).
class Case:
def __init__(self, value):
self.value = value
self.neighbors = []
def addNeighbor(self, neighbor):
# ajoute un voisin la liste
self.neighbors.append(neighbor)
def getNeighbors(self):
# renvoie la liste des voisins d'une case
return self.neighbors
def getValue(self):
# renvoie la lettre d'une case
return self.value

Didier Mller

8-19

janvier 2015

Structures de donnes avances

La liste des lettres voisines d'une case est cre par la procdure creer_connexions(grille).
La grille est implmente par une liste de 16 lments.
Pour chacune des 16 lettres de la grille, on calcule sa ligne i et sa colonne j en fonction de la
position m dans grille. On parcourt ensuite les 15 autres cases de la grille et on calcule leur ligne x
et leur colonne y. Pour tre voisin de cette case, il faut que |i-x| soit infrieur ou gal 1 et de
mme pour |j-y|.
def creer_connexions(grille):
# cre les listes d'adjacences du graphe
for m in range(16):
case = grille[m]
i, j = m//4, m%4
for n in range(16):
x, y = n//4, n%4
if abs(i-x)<=1 and abs(j-y)<=1 and m!=n:
case.addNeighbor(grille[n])

Voici les listes que l'on obtient pour la grille

u
a
e
e
s
l
r
u
i
i
e
a
o
t
s
y

:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:

a
u
a
e
u
u
a
e
s
s
l
r
i
i
i
e

s
e
e
r
a
a
e
e
l
l
r
u
i
i
e
a

l
s
l
u
l
e
e
r
i
r
u
e
t
e
a
s

u
s
i
o

a
l
i
t

e
r
e
s

e
u
a
y

l r
r u
i
s
l
e
o
i
i
s

i
r
u
a
t
e
a
y

i i e
i e a
o t s
t s y

o s
t y

La fonction la plus dlicate analyser est videmment chercher, car elle est rcursive.
On a vu comment chercher un mot dans le trie (8.9.2). Ici, le problme est un peu diffrent. En
effet, il ne serait pas trs efficace de chercher toutes les suites de lettres possibles (il y en a
12'029'624 selon [7]) et vrifier ensuite lesquels sont des mots franais dans le trie. Par exprience, il
y a gnralement entre 250 et 400 mots valides dans une grille.
On va faire l'inverse : on va parcourir le trie en utilisant les voisins.
On part du sommet du trie avec la premire lettre de la grille (celle en haut gauche).
On choisit son premier voisin dans la grille et on regarde si la suite de ces deux lettres existe dans
le trie.
Si la suite n'existe pas, on abandonne cette branche du trie, on marque ce voisin comme vu et on
essaie un autre voisin.
Si la suite existe, on regarde si c'est un mot complet. Si oui, on le mmorise. Si non, on prend un
voisin de la deuxime lettre et on analyse la suite des trois lettres.
Et ainsi de suite...
def chercher(noeud, case, vus=[]):
# cherche les mots franais dans le Trie
trouves = []
if noeud == None:
return trouves
if noeud.isWordEnd():
trouves.append(noeud.getLetter())
vus.append(case)
Didier Mller

8-20

janvier 2015

L'informatique au lyce

Chapitre 8
for voisin in case.getNeighbors():
if voisin not in vus:
results = chercher(noeud[voisin.getValue()], voisin, vus)
trouves.extend([noeud.getLetter()+ending for ending in results])

vus.remove(case)
return trouves

On parcourt selon ce principe le trie 16 fois, car il y a 16 cases possibles pour la premire lettre.
On limine finalement les doublons dans la liste des mots trouvs et on affiche tous les mots du plus
long au plus court.
for case in grille: # premire lettre du mot chercher
noeud = trie
# on se place au dbut du Trie
mots_trouves = chercher(noeud[case.getValue()], case)
if len(mots_trouves) > 0:
resultats.extend(mots_trouves)
resultats = list(set(resultats)) # limine les doublons
output = sorted(resultats, key=lambda mot: [len(mot)], reverse=True)

print(len(output),"mots trouvs")
print(output)

8.11.

Table de hachage

Une table de hachage (hash table en anglais) est une structure de donne permettant d'associer
une valeur une cl. Il s'agit d'un tableau ne comportant pas d'ordre (un tableau est index par des
entiers). L'accs un lment se fait en transformant la cl en une valeur de hachage (ou simplement
hachage) par l'intermdiaire d'une fonction de hachage. Le hachage est un nombre qui permet la
localisation des lments dans le tableau, typiquement le hachage est l'indice de l'lment dans le
tableau. Une case dans le tableau est appele alvole.

Une table de hachage implmentant un annuaire tlphonique


Diffrentes oprations peuvent tre effectues sur une table de hachage :
cration d'une table de hachage
insertion d'un nouveau couple (cl, valeur)
suppression d'un lment
recherche de la valeur associe une cl (dans l'exemple de l'annuaire, retrouver le numro
de tlphone d'une personne)
destruction d'une table de hachage (pour librer la mmoire occupe)
Tout comme les tableaux, les tables de hachage permettent un accs en O(1) en moyenne, quel
que soit le nombre d'lments dans la table. Toutefois, le temps d'accs dans le pire des cas peut tre
de O(n). Compares aux autres tableaux associatifs, les tables de hachage sont surtout utiles lorsque
le nombre d'entres est trs important.

Didier Mller

8-21

janvier 2015

Structures de donnes avances


La position des lments dans une table de hachage est alatoire. Cette structure n'est donc pas
adapte pour accder des donnes tries.

8.11.1.

Choix d'une bonne fonction de hachage

Le fait de crer un hachage partir d'une cl peut engendrer un problme de collision, c'est--dire
qu' partir de deux cls diffrentes, la fonction de hachage pourrait renvoyer la mme valeur de
hachage, et donc par consquent donner accs la mme position dans le tableau. Pour minimiser les
risques de collisions, il faut donc choisir soigneusement sa fonction de hachage.
Les collisions tant en gnral rsolues par des mthodes de recherche linaire, une mauvaise
fonction de hachage, i.e. produisant beaucoup de collisions, va fortement dgrader la rapidit de la
recherche.
Le calcul du hachage se fait parfois en 2 temps :
1. une fonction de hachage particulire l'application est utilise pour produire un nombre
entier partir de la donne d'origine
2. ce nombre entier est converti en une position possible de la table, en gnral en calculant le
reste modulo la taille de la table.
Les tailles des tables de hachage sont souvent des nombres premiers, afin d'viter les problmes
de diviseurs communs, qui creraient un nombre important de collisions. Une alternative est
d'utiliser une puissance de deux, ce qui permet de raliser l'opration modulo par de simples
dcalages, et donc de gagner en rapidit.
Un problme frquent et surprenant est le phnomne de clustering qui dsigne le fait que des
valeurs de hachage se retrouvent cte cte dans la table, formant des clusters ( grappes en
franais). Ceci est trs pnalisant pour les techniques de rsolution des collisions par adressage
ouvert (voir ci-aprs). Les fonctions de hachage ralisant une distribution uniforme des hachages
sont donc les meilleures, mais sont en pratique difficile trouver.

La fonction de hash primaire (la flche en haut) calcule une adresse et gnre une collision.
La premire case libre en ordre croissant, ici en bleu, est trouve et utilise,
consolidant ainsi deux clusters, provoquant une congestion supplmentaire.

Exercice 8.10
Programmez en Python la fonction hashCode(s) de la chane s de longueur n, qui renvoie le
hachage ord(s[0])*32^(n1) + ord(s[1])*32^(n2) + ... + ord(s[n1]) mod N, o ord est la fonction
qui renvoie le code ASCII d'un caractre et N la taille de la table de hachage.
Testez cette fonction avec les noms Chico , Groucho , Gummo , Harpo , Zeppo et
N=5, puis N=11, puis N=12.

8.11.2.

Rsolution des collisions

Lorsque deux cls ont la mme valeur de hachage, ces cls ne peuvent tre stockes la mme
position, on doit alors employer une stratgie de rsolution des collisions.
Pour donner une ide de l'importance d'une bonne mthode de rsolution des collisions,
considrons ce rsultat issu du paradoxe des anniversaires 3. Mme si nous sommes dans le cas le
plus favorable o la fonction de hachage a une distribution uniforme, il y a 95% de chances d'avoir
une collision dans une table de taille 1 million avant qu'elle ne contienne 2500 lments.
3

Le paradoxe des anniversaires est l'origine une estimation probabiliste du nombre de personnes que l'on doit runir pour avoir une chance sur
deux que deux personnes de ce groupe aient leur anniversaire le mme jour de l'anne. Il se trouve que ce nombre est 23, ce qui choque
l'intuition. partir d'un groupe de 57 personnes, la probabilit est suprieure 99 %.

Didier Mller

8-22

janvier 2015

L'informatique au lyce

Chapitre 8

De nombreuses stratgies de rsolution des collisions existent mais les plus connues et utilises
sont le chanage et l'adressage ouvert.

Chanage
Cette mthode est la plus simple. Chaque case de la table est en fait une liste chane des cls qui
ont le mme hachage. Une fois la case trouve, la recherche est alors linaire en la taille de la liste
chane. Dans le pire des cas o la fonction de hachage renvoie toujours la mme valeur de hachage
quelle que soit la cl, la table de hachage devient alors une liste chane, et le temps de recherche est
en O(n). L'avantage du chanage est que la suppression d'une cl est facile ainsi que la recherche.
D'autres structures de donnes que les listes chanes peuvent tre utilises. En utilisant un arbre
quilibr, le cot thorique de recherche dans le pire des cas est en O(log(n)). Cependant, la liste
tant suppose tre courte, cette approche est en gnral peu efficace moins d'utiliser la table sa
pleine capacit, ou d'avoir un fort taux de collisions.

Rsolution des collisions par chanage

Adressage ouvert
L'adressage ouvert consiste, dans le cas d'une collision, stocker les valeurs de hachage dans
d'autres alvoles vides. On appelle ce procd une mthode de sondage : on essaie les alvoles
h(k,0), h(k,1), ..., jusqu' ce qu'on trouve une alvole vide. Il y a trois mthodes de sondage :
Sondage linaire
Soit H : U {0, ... N1} une fonction de hachage auxiliaire.
La fonction de sondage linaire sera : h(k,i) = (H(k) + i) mod N, avec i = 0, 1, ..., N1

Rsolution des collisions par adressage ouvert et sondage linaire

Didier Mller

8-23

janvier 2015

Structures de donnes avances


Sondage quadratique
Soit H : U {0, ... N1} une fonction de hachage auxiliaire.
La fonction de sondage quadratique sera :
h(k,i) = (H(k) + c1i + c2i2) mod N, avec i = 0, 1, ..., N1, c1 et c2 0
Double hachage
Soient H1 et H2 : U {0, ... N1} deux fonctions de hachage auxiliaires.
La fonction de sondage par double hachage sera :
h(k,i) = (H1(k) + iH2(k)) mod N, avec i = 0, 1, ..., N1, c1 et c2 0

Recherche
Lors d'une recherche, si l'alvole obtenue par hachage direct ne permet pas d'obtenir la bonne cl,
une recherche sur les cases obtenues par une mthode de sondage est effectue jusqu' trouver la cl,
ou tomber sur une alvole vide, ce qui indique qu'aucune cl de ce type n'appartient la table.

Suppression

Cette manire de
faire s'appelle en
anglais lazy
deletion ,
suppression
paresseuse.

Dans un adressage ouvert, la suppression d'un lment de la table de hachage est dlicate. Dans le
schma ci-aprs, si l'on supprime John Smith sans prcaution, on ne retrouvera plus Sandra
Dee . La manire la plus simple de s'en sortir est de ne pas vider lalvole o se trouvait John
Smith , mais d'y placer le mot Supprim . On distinguera ainsi les alvoles vides des alvoles o
un nom a t effac : une alvole contenant Supprim sera considre comme occupe lors d'une
suppression, mais vide lors d'une insertion.

Facteur de charge
Une indication critique des performances d'une table de hachage est le facteur de charge qui est la
proportion de cases utilises dans la table. Plus le facteur de charge est proche de 100%, plus le
nombre de sondages effectuer devient important. Lorsque la table est presque pleine, les
algorithmes de sondage peuvent mme chouer : ils peuvent ne plus trouver d'alvole vide, alors qu'il
y en a encore.
Le facteur de charge est en gnral limit 80%, mme en disposant d'une bonne fonction de
hachage. Des facteurs de charge faibles ne sont pas pour autant significatifs de bonne performance,
en particulier si la fonction de hachage est mauvaise et gnre du clustering.

Exercice 8.11
On souhaite stocker des nombres entiers positifs en utilisant une table de hachage. La table
possdant N emplacements (de 0 N1), on utilise la clef suivante: h(x)=x modulo N.
1. Calculez la valeur de la cl pour chacun des lments de la liste suivante (N=12) : 15, 24,
125, 4, 26, 6, 78, 55, 89, 16, 124
2. En supposant que les lments soient inclus dans la table de hachage dans l'ordre de la liste
et que la rsolution des collisions soit faite par chanage, dessinez la table obtenue.
3. En supposant que les lments soient inclus dans la table de hachage dans l'ordre de la liste
et que la rsolution des collisions soit faite par un sondage linaire, dessinez la table
obtenue.

Exercice 8.12
Utilisez la fonction de hachage programme dans l'exercice 8.8 dans une table de taille N=12.
Implmentez une classe table_de_hachage .
crivez une mthode inserer , qui insre un nouvel abonn (nom et numro de
tlphone) dans l'annuaire.
crivez une mthode rechercher , qui affiche le numro de tlphone d'un abonn. Si
plusieurs abonns portent le mme nom, la mthode affichera les diffrents numros.
crivez une mthode supprimer , qui retire un abonn de l'annuaire.
Testez ensuite votre programme avec les donnes suivantes :

Didier Mller

8-24

janvier 2015

L'informatique au lyce
Chico
Groucho
Gummo
Harpo
Zeppo
Zeppo

Chapitre 8
0324661193
0324667543
0324664578
0324668501
0324660031
0324660032

Faites cet exercice deux fois : une fois en rsolvant les collisions par chanage et l'autre fois en
les rsolvant par adressage ouvert (avec un sondage linaire).

Sources
[1] Wikipdia, Structures de donnes ,
<http://fr.wikipedia.org/wiki/Catgorie:Structure_de_donnes>
[2] Wikipdia, Arbre (Structures de donnes) ,
<http://fr.wikipedia.org/wiki/Catgorie:Arbre_(structure_de_donnes)>
[3] Wikipdia, Arbre binaire , <http://fr.wikipedia.org/wiki/Arbre_binaire>
[4] Wikipdia, Arbre binaire de recherche ,
<http://fr.wikipedia.org/wiki/Arbre_binaire_de_recherche>
[5] Wikipdia, Arbre AVL , <http://fr.wikipedia.org/wiki/Arbre_AVL>
[6] Ferraro Tyler, Ruzzle-Solver , <https://github.com/TylerFerraro/Ruzzle-Solver>
[7] Mannino Miro, Ruzzle Solver Algorithm ,
<http://miromannino.com/ruzzle-solver-algorithm/>
[8] Wikipdia, Table de hachage , <http://fr.wikipedia.org/wiki/Table_de_hachage>
[9] Wikipdia, Fonction de hachage , <http://fr.wikipedia.org/wiki/Fonction_de_hachage>
[10] Nicod Jean-Marc, Les tables de hachage ,
<http://ifc.univ-fcomte.fr/~nicod/slidesHashTableL3.pdf>
[11] Wikipdia, Thorie de la complexit des algorithmes ,
<http://fr.wikipedia.org/wiki/Thorie_de_la_complexit_des_algorithmes>

Didier Mller

8-25

janvier 2015