Le but de ces deux exercices est d'implanter une représentation d'un ensemble d'éléments (de n'importe quel type) sans doublons.
Comme les éléments ne sont pas forcément consécutifs, il n'est pas possible d'utiliser un tableau
ou un bitset pour représenter les valeurs (explosion en mémoire) ni une liste (algos d'ajout/recherche/suppression en O(n)),
on utilisera donc une table de hachage où les collisions sont stockées dans une liste chaînée.
La taille de la table de hachage sera toujours une puissance de 2 (2, 4, 8, 16, 32, etc...)
pour permettre l'écriture d'une fonction de hachage rapide.
Le but du TD est de ré-écrire une table de hachage, pas d'en utiliser une
qui existe déjà, donc pour ce TD, nous n'utiliserons aucune des collections de java.util.
Pour ce TP, nous allons utiliser Maven avec une configuration (le
pom.xml)
très similaire à celle habituelle.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>fr.uge.set</groupId>
<artifactId>set</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.14.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
<configuration>
<release>27</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</build>
</project>
Comme précédemment, créer un projet Maven,
au niveau du premier écran, cocher
create simple project
puis passer à l'écran suivant en indiquant
Next.
Pour ce TP, le groupId est
fr.uge.set , l'artefactId est
set et
la version est
0.0.1-SNAPSHOT. Puis cliquer sur
Finish.
Dans ce TP, une table de hachage est un tableau contenant des listes chaînées.
Lorsque l'on veut insérer/rechercher un élément, on va dans un premier temps
trouver dans quelle case du tableau doit se situer l'élément puis dans
un second temps, on va parcourir la liste chaînée. En cas d'insertion, si l'élément
n'existe pas, on va l'insérer en tête de la liste, sinon on ne fait rien.
En cas de recherche, on renvoie vrai si l'élément fait partie de la liste chaînée.
Pour trouver la case de tableau, on va demander à l'élément de renvoyer son
hashCode, puis on choisira la case du tableau avec une opération de modulo (% comme en C).
Par exemple, si on a un tableau de 4 cases, et que l'on veut insérer "Jane",
le hashCode de "Jane" est 2301262, modulo 4 cela fait 2, donc
il faut ajouter dans la liste chaînée à la case 2, un maillon (
Entry)
contenant "Jane".
On obtient la table de hachage suivante
0: [null]
1: [null]
2: [] -> Entry("Jane", null)
3: [null]
Si l'on veut maintenant insérer "John", le hashCode de "John" est 2314539,
modulo 4, on obtient 3. On insère donc le maillon contenant "John" comme ceci
0: [null]
1: [null]
2: [] -> Entry("Jane", null)
3: [] -> Entry("John", null)
Enfin, si l'on veut ajouter "Arthur", la hashCode est 1969735650, modulo 4
cela fait 2, donc on ajoute le maillon contenant "Arthur" devant le maillon
contenant "Jane".
0: [null]
1: [null]
2: [] -> Entry("Arthur", ) -> Entry("Jane", null)
3: [] -> Entry("John", null)
En pratique, on utilise rarement le modulo, car le hashCode peut être négatif et
le modulo d'un nombre négatif est négatif. On s'arrange pour que la taille de la table
soit une puissance de deux car dans ce cas,
h % length est équivalent
à
h & (length - 1) (et % est aussi une opération assez lente, enfin comparée à &).
Reste à savoir comment on dimensionne le tableau en fonction du nombre d'éléments...
Il y a plusieurs stratégies, celle considérée comme efficace, mais au détriment de la taille en mémoire,
consiste à avoir toujours au moins 2 fois plus de cases du tableau que d'éléments.