Structures de données et complexité
Structures de données et complexité
Alexandre Duret-Lutz
[email protected]
2 août 2010
A. Duret-Lutz Algorithmique 1 / 60
Troisième partie III
Structures de données
1 Types abstraits, structures de Tables de hachage
données Fonctions de hachage
Représentations d'ensembles
Hachage avec chaînage
2
Adressage ouvert
Tableaux Arbres binaires de recherche
Listes Arbres rouge et noir
Piles et les Skip lists
Files de priorité Complexités des structures
3 Tableaux associatifs, de recherche présentées
structures de recherche
A. Duret-Lutz Algorithmique 2 / 60
Types abstraits, structures de données
1 Types abstraits, structures de données
2 Représentations d'ensembles
Tableaux
Listes
Piles et les
Files de priorité
3 Tableaux associatifs, structures de recherche
Tables de hachage
Fonctions de hachage
Adressage ouvert
A. Duret-Lutz Algorithmique 4 / 60
Représentations d'ensembles
1 Types abstraits, structures de données
2 Représentations d'ensembles
Tableaux
Listes
Piles et les
Files de priorité
3 Tableaux associatifs, structures de recherche
Tables de hachage
Fonctions de hachage
Adressage ouvert
Pile (stack )
File (queue )
Bien sûr il nous arrive aussi de vouloir trier une séquence, concaténer
(union) deux ensembles, ou à l'inverse en couper un en deux, etc.
A. Duret-Lutz Algorithmique 7 / 60
Tableaux
tableau.
A. Duret-Lutz Algorithmique 8 / 60
Tableaux dynamiques (1/2)
Leur taille peut varier.
Besoin de faire realloc() en C quand il n'y a plus de cases libres.
En C++ std::vector fait le realloc() lui-même. La réallocation
d'un tableau demande Θ(n) opérations car il faut le copier.
L'insertion, normallement en Θ(1), devient en Θ(n) quand cette
réallocation est nécessaire.
On veut étudier la complexité amortie d'une insertion dans une
séquence d'insertions.
Lors d'une réallocation, on s'arrange pour allouer plusieurs nouvelles
cases d'un coup an que le surcoût de cette réallocation reste
marginal par rapport au nombre des insertions.
Plusieurs façons de choisir la nouvelle taille d'un tableau sont
possibles et donnent des complexités diérentes.
A. Duret-Lutz Algorithmique 9 / 60
Tableaux dynamiques (2/2)
On considère une insertion qui provoque une réallocation dans un
tableau de taille n, avec deux façons d'agrandir le tableau :
Agrandissement constant de k cases. Il y a donc une réallocation
toutes les k cases : le côut moyen des k dernières insertions est
(k − 1)Θ(1) + 1Θ(n)
= Θ(n)
k
5 1 3 7 2 8 2 5 3 9 0 4 9 4 2
5 i ← Parent (i )
A. Duret-Lutz Algorithmique 16 / 60
Tableaux associatifs, structures de recherche
1 Types abstraits, structures de données
2 Représentations d'ensembles
Tableaux
Listes
Piles et les
Files de priorité
3 Tableaux associatifs, structures de recherche
Tables de hachage
Fonctions de hachage
Adressage ouvert
A[f (x )] 6= 0.
A. Duret-Lutz Algorithmique 21 / 60
GPerf
A. Duret-Lutz Algorithmique 22 / 60
Hachage avec chaînage
Lorsqu'une fonction de hachage parfaite n'est pas disponible (par
exemple m est trop petit, ou bien l'ensemble F est sans cesse
modié) deux éléments peuvent devoir être rangés au même indice et
l'on doit gérer ces collision.
i []
A i
Comme dans le tris par paquet, la façon la plus 0 chat
simple et de conserver une liste de valeurs 1 /
possibles pour chaque indice du tableau. 2 /
Reprenons F = {"chat", "chien", "oie", 3 /
"poule", "loup"}.
4 oie
..
Alors x ∈ F ssi Search(A[f (x )], x ) 6= 0. . /
La recherche ne se fait plus en Θ(1) parce qu'il 8 chien
..
faut parcourir une liste. Dans le pire des cas, la . /
liste fait n = |F| éléments, dans le meilleur on 20 poule, loup
espère n/m éléments. ..
A. Duret-Lutz Algorithmique
. / 23 / 60
Hachage avec chaînage : hachage uniforme
A. Duret-Lutz Algorithmique 24 / 60
Exemples de fonctions de hachage : La division
f (x ) = x mod m
On évitera une puissance de deux telle que m = 256 car cela revient
à ignorer les 8 bits les plus faibles de x , souvent ceux qui varient le
plus. On conseille de prendre pour m un nombre premier assez éloigné
d'une puissance de 2.
Par exemple pour représenter 3000 éléments avec une moyenne de
deux éléments par liste, on peut choisir m = 1543.
Une implémentation de table de hachage par cette méthode contient
typiquement une liste de nombres premiers à utiliser au fur et à
mesure que la table s'agrandit. Par exemple si on double la taille de
la table chaque fois qu'on la réalloue : 53, 97, 193, 389, 769, 1543,
3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241, etc.
La classe std::hash_map du C++ fait cela avec la fonction
f (x ) = g (x ) mod m où g est fournie par l'utilisateur (pour convertir
machine de bureau) une paire de messages qui ont la même clef MD5.
A. Duret-Lutz Algorithmique 27 / 60
Autre exemple de fonction de hachage pour entier
/* sizeof int == 32 */
int wang32_hash(int key)
On peut combiner cette fonction
{ plusieurs fois pour hacher quelque
key += ~(key 15); chose de plus gros. Par exemple
key ^= (key 10);
wh(wh(wh(s[0])^s[1])^s[2])
key += (key 3);
key ^= (key 6); L' avalanche prend plus de
key += ~(key 11); sens lorsque la fonction est
key ^= (key 16);
return key;
appliquée ainsi plusieurs fois.
}
Comment vérier une telle fonction ? Exercice : tracez les 30000
premières valeurs de y = wh(x ) avec Gnuplot. On voit des motifs de
zones vierges apparaître. Tracez y = wh(wh(x )) : ça paraît
beaucoup plus aléatoire.
A. Duret-Lutz Algorithmique 28 / 60
y = wh(x ) et y = wh(wh(x ))
A. Duret-Lutz Algorithmique 29 / 60
Adressage ouvert
Tous les éléments sont stockés dans la table, sans liste ni pointeurs.
Résolution des collisions sans chaînage : on doit juste examiner
plusieurs positions pour trouver la bonne.
Une fonction de hachage pour une clef x prend aussi une itération i .
Pour insérer une valeur, on teste si A[f (x , 0)] est libre, sinon
A[f (x , 1)], sinon A[f (x , 2)], etc. On dit qu'on sonde diérentes
positions.
La fonction f doit être telle que f (x , i ) parcourt [[0, m[[ lorsque i
parcourt [[0, m[[. L'ordre de parcours dépend de la clef x .
La recherche se fait de la même façon jusqu'à ce qu'on tombe sur
une case vide.
Danger à la suppression : pourquoi ne peut-on pas remplacer vider la
case bêtement ?
A. Duret-Lutz Algorithmique 30 / 60
Fonctions de hachage pour adressage ouvert
A. Duret-Lutz Algorithmique 31 / 60
Complexité des recherches dans l'adressage ouvert
A. Duret-Lutz Algorithmique 32 / 60
Taille critique des tableaux de hachage
On retiendra surtout :
√ √
n (0.5, N ) ≈ 1.177 N = Θ( N )
√
C'est-à-dire qu'a partir de N valeurs dans une table de hachage
(hachée uniformément) on a à peu près une chance sur deux d'avoir
une collision.
A. Duret-Lutz Algorithmique 33 / 60
Arbres binaires de recherche
Un arbre binaire dont les n÷uds sont étiquetés par des valeur est dit
de recherche si pour un n÷ud r étiqueté par v toutes les étiquettes
du sous-arbre gauche de r sont ≤ v , et toutes celles du du sous-arbre
droit de r sont ≥ v .
4
7 6
8
4 9
7 9
6 8 10 10
Tous les ABR ne sont pas équilibrés. La complexité de la recherche
est O(h) avec h la hauteur de l'arbre. Recherche du minimum et
maximum en O(h) aussi.
A. Duret-Lutz Algorithmique 34 / 60
Ordre inxe
Parcourir l'arbre dans l'ordre inxe permet de visiter les clefs dans
l'ordre.
InxPrint(T , z )
1 if LeftChild(z ) 6= NIL then
2 InxPrint(T , LeftChild (z ))
3 print key (z )
4 if RightChild(z ) 6= NIL then
5 InxPrint(T , RightChild (z ))
PrexPrint(T , z ) SuxPrint(T , z )
1 print key (z ) 1 if LeftChild(z ) 6= NIL then
2 if LeftChild(z ) 6= NIL then 2 InxPrint(T , LeftChild (z ))
3 InxPrint(T , LeftChild (z )) 3 if RightChild(z ) 6= NIL then
4 if RightChild(z ) 6= NIL then 4 InxPrint(T , RightChild (z ))
5 InxPrint(T , RightChild (z )) 5 print key (z )
A. Duret-Lutz Algorithmique 35 / 60
Insérer dans un ABR est facile
Entrée : un ABR T et un enregistrement z à y ajouter
Sortie : l'ABR T contenant z
TreeInsert(T , z )
1 y ← NIL
2 x ← Root (T )
3 while x 6= NIL do
4 y ← x
5 if key (z ) < key (x )
8 Parent (z ) ← y
9 if y = NIL then
10 Root (T ) ← z
11 else
14 else RightChild (y ) ← z
A. Duret-Lutz Algorithmique 36 / 60
Suppression (1/2)
A. Duret-Lutz Algorithmique 37 / 60
Suppression (2/2)
TreeDelete(T , z )
1 x ← NIL
2 if LeftChild (z ) = NIL or RightChild (z ) = NIL
3 then y ← z
4 else y ← TreeSuccessor (z )
5 if LeftChild (y ) 6= NIL
6 then x ← LeftChild (y )
7 else x ← RightChild (y )
8 if x 6= NIL then Parent (x ) ← Parent (y )
9 if Parent (y ) = NIL then
10 Root (T ) ← x
11 else
12 if y = LeftChild (Parent (y ))
13 then LeftChild (Parent (y )) ← x
14 else RightChild (Parent (y )) ← x
15 if y 6= z then key (z ) ← key (y )
A. Duret-Lutz Algorithmique 38 / 60
Le problème des ABR
Insertion, Suppression, Recherche, Prédécesseur, Successeur,
Minimum et Maximum s'eectuent tous en O(h) et
blog nc ≤ h ≤ n
| {z } |{z}
cas équilibré cas déséquilibré
12
7 20
2 10 NIL NIL
NIL 5 8 11
A. Duret-Lutz Algorithmique 41 / 60
Propriété
Un arbre rouge et noir avec n n÷uds internes possède une hauteur au
plus égale à b2 log(n + 1)c.
Si on fait disparaître les n÷uds rouges, les n÷uds internes noirs
ont entre 2 et 4 ls, et toutes les branches de l'arbre ont toutes
la même longueur h0 .
La hauteur de l'arbre complet est h ≤ 2h0 car il ne peut pas y
avoir plus de n÷uds rouges que de n÷uds noirs sur une branche.
Le nombre de feuilles des deux arbres est n + 1.
On a donc
n +1 ≥ 2 log(n+1) ≥ h0 ≥ h/2 =⇒ h ≤ 2 log(n+1)
h 0
=⇒
D'autre part la longueur minimale d'une branche est log(n + 1)
(moitié de la hauteur). En conséquence, Search, Minimum,
Maximum, Successor et Predecessor sont en Θ(log n). Ce n'est pas
évident pour Insert et Delete.
A. Duret-Lutz Algorithmique 42 / 60
Rotations
Insérer avec TreeInsert ne préserve pas les propriétés des ARN. On
peut rétablir ces propriétés en changeant les couleurs des n÷uds et en
eectuant des rotations localement.
p p
y x
RightRotate
x γ α y
LeftRotate
α β β γ
8 then Root (T ) ← y
9 else if x = LeftChild (p)
10 then LeftChild (p) ← y
11 else RightChild (p) ← y
12 LeftChild (y ) ← x
13 Parent (x ) ← y
A. Duret-Lutz Algorithmique 44 / 60
Insertion dans un ARN
A. Duret-Lutz Algorithmique 45 / 60
Insertion de 9
12
7 20
2 10
5 8 11
9
A. Duret-Lutz Algorithmique 46 / 60
Insertion de 9
12
7 20
2 10
5 8 11
9
A. Duret-Lutz Algorithmique 46 / 60
Insertion de 9
12
10 20
7 11
2 8
5 9
A. Duret-Lutz Algorithmique 46 / 60
Insertion de 9
10
7 12
8 2 11 20
5 9
A. Duret-Lutz Algorithmique 46 / 60
Trois cas à gérer : cas 1
Le père et l'oncle sont tous les deux rouges.
(Dans tous ces cas, les lettres grecques représentent des sous-arbres
avec la même hauteur noire.)
C C
β γ β γ
C C
rotation
A δ B δ
α B A γ
β γ α β
On eectue la rotation qui convient pour aligner ls, père, et
grand-père. Le problème n'est pas résolu, mais on l'a transformé en
cas 3 .
A. Duret-Lutz Algorithmique 48 / 60
Trois cas à gérer : cas 3
Le père est rouge, l'oncle est noir, et le n÷ud courant est dans l'axe
pèregrand-père.
C B
rotation +
δ
inversion B et C
B A C
A γ α β γ δ
α β
Après cette correction l'arbre est correct.
A. Duret-Lutz Algorithmique 49 / 60
Complexité de l'insertion
A. Duret-Lutz Algorithmique 50 / 60
RBTreeInsert
RBTreeInsert(T , z )
1 TreeInsert (T , z )
2 Color (z ) ← red
3 while Color (Parent (z )) = red do
4 if Parent (z ) = LeftChild (Parent (Parent (z ))) then
5 uncle ← RightChild (Parent (Parent (z )))
7
Color (Parent (z )) ← black
8 cas 1
Color (uncle ) ← black
9 z ← Parent (Parent (z ))
10
Color (z ) ← red
11 else if z = RightChild (Parent (z )) then
12 cas 2
z ← Parent (z )
13 LeftRotate (T , z )
14 else
15
Color (Parent (z )) ← black
cas 3
16 Color (Parent (Parent (z ))) ← red
17
RightRotate (T , Parent (Parent (z )))
18 else comme le then , en échangeant Left et Right
19 Color (Root (T )) ← black
A. Duret-Lutz Algorithmique 51 / 60
La suppression
A. Duret-Lutz Algorithmique 52 / 60
Conclusion sur les ARN
Les arbres rouges et noir permettent d'eectuer toutes ces opérations
en Θ(log n) :
Insert
Delete
Minimum
Maximim
Successor
Predecessor
De plus Search se fait en O(log n).
L'intérêt sur les tables de hachage est surtout que les éléments sont
conservés triés.
Cette structure de donnée est celle utilisée par std::map en C++.
A. Duret-Lutz Algorithmique 53 / 60
Skip list
Skip list = liste à enjambement.
Généralisation de la structure de liste triée.
Il s'agit d'une structure probabiliste, ayant la même complexité en
moyenne que les arbres rouges et noirs (i.e. Θ(log n) en moyenne
pour toutes les opérations), avec de grandes chances d'atteindre
cette moyenne, et plus simple à implémenter.
Exemple de skip list à deux niveaux :
• • •
1 • 2 • 3 • 4 • 5 • 6 7 • 8 • 9
•
On localise d'abord l'intervale de l'élément recherché dans la liste de
plus haut niveau, puis on descend dans la liste inférieure pour aner
la recherche.
A. Duret-Lutz Algorithmique 54 / 60
Skip list a deux niveaux
Où doit-on connecter les deux niveaux ?
On veut un espacement régulier, mais avec quel espacement ?
Si on note t1 et t2 la taille des deux listes. Le temps de recherche
d'un élément dans la skip list est en O(t1 + t2 /t1 ). Cette somme est
minimale lorsque
√ les termes sont égaux : t1 = t2 /t1 autrement dit
quand t1 = t2 .
√
n
• •
1 • 2 • 3 • 4 • 5 • 6 • 7 • 8 • 9
√ √ √
n n n
n
√ √
Le coût de recherche est alors de l'ordre de 2 n = O( n ).
A. Duret-Lutz Algorithmique 55 / 60
Skip list à plus de 2 niveaux
√
2 listes : 2 · n
√
3 listes : 3 · n 3
√
k listes : k ·
k
n
√
log n listes : log n · = log n · e ln n
= log n · e ln 2 = 2 log n
1
log n log n
n
•
• •
1 • • 5 • • 9
• 2 • 3 • 4 • • 6 • 7 • 8 •
L'exemple ci-dessus est une skip list idéale. Les recherches y sont
toujours en Θ(log n).
Comment réaliser l'insertion ?
A. Duret-Lutz Algorithmique 56 / 60
Algorithme d'insertion dans une skip list
Soit x à insérer dans une skip list.
Eectuer une recherche pour trouver où insérer x dans la liste du
bas (niveau 0).
Insérer x dans la liste du bas.
Avec une probabilité 1/2, ajouter x à la liste de niveau 1.
Si x a été ajouté au niveau 1, avec une probabilité 1/2, ajouter x
à la liste de niveau 2.
Continuer pour tous les niveaux.
Au nal x apparaît
au niveau 0 avec une probabilité 1
au niveau 1 avec une probabilité 1/2
au niveau 2 avec une probabilité 1/4
au niveau k avec une probabilité 2−k
A. Duret-Lutz Algorithmique 57 / 60
Analyse du coût de la recherche (1/2)
On évalue le côut de la recherche en comptant les déplacement à
partir de la n : à partir du niveau 0, il faut remonter jusqu'au niveau
k en se déplaçant vers la gauche quand on ne peut pas monter.
C (k ) = (1 − p )(1 + C (k )) + p (1 + C (k − 1))
A. Duret-Lutz Algorithmique 58 / 60
Analyse du coût de la recherche (2/2)
C (0) = 0
C (k ) = 1/p + C (k − 1) = k /p
A. Duret-Lutz Algorithmique 59 / 60
Complexités des structures de recherche présentées
table de arbre
opération hachage R. et N. skip list
A. Duret-Lutz Algorithmique 60 / 60