• Aucun résultat trouvé

Chapitre 2 Structures de données de base : les tableaux

N/A
N/A
Protected

Academic year: 2022

Partager "Chapitre 2 Structures de données de base : les tableaux"

Copied!
34
0
0

Texte intégral

(1)

Chapitre 2

Structures de données de base : les tableaux

1. introduction

2. présenter le concept de tableaux 3. applications aux problèmes de tri

4. applications aux problèmes de recherche en table

1. Introduction

Pour développer un programme en vue de résoudre un problème, on accompli généralement les étapes suivantes :

0- Définition du problème;

1- Analyse du problème,

2- Conception d’un algorithme complet de la solution.

3- Rédaction des programmes : 4- Mise au point des programmes 5- Rédaction du rapport de programme :

Il est reconnu que l’élaboration d’un algorithme (programme) est grandement simplifiée par l’utilisation de structures de données complexes et des primitives de manipulation de haut niveau.

Le choix de structures de données influe généralement sur l’efficacité des algorithmes.

• Le choix d’un algorithme peut déterminer le type de la structure de données , et des fois l’adaptation d’une structure de données particulière peut déterminer quel algorithme utiliser.

• Avant de décider quelle structure de données choisir , il est nécessaire de

comprendre la manière dont les données vont être utilisées. Il est utile d’analyser le problème considéré à partir de plusieurs angles. Ainsi, nous aurions besoin de savoir :

1. la longueur des champs (i.e. le champs nom)

2. la plage des valeur possibles de données numériques 3. la quantité des données à traiter

4. les différentes opérations à effectuer sur les données.

5. etc …

• La réponse à ce genre de questions va nous aider à mieux comprendre le problème en question et donnera un aiguise le choix dans les structures des données supportant la résolution du problème en question.

(2)

Types de données, structures de données et types abstraits de données

Le type d’une variable est l’ensemble des valeurs qu’elle peut prendre. Par exemple, une variable de type booléen peut prendre les valeurs Vrai ou Faux. Les types de données élémentaires varient d’un langage à l’autre. Ce sont généralement les entiers, les réels, les booléens et les caractères.

Un type abstrait de données est un ensemble d’objets muni d’un ensemble d’opérations. On peut prendre par exemple plusieurs ensembles de nombres entiers en les dotant des opérations ensemblistes union, intersection et différence.

Les types abstraits de données sont une généralisation des types de données élémentaires (entier, réel, …) tout comme les procédures sont une généralisation des opérations élémentaires (+, -, …) Pour mettre en œuvre sur ordinateur les types abstraits de données, il faut recourir à des structures de données, qui sont des ensembles de variables, à priori de types différents et reliées de multiples façons.

On en distingue deux manières de relier des variables entre-elles :

1. structures linéaires : quelque soit l’élément de cette structure, il est relié à au plus un prédécesseur et au plus un successeur

2. structures non linéaires :

arbres : quelque soit l’élément, il possède au plus un prédécesseur et 0 ou plusieurs successeurs

graphes : quelque soit l’élément, il possède 0 ou plusieurs prédécesseurs et 0 ou plusieurs successeurs,

La cellule est la ‘base’ des structures de données. Une cellule peut être considérée comme une boîte capable de contenir une valeur issue d’un type élémentaire ou complexe. On crée une structure de données en donnant un nom à un ensemble structuré (agrégat) de cellules, et éventuellement, en interprétant les valeurs de certaines comme des liens entre cellules.

Il existe deux de base de structures de données :

les structures de données contiguës : ce sont en fait des tableaux.

les structures de données chaînées : la même chose mais en représentation avec des pointeurs.

Avec un tableau, vous devez connaître au moment de l'écriture du programme le nombre de données que devra recevoir ce dernier, c'est-à-dire son nombre de cases : ex : int tab[30]; (sauf évidemment si vous utilisez de l'allocation de mémoire dynamique).

Avec les structures de données chaînées, il n'y a nul besoin de savoir combien d'éléments va contenir la structure. De plus, et c'est là sûrement son plus gros avantage, vous pouvez insérer une valeur n'importe où dans la liste, sans algorithme de décalage, et d'une manière très aisée.

(3)

Cependant l'accès direct à une valeur ne sera pas possible comme avec les tableaux (tab[23]

me donne directement accès au 24ème élément), il faudra un algorithme de parcours.

Le choix des structures de données se fait suivant le problème à résoudre

Le tableau est le mécanisme d’agrégation le plus simple dans la plupart des langages de programmation

2. LES TABLEAUX

Définition: un tableau est une suite ordonnées d’un nombre fixé d’éléments de même type.

Si A est l’adresse de la location du tableau A[bas,haut] dont chaque élément est un k- octet, l’adresse de A[i]est calculée comme suit :

(4)

A[i] = A + k(i A[i] = A + k(iA[i] = A + k(i A[i] = A + k(i----bas)bas)bas)bas)

