Série 6 – Ensembles par hachage : corrigé

Introduction

Le code du corrigé est disponible sous la forme d'une archive Zip, qui contient également le code de l'énoncé. Les solutions aux différents exercices sont brièvement discutées ci-dessous.

Exercice 1 – itérateur d'ensemble

Itérateur statique

Pour parcourir les deux dimensions de la table de hachage, l'itérateur doit logiquement stocker une information par dimension, comme expliqué dans l'énoncé.

La position dans la première dimension est stockée dans un entier nommé i, initialisé à 0. La position dans la seconde dimension est quant-à-elle stockée sous la forme d'un itérateur nommé it et initialisé avec un itérateur sur la première liste, c-à-d celle qui apparaît à la position 0 dans la table.

Ces deux informations sont liées par l'invariant suivant : l'itérateur it est toujours un itérateur sur la liste de position i dans la table de hachage.

La méthode next de l'itérateur commence par appeler la méthode hasNext de l'itérateur it afin de savoir s'il reste des éléments dans la liste d'indice i. Tant que ce n'est pas le cas, elle passe à la liste suivante en incrémentant i et en obtenant un itérateur sur la liste d'après.

Une fois cela fait, il est garanti que l'itérateur it possède encore un élément. Celui-ci est obtenu avec la méthode next de it et directement retourné.

Si la compréhension de cette technique d'itération vous pose problème, pensez d'abord à un cas légèrement plus simple, à savoir l'itération sur un tableau bidimensionnel. Dans ce cas, les deux dimensions sont parcourues chacune par un indice entier (p.ex. i pour la première, j pour la seconde). Une fois que vous avez bien compris cette version simplifiée, la version décrite ci-dessus où l'itérateur joue le rôle du second indice (j dans l'exemple) devrait être plus facile à comprendre.

En combinant ces différentes observations, on obtient le code suivant pour la classe de l'itérateur (qui est bien entendu imbriquée dans la classe SimpleHashSet, qui n'est pas montrée ci-dessous pour alléger la présentation) :

private static final class SHSIterator<E> implements Iterator<E> {
    private final ListSet<E>[] table;
    private int remaining;
    private int i;
    private Iterator<E> it;

    public SHSIterator(ListSet<E>[] table, int size) {
        this.table = table;
        this.remaining = size;
        this.i = 0;
        this.it = table[i].iterator();
    }

    @Override
    public boolean hasNext() {
        return remaining > 0;
    }

    @Override
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        --remaining;
        while (! it.hasNext())
            it = table[++i].iterator();
        return it.next();
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

La solution pour cette première version se trouve dans la classe SimpleHashSet.

Itérateur non statique

En rendant l'itérateur non statique, on a la possibilité de simplifier passablement son code puisqu'il n'a plus besoin de constructeur pour obtenir la table et sa taille, ni de paramètre de type. Les uns et les autres sont en effet accessibles directement via l'instance englobante.

La méthode remove devient particulièrement simple à mettre en œuvre, car il lui suffit d'appeler la méthode remove de l'itérateur it avant de décrémenter la taille de l'ensemble, stockée dans le champ size de l'instance englobante. La version statique de l'itérateur n'avait pas accès à ce champ.

Notez que contrairement à la méthode remove de la classe SimpleHashSet elle-même, qui ne sait pas si l'élément qu'elle a reçu est bien dans l'ensemble et doit donc ruser un peu pour mettre à jour la taille, la méthode remove de l'itérateur supprime toujours un élément, le dernier retourné par next, et peut donc sans hésiter décrémenter la taille.

private final class SHSIterator implements Iterator<E> {
    private int remaining = size;
    private int i = 0;
    private Iterator<E> it = table[i].iterator();

    @Override
    public boolean hasNext() { /* comme avant */ }

    @Override
    public E next() { /* comme avant */ }

    @Override
    public void remove() {
        it.remove();
        --size;
    }
}

La solution pour cette seconde version se trouve dans la classe SimpleHashSet2.

Itérateur anonyme

Pour rendre l'itérateur anonyme il suffit de déplacer son corps après l'énoncé new de la méthode iterator de la classe SimpleHashSet. On obtient donc le code suivant :

public class SimpleHashSet3<E> implements Set<E> {
    private int size;
    private ListSet<E>[] table;

    // … autres méthodes

    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private int remaining = size;
            private int i = 0;
            private Iterator<E> it = table[i].iterator();

            @Override
            public boolean hasNext() { /* comme avant */ }

            @Override
            public E next() { /* comme avant */ }

            @Override
            public void remove() { /* comme avant */ }
        };
    }

    private ListSet<E> listFor(E elem) {
        return table[Math.abs(elem.hashCode() % table.length)];
    }
}

La solution pour cette troisième version se trouve dans la classe SimpleHashSet3.

Exercice 2 – rehachage

Pour mettre en œuvre le rehachage, il faut après chaque ajout recalculer le facteur de charge et, si celui-ci est au-dessus de la limite, créer une nouvelle table, y replacer les éléments de la table actuelle, et finalement utiliser cette nouvelle table en lieu et place de l'ancienne. Ces changements doivent bien entendu être apportés à la méthode add, responsable de l'ajout, qui appelle la nouvelle méthode rehash pour effectuer le rehachage si nécessaire.

La méthode rehash alloue une nouvelle table de bonne taille et y replace tous les éléments de la table actuelle, obtenus en parcourant l'ensemble (c-à-d this) au moyen d'une boucle for-each. Etant donné que la création d'une table est également faite dans le constructeur de la classe, il est intéressant de factoriser ce code commun dans une méthode privée (et statique) nommée newTable. D'autre part, étant donné que rehash doit calculer la position des éléments dans une nouvelle table de hachage, elle ne peut pas directement utiliser la méthode listFor de SimpleHashSet, qui référence la table actuelle. Une bonne manière de résoudre ce problème est de changer la définition de listFor pour en faire une méthode statique qui prend explicitement la table en argument.

On obtient donc finalement le code suivant (toutes les parties non liées au rehachage ont été supprimées) :

public class SimpleHashSet4<E> implements Set<E> {
    private static final double IDEAL_LOAD_FACTOR = 0.75;
    private static final double MAX_LOAD_FACTOR = 1;
    private static final int MIN_CAPACITY = 10;

    private int size;
    private ListSet<E>[] table;

    public SimpleHashSet4() { /* … */ }

    // … autres méthodes

    @Override
    public void add(E newElem) {
        // … début comme avant
        double loadFactor = (double)size / (double)table.length;
        if (loadFactor > MAX_LOAD_FACTOR)
            rehash();
    }

    private void rehash() {
        ListSet<E>[] newTable =
            newTable((int)(size / IDEAL_LOAD_FACTOR));
        for (E elem: this)
            listFor(newTable, elem).add(elem);
        table = newTable;
    }

    private static <E> ListSet<E>[] newTable(int capacity) {
        @SuppressWarnings("unchecked")
        ListSet<E>[] table = new ListSet[capacity];
        for (int i = 0; i < table.length; ++i)
            table[i] = new ListSet<>();
        return table;
    }

    private static <E> ListSet<E> listFor(ListSet<E>[] table,
                                          E elem) {
        return table[Math.abs(elem.hashCode() % table.length)];
    }
}

La solution pour cette quatrième et dernière version se trouve dans la classe SimpleHashSet4.