Ensemble par hachage préservant l'ordre
CS-108 — Corrigé de la série 12
Introduction
Le code du corrigé est disponible dans une archive Zip. Les solutions aux différents exercices sont brièvement discutées ci-dessous.
Exercice 1 : capacité fixe
Les attributs de la classe SOrderPreservingSet1
sont ceux présentés à la figure 2 de l'énoncé, ou presque, puisque la taille n'a pas besoin d'être mémorisée explicitement. En effet, dans cette première version, la taille de l'ensemble est égale à la taille du tableau contenant les éléments.
Dès lors, la méthode size
n'a rien d'autre à faire que retourner la taille du tableau elements
:
public int size() { return elements.size(); }
Pour ajouter un élément à l'ensemble, il faut parcourir l'amas correspondant à l'élément à ajouter, depuis son index (tI
dans le code plus bas). Si, lors de ce parcours, on rencontre un élément égal à celui à ajouter, alors l'élément est déjà dans l'ensemble, et il n'y a rien à faire. Par contre, si on atteint la fin de l'amas, alors on ajoute l'élément au tableau des éléments, et place son index dans le premier emplacement libre après l'amas.
public void add(E e) { int tI = table.indexFor(requireNonNull(e)); for (; table.isNonEmpty(tI); tI += 1) { if (elements.get(table.get(tI)).equals(e)) return; } table.set(tI, elements.size()); elements.add(e); }
Comme on le constate, le parcours de cette portion d'amas est très similaire au parcours de la liste à laquelle appartient l'élément dans la classe SHashSet
du cours.
Pour déterminer si un élément appartient à l'ensemble, il faut faire la même chose que pour ajouter un élément, si ce n'est qu'aucune modification n'est à apporter à l'ensemble.
public boolean contains(E e) { for (int tI = table.indexFor(requireNonNull(e)); table.isNonEmpty(tI); tI += 1) { if (elements.get(table.get(tI)).equals(e)) return true; } return false; }
Finalement, pour créer un itérateur permettant de parcourir les éléments, il suffit d'en créer un sur une vue non modifiable sur le tableau les contenant :
public Iterator<E> iterator() { return Collections.unmodifiableList(elements).iterator(); }
Exercice 2 : redimensionnement
Le redimensionnement de la table se fait de manière assez similaire à ce qui est fait dans la classe SHashSet
du cours, c.-à-d. que le contenu de l'ensemble est parcouru — dans l'ordre d'insertion — et ses éléments ajoutés l'un après l'autre à une table nouvellement créée et destinée à remplacer la précédente.
On notera que lors de cette réinsertion, il faut connaître l'index de chacun des éléments, puisque c'est ce qui est stocké dans la table. Il aurait bien entendu été possible de le stocker dans une variable incrémentée à chaque insertion, mais une autre solution consiste à utiliser un itérateur de type ListIterator
pour parcourir les éléments, qui offre la méthode previousIndex
pour connaître l'index du dernier élément retourné par next
. C'est la solution que nous avons choisi d'utiliser ci-dessous.
private void rehashIfNeeded() { int idealLogCapacity = Table.idealLogCapacity(size()); if (table.logCapacity() >= idealLogCapacity) return; Table<E> newTable = Table.ofLogCapacity(idealLogCapacity); ListIterator<E> elementsIt = elements.listIterator(); while (elementsIt.hasNext()) { int tI = newTable.indexFor(elementsIt.next()); while (newTable.isNonEmpty(tI)) tI += 1; newTable.set(tI, elementsIt.previousIndex()); } table = newTable; }
Exercice 3 : suppression
Comme le dit l'énoncé, la suppression affecte la totalité des opérations, ainsi que les attributs, en tout cas si on la met en œuvre au moyen de la technique proposée, basée sur les « pierres tombales ».
La première chose à faire pour gérer la suppression est d'ajouter un attribut pour mémoriser la taille de l'ensemble, puisque celle-ci n'est plus égale à celle du tableau contenant les éléments — qui pourra désormais contenir des valeurs nulles lors de suppression d'éléments. La méthode size
doit être adaptée en fonction :
private int size = 0; public int size() { return size; }
La méthode add
doit être modifiée pour ignorer les pierres tombales lors du parcours de l'amas. Cette modification est la seule qui soit obligatoire, mais il est avantageux d'en faire une autre, qui consiste à remplacer la première pierre tombale de l'amas par la valeur à ajouter. Pour ce faire, notre version de add
mémorise la position de cette pierre tombale dans la variable firstDeleted
.
public void add(E e) { int tI = table.indexFor(requireNonNull(e)); int firstDeleted = Integer.MAX_VALUE; for (; table.isNonEmpty(tI); tI += 1) { int eI = table.get(tI); if (eI == Table.SLOT_DELETED) firstDeleted = Math.min(firstDeleted, tI); else if (elements.get(eI).equals(e)) return; } table.set(Math.min(firstDeleted, tI), elements.size()); elements.add(e); size += 1; rehashIfNeeded(); }
La méthode contains
doit elle aussi être adaptée pour ignorer les pierres tombales.
public boolean contains(E e) { for (int tI = table.indexFor(requireNonNull(e)); table.isNonEmpty(tI); tI += 1) { int eI = table.get(tI); if (eI != Table.SLOT_DELETED && elements.get(eI).equals(e)) return true; } return false; }
La méthode remove
doit parcourir l'amas contenant l'élément à supprimer de la même manière que add
et contains
. Dès que cet élément est trouvé, il est supprimé en :
- le remplaçant par
null
dans le tableau des éléments, - remplaçant son index dans la table par une pierre tombale — si cet index se trouve au milieu de l'amas — ou en le libérant — si cet index se trouve à la fin de l'amas.
Cela fait, la taille de l'ensemble peut être diminuée d'une unité.
public void remove(E e) { for (int tI = table.indexFor(requireNonNull(e)); table.isNonEmpty(tI); tI += 1) { int eI = table.get(tI); if (eI != Table.SLOT_DELETED && elements.get(eI).equals(e)) { elements.set(eI, null); table.set(tI, table.isNonEmpty(tI + 1) ? Table.SLOT_DELETED : Table.SLOT_EMPTY); size -= 1; break; } } }
La méthode rehashIfNeeded
doit être adaptée afin de :
- prenant en compte les pierres tombales pour décider si un rehachage est nécessaire ou non — en utilisant
elements.size()
, qui inclut les éléments supprimés, plutôt quesize()
, lors du test initial, - supprimant les éléments nuls du tableau des éléments, ce qui se fait facilement au moyen de la méthode
remove
de l'itérateur utilisé pour les parcourir.
private void rehashIfNeeded() { if (table.logCapacity() >= idealLogCapacity(elements.size())) return; Table<E> newTable = Table.ofLogCapacity(idealLogCapacity(size)); ListIterator<E> elementsIt = elements.listIterator(); while (elementsIt.hasNext()) { E e = elementsIt.next(); if (e == null) { elementsIt.remove(); continue; } int tI = newTable.indexFor(e); while (newTable.isNonEmpty(tI)) tI += 1; newTable.set(tI, elementsIt.previousIndex()); } table = newTable; }
La méthode iterator
est de loin la plus compliquée à modifier, puisqu'elle ne peut plus se baser sur un itérateur parcourant les éléments du tableau, pour deux raisons : premièrement, ce tableau contient des éléments nuls, et deuxièmement, la méthode remove
de l'itérateur ne peut pas se contenter de supprimer les éléments du tableau, la table de hachage doit également être mise à jour.
Néanmoins, l'itérateur n'est pas excessivement difficile à écrire car il combine des techniques déjà utilisées précédemment, à savoir :
- l'utilisation d'un attribut nommé
remaining
mémorisant le nombre d'éléments restants à fournir, qui simplifie l'écriture de la méthodehasNext
, - l'utilisation d'un attribut nommé
canRemove
permettant de savoir si un appel àremove
est autorisé, - l'utilisation d'un index nommé
currI
désignant le dernier élément fourni par l'itérateur, et similaire ànextI
utilisé dans d'autres contextes, si ce n'est quenextI
désigne le prochain élément fourni.
Finalement, et comme souvent, le code de la méthode remove
de l'itérateur ressemble beaucoup à celui de la méthode remove
de la classe elle-même. La différence principale est que la méthode remove
de l'itérateur a la garantie que l'élément se trouve dans l'ensemble, ce qui simplifie sa mise en œuvre.
public Iterator<E> iterator() { return new Iterator<>() { int remaining = size(); int currI = -1; boolean canRemove = false; @Override public boolean hasNext() { return remaining > 0; } @Override public E next() { if (!hasNext()) throw new NoSuchElementException(); canRemove = true; remaining -= 1; do currI += 1; while (elements.get(currI) == null); return elements.get(currI); } @Override public void remove() { if (!canRemove) throw new IllegalStateException(); canRemove = false; int tI = table.indexFor(elements.get(currI)); while (table.get(tI) != currI) tI += 1; elements.set(currI, null); table.set(tI, table.isNonEmpty(tI + 1) ? Table.SLOT_DELETED : Table.SLOT_EMPTY); size -= 1; } }; }