C’est la formule d’accès à un élément pour un tableau à une dimension. Pour les dimensions plus grandes (comme dans le cas de matrices), la formule d’accès peut être calculée d’une manière équivalente.

Opérations de base opérées sur un tableau

1. retrouver un élément O(n) 2. supprimer un élément O(n) 3. ajouter un élément O(n) 4. accéder un élément O(1)

3. Applications de tri dans les tableaux

3.1. Présentation du problème

Le tri consiste à réarranger une permutation of n objets de telle manière Xn

X X

X123 ≥...≥ tri par ordre décroissant X1X2X3 ≤...≤ Xn tri par ordre croissant 3.2. Comment trier ?

3.2. Comment trier ? 3.2. Comment trier ?

3.2. Comment trier ? Il existe plusieurs solutions:

3.2.1 Tri par sélection 3.2.1 Tri par sélection 3.2.1 Tri par sélection 3.2.1 Tri par sélection Répéter

1. chercher le plus grand (le plus petit) élément 2. le mettre à la fin (au début)

(5)

Exemple

Figure:Figure:Figure:Figure: tri par sélection

Implémentation Implémentation Implémentation Implémentation

void selsort(Elem* array, int n) {

for (int i=0; i<n-1; i++) { // Selectionner le ième element int lowindex = i; // mémoriser cet indice

for (int j=n-1; j>i; j--) // trouver la plus petite valeur if (key(array[j]) < key(array[lowindex]))

lowindex = j; // mettre à jour l’index swap(array, i, lowindex); échanger

} }

(6)

Complexité ComplexitéComplexité

Complexité : : : : Le pire cas, le plus mauvais cas et le cas moyen sont pareils (pourquoi?)

Pour trouver le plus petit éléments, (n-1) itérations sont nécessaires, pour le 2ème plus petit élément, (n-2) itérations sont effectuées, .… Pour trouver le dernier plus petit élément, 0 itération sont effectuées. Le nombre d’itérations que l’algorithme effectue est donc:

Si par contre, nous prenons comme mesure d’évaluations le nombre de mouvement de données, alors l,algorithme en effectue n-1, car il y a exactement un échange par itération.

3.2.2. Tri par Insertion

Dans ce cas, itérativement, nous insérons le prochain élément dans la partie qui est déjà triée précédemment. La partie de départ qui est triée est le premier élément.

En insérant un élément dans la partie triée, il se pourrait qu’on ait à déplacer plusieurs autres.

void inssort(Elem* array, int n) {

for (int i=1; i<n; i++) // insérer le ième element

for (int j=i; (j>0) && (key(array[j])<key(array[j-1])); j--) swap(array, j, j-1);

}

(7)

Figure: tri par insertion

Complexité ComplexitéComplexité

Complexité : : : : Comme nous n’avons pas nécessairement à scanner toute la partie déjà triée, le pire cas, le meilleur cas et le cas moyen peuvent différer entre eux.

Meilleur cas Meilleur casMeilleur cas

Meilleur cas: Chaque élément est inséré à la fin de la partie triée. Dans ce cas, nous n’avons à déplacer aucun élément. Comme nous avons à insérer (n-1) éléments, chacun générant seulement une comparaison, la complexité est en O(n).

Pire cas:

Pire cas: Pire cas:

Pire cas: Chaque élément est inséré au début de la partie trié. Dans ce cas, tous les éléments de la partie triée doivent être déplacés à chaque itération. La ième itération génère (i-1) comparaisons et échanges de valeurs:

Note:

Note:Note:

Note: C’est le même nombre de comparaison avec le tri par sélection, mais effectue plus d’échanges de valeurs. Si les valeurs à échanger sont importantes, ce nombre peut ralentir cet algorithme d’une manière significative.

(8)

Cas moyen Cas moyenCas moyen

Cas moyen : : : : Si on se donne une permutation aléatoire de nombre, la probabilité d’insérer l’élément à la kème position parmi les positions (0,1,2, …, i-1) est 1/i. Par conséquent, le nombre moyen de comparaisons à la ième itération est:

En sommant sur i, on obtient:

3.2.3. Tri par bulles.

La stratégie de cet algorithme est comme suit :

1. Parcourir le tableau en comparant deux à deux les éléments successifs, permuter s'ils ne sont pas dans l'ordre

2. À la fin du parcours ascendant, le minimum se trouve en tête du tableau. On recommence cette opération en considérant le reste des éléments.

Exemple

(9)

void bubsort(Elem* array, int n) { // Bubble Sort for (int i=0; i<n-1; i++) // échanger

for (int j=n-1; j>i; j--)

if (key(array[j]) < key(array[j-1])) swap(array, j, j-1);

}

3.2.4. Tri far fusion Cet algorithme divise en deux parties égales le tableau de données en question. Après que ces deux parties soient triées d’une manière récursive, elle sont fusionnées pour le tri de l’ensemble des données. Remarquez cette fusion doit tenir compte du fait que ces parties soient déjà triées.

(10)

Implantation

void mergesort(Elem* array, Elem* temp, int left, int right) {

int i, j, k, mid = (left+right)/2;

if (left == right) return;

mergesort(array, temp, left, mid); // la première moitié mergesort(array, temp, mid+1, right);// Sort 2nd half

// l’opération de fusion. Premièrement, copier les deux moitiés dans temp.

for (i=left; i<=mid; i++) temp[i] = array[i];

for (j=1; j<=right-mid; j++)

temp[right-j+1] = array[j+mid];

// fusionner les deux moités dans array for (i=left,j=right,k=left; k<=right; k++) if (key(temp[i]) < key(temp[j])) array[k] = temp[i++];

else array[k] = temp[j--];

}

(11)

Complexité:

La complexité de cet algorithme est donnée par la relation suivante:

n étant le nombre d’éléments dans le tableau.

Exercice ExerciceExercice

Exercice: pourquoi les fonctions plancher et plafond comme paramètres dans T(.)?

Question QuestionQuestion

Question :::: Peut-on parler des trois différentes complexités pour cet algorithme?

Dans le but de simplifier la résolution de l’équation 15.10, nous supposons que

pour un entier k ≥0. En remplaçant O(n) par n, on obtient (en principe, on doit la remplacer par cn):

:

La complexité temporelle de tri par fusion est donc en O(nlogn). 3.2.5. Le tri rapide

La stratégie de l’algorithme de tri rapide (quicksort) consiste, dans un premier temps, à diviser le tableau en deux parties séparées par un élément (appelé pivot) de telle manière que les éléments

(12)

de la partie de gauche soient tous inférieurs ou égaux à cet élément et ceux de la partie de droite soient tous supérieurs à ce pivot (dans l’algorithme donné ci-dessus, la partie qui effectue cette tâche est appelée partition) . Ensuite , d’une manière récursive, ce procédé est itéré sur les deux parties ainsi crées. Notez que qu’au départ, le pivot est choisi dans la version de ci-dessus, comme le dernier élément du tableau.

Implantation

void tri_rapide_bis(int tableau[],int debut,int fin){

if (debut<fin){

int pivot=partition(tableau,debut,fin);

tri_rapide_bis(tableau,debut,pivot-1);

tri_rapide_bis(tableau,pivot+1,fin);

} }

void tri_rapide(int tableau[],int n) {

tri_rapide_bis(tableau,0,n-1);

}

void echanger(int tab[], int i, int j) {

int memoire;

memoire=tab[i];

tab[i]=tab[j];

tab[j]=memoire;

}

int partition(int tableau[], int deb, int fin){

int pivot = tableau[fin];

int i = debut ; j = fin;

do{

do i++

(13)

while (tableau[i] < pivot)

do j--

while (tableau[j] > pivot) if (i < j)

echanger (tableau,i,j) } while(i < j)

tableau[deb] = a[j]; tableau[j] = pivot;

return(j) }

Choix du pivot : Le choix idéal serait que ça coupe le tableau exactement en deux parties égales.

Mais cela n’est pas toujours possible. On peut prendre le premier élément. Mais il existe plusieurs autres stratégies!

Partitionnement :

on parcourt le tableau de gauche à droite jusqu'à rencontrer un élément supérieur au pivot

on parcourt le tableau de droite à gauche jusqu'à rencontrer un élément inférieur au pivot

on échange ces deux éléments

(14)

et on recommence les parcours gauche-droite et droite-gauche jusqu'à a avoir :

il suffit alors de mettre le pivot à la frontière (par un échange)

(15)

Exemple

Complexité ComplexitéComplexité Complexité

À l’appel de QSORT (1,n), le pivot se place en position i. Ceci nous laisse avec un problème de tri de deux sous parties de taille i-1 et n-i. L’algorithme de partition clairement a une complexité au plus de cn pour une constante c.

1 ) 1 (

) ( ) 1 ( ) (

=

+

− +

= T

cn i n T i

T n T

Voyons les trois cas possibles de complexité:

Cas défavorable Cas défavorableCas défavorable Cas défavorable

Le pivot est à chaque fois le plus petit élément. La relation de récurrence devient

(16)

T(n) = T(n – 1) + cn (pourquoi ?) T(n -1) = T(n - 2) + c(n - 1)

T(n - 2) = T(n - 3) + c(n - 2)

... …….

T(2) = T(1) + c(2)

En ajoutant membre à membre, on obtient : T(n) = O(n2)

Cas favorable :

Dans le meilleur des cas, le pivot est, à chaque fois, situé au milieu de la parti à trier.

T(n) = 2T(n/2) + cn

Ce développement s’arrête dès qu’on atteint T(1). Autrement dit, dès que

1 2 / k =

n

n k

n=2k ⇒ =log Solution finale: T(n) = O(n log n)

Question : déterminer la relation de récurrence exacte de la complexité dans ce cas de figure?

(17)

Cas moyen

Nous savons que T(n) = T(i-1) + T(n - i) + cn (voir plus haut). Supposons que toutes les permutations des éléments du tableau soient équiprobables. Autrement dit, la probabilité que la case i soit choisie comme pivot est 1/n. Comme i peut varier entre 1 et n, alors on obtient facilement la récurrence suivante:

=

− +

− +

= n

i

i n t i

n t cn n t

1

) ( ) 1 1 (

) (

Comme

∑ ∑

= =

=

n

i

n

i

i n t i

t

1 1

) ( )

1

( , on obtient la relation:

=

+

= n

i

i n t cn n t

1

) 2 ( )

( qui peut s’écrire

aussi:

(1)

=

+

= n

i

i t cn

n nt

1

2 2 ( )

) ( qui est aussi vraie pour n+1. Autrement dit,

(2)

+

=

+ +

= +

+ 1

1

2 2 ( )

) 1 ( ) 1 ( ) 1 (

n

i

i t n

c n

t n

en soustrayant (1) de (2), on obtient:

1) 1 ( 2 ) 1 (

) 1 2 ( 1 ) ( ) 1 (

) 1 2 ( ) ( ) 1 ( ) 1 (

) 1 ( 2 ) 1 2 ( ) ( ) 1 ( ) 1 (

n c n

n n c n n

n t n n t

n c n nt n

t n

n t n

c n nt n

t n

− −

− =

= +

− − +

+

=

− +

+ + +

=

− + +

Cette équation est aussi vraie pour n, n-1, n-2, …, 4

1) 1 2 ( 2 2

) 1 ( 1 ) (

− −

= −

− −

c n n

n n t n

n t

2) 1 3 ( 2 3

) 2 ( 2

) 1 (

− −

= −

− −

n c n

n n t n

n t

3) 1 4 ( 2 4

) 3 ( 3

) 2 (

− −

= −

− −

n c n

n n t n

n t

(18)

………

2) 1 1 (2 1

) 2 ( 2

) 3

( −t =c

t

En additionnant membre à membre pour n, n-1,…., on obtient après simplifications:



 

− − +



 

 + +

+ −

= −

− − 1

1 1 1 3 ...

1 2 1 1

) 2 ( 1 ) (

c n n

c n t

n n t

Or, on sait que :

) 1 2 log(

... 1 2 1 1 ) 2

log( < −

+ − + +

<

n

n n on obtient donc :

) 2 1 ( 1 1 ) 1 log(

) 1 ( )

( t

n n n

c n

t +

 

− − +

<

t(2) étant une constante, on obtient : t(n)=O(nlogn). Exercice

ExerciceExercice Exercice

1. Déterminer la complexité spatiale de cet algorithme.

2. Montrer que les différentes complexités ne changent pas en utilisant la notation θ.

3.2.6. Le tri de Shell

La stratégie de cette méthode consiste à subdiviser à subdiviser la liste clés à trier en plusieurs sous-listes de telle manière que les éléments de chacune de ces listes sont à des positions à distance fixée, appelé incrément. Chacune de sous-listes est triée en utilisant l’algorithme de tri par insertion. Ensuite, un autre groupe de sous-liste est choisi avec un incrément plus petit que le précédent. On répète ce processus jusqu’à ce que l’incrément soit égal à 1.

(19)

Par exemple, supposons que le nombre n d’éléments à trier soit une puissance de 2. Si on choisit un incrément de départ de n/2, ensuite de n/4, … , jusqu’à 1, alors pour n

=16, on aura 8 sous-listes de 2 éléments, séparé de 8 positions dans le tableau; ensuite 4 sous-listes de 4 éléments séparés de 4 postions dans le tableau; ensuite 2 sous-listes de 8 éléments séparés 2 positions dans le tableau; ensuite une seule de 16 éléments.

Par exemple :

void shellsort(Elem* array, int n) {

for (int i=n/2; i>2; i/=2) // pour chaque incrément for (int j=0; j<i; j++) // trier chque sous-liste inssort2(&array[j], n-j, i);

inssort2(array, n, 1);

}

// Version de tri par insertion avec des incréments qui varient void inssort2(Elem* A, int n, int incr) {

for (int i=incr; i<n; i+=incr) for (int j=i; (j>=incr) &&

(key(A[j])<key(A[j-incr])); j-=incr) swap(A, j, j-incr);

(20)

}

La complexité de cet algorithme est de O(n3/2)

Résultats expérimentaux sur pentium III windows 98

(21)

4. Recherche en tables (tables de hachage)

4. Les tables de hachage

4.1. Introduction

Comme les tableaux, les tables de hachage permettent de gérer des données par l'intermédiaire de clés (appelées indices pour les tableaux). Elles sont utilisées dans le cas où l'univers U des clés est trop grand pour être représenté intégralement, par exemple dans le cas où les clés sont des mots ou des numéros de sécurité sociale.

On réserve une table (table de hachage, hashTable) assez grande pour contenir toutes les données. Pour chaque donnée, on doit déterminer la place où il faut la placer ou la chercher.

(22)

L'idée de base est d'utiliser une fonction h: U {0,...,m-1} de la clé pour déterminer l'objet à trouver et non la clé elle-même, comme c'est le cas pour les tableaux.

Chaque donnée possède une clé unique. À partir de cette clé, une fonction de hachage calcule l'indice (le code) de la place de la donnée dans la table.

Si la fonction de hachage attribue le même code à deux données différentes (ce qui arrive certainement), la deuxième donnée ne pourra être placée dans une place déjà occupée: il se produit alors une collision. Il faut donc lui trouver une autre place.

Par conséquent, il y a lieu de résoudre deux problèmes :

1. choisir une bonne fonction de hachage, qui calcule rapidement le code en évitant le plus possible les collisions

2. traiter les collisions qui se produiront inévitablement.

4.2. Choix d'une fonction de hachage

On doit classer et retrouver des éléments (entiers, chaînes de caractères...) placés dans un tableau de m éléments.

Une fonction de hachage associe à un élément un indice de tableau, c'est-à-dire un entier entre 0 et m-1 (ou entre 1 et m, selon le langage de programmation choisi), indiquant l'endroit où cet élément doit être placé.

Pour choisir une fonction de hachage, il faut bien connaître les éléments à hacher.

Par exemple, utiliser les deux premières lettres pour coder des chaînes de caractères est une bonne formule pour des chaînes sans signification particulière, mais non pour des noms propres, où on trouvera beaucoup de noms commençant par DE, DU, LE, VAN..., ou pour des identificateurs Fortran (les noms des variables entières commencent par I, J, K, L, M ou N).

Dan ce qui suit, nous allons étudier quelques exemples de fonction de hachage pour des mots (chaînes de lettres ABC...Z). À titre illustratif, on suppose que les lettres sont codées sur 5 bits :

(23)

A = 00001 ; B = 00010 ; C = 00011 ... ; Z = 11010 (nombres 1 à 26 codés en base 2) Une fonction de hachage associera donc, à une suite de 0 et de 1, un entier compris entre 0 et m-1 (entre 1 et m).

4.2.1. Hachage par extraction

On extrait les bits 1,2,7,8 (en comptant de droite à gauche) : en les rapprochant, on obtient un entier entre 0 et 15.

ET 00101 10100 h(ET) = 1000 = (8)10 OU 01111 10101 h(OU) = 1101 = (13)10

NI 01110 01001 h(NI) = 1101 = (13)10 collision IL 01001 01100 h(IL) = 0000 = (0)10

Fonction très facile à calculer, mais ne donnant pas de bons résultats.

Une bonne fonction de hachage doit faire intervenir tous les bits de la représentation.

4.2.2. Hachage par Compression

On coupe les chaînes en morceaux de même longueur (5 par exemple, la longueur du code d'une lettre).

On additionne ces morceaux. Pour éviter les retenues, on remplace l'addition par le "ou exclusif"

XOR .

h(ET) = 00101 10100 = 10001 = (17)10 h(OU) = 01111 10101 = 11010 = (26)10 h(NI) = 01110 01001 = 00111 = (7)10 mais h(TE) = h(ET) car l'opération est commutative.

Une bonne fonction de hachage doit briser les sous-chaînes de bits.

On peut améliorer la fonction en faisant des décalages, par exemple circulairement vers la droite, avec un pas différent pour chaque lettre.

(24)

CAR = 00011 00001 10010

on décale de 1 le code de C, de 2 le code de A, de 3 le code de R : CAR 10001 01000 01010 = 10011 = (19)10 ARC = 00001 10010 00011

on décale de 1 le code de A, de 2 le code de R, de 3 le code de C : ARC 10000 10100 01100 = 01000 = (8)10 Les anagrammes ont des codes différents.

C'est une technique souvent utilisée pour réduire la taille des éléments à celle d'un mot mémoire.

4.2.3. Hachage par division

On calcule le reste de la division par m de la valeur de l'élément.

C'est une fonction de hachage facile et rapide à calculer, mais sa qualité dépend de la valeur de m. En général, on préfère prendre m premier.

Par exemple, si la table est de dimension 12 et la clé est 100, alors h(k) = 4. Comme cette fonction ne nécessite qu’une simple division, le hachage par division est rapide.

Quand on utilise cette fonction, on évite en général certaines valeurs de m. Par exemple, m ne doit pas être une puissance de 2, car si m = 2p, alors h(k) représente juste les p premiers bits de k.

En général, il est préférable (comme mentionnée plus haut) d’avoir une fonction de hachage qui utilise tous les bits de la clé.

4.2.4. Hachage par multiplication

Soit un réel strictement compris entre 0 et 1.

On pose :

h(e) = partie entière de ((e * ) mod 1) * m

on multiplie e par

on garde la partie décimale ("mod 1")

on la multiplie par la taille du tableau

on garde la partie entière

(25)

exemple : = 0.6125423371 ; m = 30

h(ET) = [ ((180 * ) mod 1 ) * m ] = [ (110.25762068 mod 1) * 30 ] = [ 0.25762068 * 30 ]

= [ 7.72862034 ] = 7

Des études théoriques montrent que:

- 1 = = 0,6180339887 et 2 - = 1 - = 0,38119660113 sont de bonnes valeurs de .

Il n'y a pas de fonction de hachage universelle.

Cependant, une bonne fonction :

doit être rapide à calculer

doit répartir d’une manière uniforme les éléments Elle dépend donc :

de la machine

des éléments

Cependant, ce qu’il faut retenir c’est qu’il est très difficile d’avoir une fonction qui évite les collisions.

Théorème La probabilité qu'une fonction uniforme h : E [0,m-1] soit injective est :

mn

n m m

m

m( −1)( −2)...( − +1)

où n est le nombre d'éléments de E

Exemple: si m = 365 et n = 23, la probabilité est inférieure à 1/2. Ou encore si l'on réunit 23 personnes, il y a plus d'une chance sur 2 que 2 d'entres elles soient nées le même jour du même mois !

Une autre façon de voir ce théorème : si on veut une fonction de hachage sans collisions pour 23 éléments, il faudra un tableau de dimension supérieur à 365. C'est totalement disproportionné.

(26)

4.3. Résolution des collisions

On suppose que l'on a choisi une bonne fonction de hachage, mais les collisions restent inévitables. Pour les résoudre, deux stratégies :

les méthodes indirectes : le hachage par chaînage. Les éléments en collision sont chaînés entre eux.

les méthodes directes : le hachage par calcul : On calcule un nouvel emplacement.

4.3.1. Les méthodes indirectes

4.3.1.1 Le hachage avec chaînage séparé

Exemple : 13 éléments pour 12 places. Aucune fonction de hachage n'évitera qu'il y ait au moins une collision, puisqu'il y a plus d'éléments que de cases. On place dans chaque case une liste d'éléments

(27)

Mise en oeuvre :Une définition possible (clés de type int, valeurs de type int) est la suivante :

/* Liste de chainage */

typedef struct s_maillon *p_maillon_t;

typedef struct s_maillon{

int cle;

int valeur;

p_maillon_t suivant;

} maillon_t;

/* Table */

typedef struct s_table *p_table_t;

typedef struct s_table{

int n;

p_maillon_t *alveoles;

} table_t;

Fonction de hachage : Une fonction de hachage rudimentaire peut être la suivante.

int h(int m, char *cle){

int x = 0;

while (*cle){

x += *cle;

cle++;

}

return cle % m;

}

Algorithme d’insertion d’un élément

void ajout(p_table_t t, char *cle, void *objet) {

int a = 0;

p_maillon_t m = NULL;

a = h(t->m, cle);

m = t->alveoles[a];

while (m){

if (strcmp(m->cle, cle) == 0) break;

m = m->suivant;

} if (!m){

(28)

m = malloc(sizeof(maillon_t));

m->cle = malloc(1 + strlen(cle));

strcpy(m->cle, cle);

m->suivant = t->alveoles[a];

t->alveoles[a] = m;

}

m->valeur = objet;

}

Algorithme de recherche d’un élément void *recherche(p_table_t t, char *cle){

int a = 0;

p_maillon_t m = NULL;

a = h(t->m, cle);

m = t->alveoles[a];

while (m){

if (strcmp(m->cle, cle) == 0) return m->valeur;

m = m->suivant;

}

return NULL;

}

Calcul de la complexité: Considérons dans un premier temps le cas de l’insertion d’un élément.

Observons que l’insertion qu’elle se fait à la fin de la liste chaînée correspondante. Il n’est pas difficile de remarquer que la complexité de cette opération est proportionnelle à la longueur L(i) de cette liste i (compris entre 0 et m-1).

Dans le cas défavorable, l’insertion est en O(n). En effet, considérons le cas où toutes les insertions se font à partir d’une même entrée du tableau. Cela aura pour effet de créer une seule liste de n éléments.

Dans le cas moyen, la situation est bien meilleure. Supposons que la probabilité que l’entrée i du tableau soit la position où l’insertion va se faire est de 1/m. la complexité dans le cas moyen est alors (revoir la définition donnée dans le chapitre 1) comme suit :

=

=

1

0

) 1 (

) (

m

i

moy

L i

n m t

Comme la somme sur L(i) représente le nombre d’éléments n existant dans toute la structure, on déduit que l complexité dans le cas moyen est

m n n tmoy( )=

(29)

Dans le cas où il s’agit de l’algorithme de recherche d’un élément dans la table, il y a lieu de distinguer deux cas :

1. L’élément à rechercher n’existe pas: La liste chaînée i est balayée, dans ce cas, jusqu’à la fin. Ceci revient à faire le même travail qu’une insertion d’un élément. Autrement dit, la complexité est :

m n n tmoy( )=

2. L’élément à rechercher existe: Si la clé k recherchée a été la ième clé insérée (événement de probabilité 1/n), la longueur moyenne de liste correspondante, comme on vient de le voir, juste avant cette insertion est (j-1)/m. Donc, le nombre moyen de la recherche de la clé k est 1+ (j- 1)/m. Comme la clé k peut varier de 1 à n, il en résulte que le nombre moyen de clés examinées lors d’une recherche fructueuse

=

+ −

− = +

= n

j

moy m

n m

j n n

t

1 2

1 1 1) 1 1 ( ) (

Quand n est proche de m, ces complexités deviennent des constante c’est-à-dire en θ(1).

Définition : On appelle taux ou facteur de remplissage m

= n α .

4.3.1.2 Le hachage coalescent (fusion)

Si l'allocation dynamique n'est pas possible, on réserve un espace mémoire de taille fixe, en deux parties :

la zone d'adresses primaires de taille p

la réserve (pour les collisions) de taille r total : m = p + r

la fonction h retourne un entier entre 1 et p

En cas de collision, on remplit la réserve de bas en haut (suivant des adresses décroissantes).

Exemple : 7 éléments, 9 places dont 3 pour la réserve

(30)

Si la réserve est trop petite, elle est trop vite remplie. Si elle est trop grande, on diminue l'effet de dispersion du hachage. On intègre donc la réserve au tableau lui-même, en commençant par la fin du tableau. Soit l’exemple suivant de 10 éléments, 11 places.

En rouge :

élément à sa place En noir :

élément placé après une collision

les listes d'éléments de codes 5, 9 et 11 fusionnent :

Notons que la suppression d'un élément est compliquée, comme dans toutes les méthodes de hachage. On préfère marquer la case "libre" sans rompre les chaînages, pour pouvoir la réutiliser.

4.3.2. Les méthodes directes

Si les liens du chaînage prennent trop de place en mémoire, on préfère résoudre les collisions par calcul. On définit une fonction des essais successifs :

x { f1(x), f2(x), ..., fm(x) }: permutation de { 1, 2,. .., m } ou de { 0, 1, ..., m-1 }.

Pour placer x, on essaie dans l'ordre les cases f1(x), f2(x), ..., jusqu'à ce qu'on trouve une case libre (sauf si le tableau est plein).

4.3.2.1. Le hachage linéaire

S'il y a collision dans la case i, on essaie la case i+1 modulo m (m étant la taille du tableau).

(31)

f1(x) = h(x)

f2(x) = ( h(x) + 1 ) mod m .………….

fi(x) = ( h(x) + i-1 ) mod m Exemple : m = 10

Formation de groupements contigus, pas de dispersion lors du deuxième hachage.

4.3.2.2. Le double hachage

fi(x) = ( h(x) + d(x) (i-1) ) modulo m

d(x) est une deuxième fonction de hachage.

Si d(x) = k, on observe des groupements de k en k (si k = 1, on retrouve le hachage linéaire).

Il est préférable que d(x) soit premier avec m. Par exemple, on choisit : m premier (proche d'une puissance de 2) ou m = 2p et d(x) impair

4.3.2.3. Algorithme de recherche avec hachage linéaire

La table de hachage est implémentée comme un tableau t dont les cases vont contenir les objets.

Initialement, la table est vide, c'est-à-dire chaque case contient un objet spécial vide (par exemple, un objet dont la clé est la chaîne vide "").

const data empty_data = {""} ; bool is_empty(data d) { return d.key.length() == 0;

}

Insertion d’un élément

Si un objet de clé doit être inséré, et que t[h(x)] est vide, alors l'insertion se fait à cette place. Si en revanche t[h(x)] est déjà occupé, et que le contenu a une clé différente de , alors on calcule

(32)

des valeurs de hachage supplétives h1(x),h2(x),..., jusqu'à ce que l'on trouve t

[ ]

hix) vide ou contenant un objet de clé .

Pour générer ces nouvelles valeurs de hachage, la méthode la plus simple est le hachage linéaire, qui choisit hi(x)=(h(x)+i)modm.

bool insert(const & d, int m, const t[]) {

int v = h(d.key);

int i;

for (i=0; i<m; i++) {

if (is_empty (t[(v+i)%m])) {

t[(v+i)%m] = d; // insertion à la ième place disponible break;

} else if (t[(v+i)%m].key == d.key) {

break; // ou bien, mise à jour des autres champs }

}

if (i == m) {

return false; // table pleine, insertion non réalisée } else {

return true; // objet déjà dans la table ou inséré }

}

Recherche d’un élément : De même, pour chercher un objet de clé , on teste les objets en t[h(x)], et éventuellement en t

[

h1(x)

] [

,t h2(x)

]

, etc, jusqu'à ce que la clé de l'objet qui s'y trouve soit égale à , ou bien que l'objet soit vide. Dans le cas où la table permet aussi des suppressions, il faut remplacer un objet supprimé par un objet spécial supprimé, distinct de l'objet vide. En insertion, on utilisera la première case vide ou supprimé, tandis qu'en recherche, on ne s'arrêtera qu'à la première case vide.

datasearch(const string s, int m, const t[]) { int v = h(s); int i;

for (i=0; i<m; i++) {

if (is_empty (t[(v+i)%m])) { return empty_data;

} else if (t[(v+i)%m].key == s) { return t[(v+i)%m];

} }

return empty_data; // objet non trouvé }

(33)

4.3.2.3 Complexité de l’algorithme d’insertion: Il est clair que dans le pire des cas, la complexité est en O(n): considérons où n = m-1 et, lors de l’insertion ou de la recherche d’un élément, il y a O(n) collisions.

Dans le cas moyen, mettons nous dans la situation où la fonction de hashage place d’une manière uniforme les éléments de la table. Dans ce cas, procédons comme suit:

La probabilité p1 d’avoir une collision à la première itération est : m

p1 = n

La probabilité p2 d’avoir deux collisions est la probabilité d’avoir une collision à la première itération. Notre fonction de rehashage multipliée par la probabilité d’avoir une autre collision sur les (m-1) entrées et les (n-1) éléments du tableau. Autrement dit:

1 1

2

× −

= m

n m p n

En procédant de la même manière, on obtient :

1 .... 1

1 1

+

− +

× −

− ×

× −

= m i

i n m

n m pi n

Par définition, nous avons :

=

= n

i i

moy n ip

T

1

) ( Si on approxime

i

i m

p n

 

= pour de grande valeurs de n, on obtient

n m n m Tmoy

= − ) (

Note : La valeur exacte est :

1 ) 1

( − +

= + n m n m

Tmoy

4.3.2.4. Complexité de l’algorithme de recherche d’un élément: Par analogie à l’analyse qu’on effectuée dans le cas de la méthode de hachage avec chaînage séparé (voir ci-dessus), on obtient : 1. L’élément à rechercher n’existe pas :

1 ) 1

( − +

= + n m n m Tmoy

2. L’élément à rechercher existe :

= − +

= n +

i

moy m i

m n n

T

1 1

1 ) 1

(

(34)

En approximant une somme par une intégrale et en utilisant les développements limitées de la fonction logarithme, on arrive au résultat (voir Aho et al., 1983) pour de plus amples informations):

1 1 )

( = + +

m n n

Tmoy

Quand n est proche de m, ces complexités deviennent des constantes, c’est-à-dire enθ(1).

4.4. Résumé

Lorsque le nombre d’éléments d’un univers U des clés est trop grand pour être représenté intégralement, par exemple dans le cas où les clés sont des mots ou des numéros de sécurité sociale. Dan ce cas, on utilise une table de hachage de dimension m beaucoup plus petite que le cardinal de U et d’une fonction de hachage h associant à chaque élément de U un indice de la table. Les opérations fondamentales dans la gestion d’une table de hachage sont l’insertion, la recherche et la suppression. Les méthodes de hachage donnent des résultats excellents en moyenne θ(1)), mais lamentables dans le pire cas θ(n), car il n'est pas possible d'éviter les collisions. En particulier, le choix de la fonction de hachage est fondamental. En terme de complexité, le hachage avec chaînage séparé est mieux que le hachage coalescent qui est mieux que le double hachage, qui est mieux que le hachage linéaire.

Références :

1. D. Rebaïne (2000) : une introduction à l’analyse des algorithmes, ENAG Éditions.

2. Cormen et al. (1990): Algorithms, MacGrw Hill.

3. E. Horowitz et al. (1996): Computer algorithms in C++, Computer Science Press.

4. E. Horowitz, S. Sahni (1976): Data structures,

Références

Documents relatifs

Dans la fonction publique de l’État, le salaire mensuel brut moyen est plus élevé parmi les corps et emplois à la décision du gouvernement (12 084 euros) que parmi les autres

sexe et poste : deux variables de type caractère ('f' pour féminin, 'm' pour masculin; 'a' pour analyste, 'p' pour programmeur, 'o' pour opérateur).. numero : numéro de l'employé

On y relève les précisions suivantes sur le secteur d'activité des coiffeurs à domicile; 'L'exercice de la profession de coiffeur dans les hôpitaux ou maisons de retraite, dans

Considérant que, statuant sur les pourvois formés par les sociétés Les Grands Moulins Storion, Les Grands Moulins de Paris, Les Grands Moulins de Strasbourg, Les Grands Moulins

Considérant que, par l'arrêt susvisé, la Cour de cassation (chambre commerciale, économique et financière) a, sur les pourvois formés par la chambre syndicale

Reneault montrent que la société Simat a participé à une réunion des négociants en produits en béton qui s'est déroulée à Millau le 24 juillet 1991, à laquelle étaient

La politique national de la promotion national de la femme constitue le cadre stratégique de contrôle et de mise en œuvre de ces engagements et notamment des

En utilisant les variations de la fonction | = x2, déterminer l'ensemble dans lequel y prend ses valeurs, suivant.les conditions données pour x.. En utilisant