CS-108 — Examen intermédiaire : corrigé
1 Rotation de chaînes
Partie 1
La définition du constructeur et des accesseurs est triviale :
public RotatedString(String string, int index) { if (! (0 <= index && index < string.length())) throw new IndexOutOfBoundsException(); this.string = string; this.index = index; } public String string() { return string; } public int index() { return index; }
Partie 2
La méthode charAt
commence par calculer l'index du caractère désiré dans la chaîne avant rotation, puis appelle la méthode charAt
sur cette chaîne afin de l'obtenir. Pour calculer l'index en question, il suffit d'ajouter l'index du début de la rotation à l'index du caractère, puis de ramener le résultat à une valeur valide au moyen du reste de la division. On obtient donc :
public char charAt(int i) { if (! (0 <= i && i < string.length())) throw new IndexOutOfBoundsException(); return string.charAt((index + i) % string.length()); }
Partie 3
La manière la plus simple de définir la méthode equals
consiste à utiliser la méthode compareTo
et de voir si elle retourne 0. On obtient alors :
@Override public boolean equals(Object thatO) { return (thatO instanceof RotatedString) && (compareTo((RotatedString)thatO) == 0); }
Pour la méthode hashCode
, la principale difficulté consiste à constater que la formule donnée, qui n'est pas totalement triviale, peut se calculer très facilement au moyen d'une boucle :
@Override public int hashCode() { int h = 0; for (int i = 0; i < string.length(); ++i) h = h * 31 + charAt(i); return h; }
Bien entendu, hashCode
utilise la méthode charAt
définie plus haut afin d'obtenir facilement le ie caractère de la chaîne après rotation.
Partie 4
Une manière simple de mettre en œuvre l'ordre lexicographique consiste à parcourir en parallèle les caractères des deux chaînes, en les comparant deux à deux, et en s'arrêtant à la première différence ou lorsque la chaîne la plus courte est épuisée. A ce moment, de deux choses l'une :
- soit on a trouvé une différence entre deux caractères des chaînes, auquel cas il suffit de retourner le résultat de leur comparaison,
- soit on est arrivé au bout de la plus courte des deux chaînes, auquel cas il suffit de retourner le résultat de la comparaison de leur longueur.
@Override public int compareTo(RotatedString that) { int l1 = this.string.length(); int l2 = that.string.length(); for (int i = 0; i < Math.min(l1, l2); ++i) { int cmp = Character.compare(this.charAt(i), that.charAt(i)); if (cmp != 0) return cmp; } return Integer.compare(l1, l2); }
Partie 5
La méthode toString
s'écrit très facilement au moyen d'un bâtisseur de chaîne et de la variante de la méthode append
donnée dans le formulaire :
@Override public String toString() { return new StringBuilder(string.length()) .append(string, index, string.length()) .append(string, 0, index) .toString(); }
Il est également possible d'utiliser la méthode substring
de String
, mais cela est légèrement moins efficace.
Partie 6
Le problème de cette définition de hashCode
est qu'elle n'est pas compatible avec equals
, dans le sens où il est possible d'avoir deux rotations considérée comme égales par equals
mais ayant des valeur de hachage différentes. Il en va par exemple ainsi des rotations o1
et o2
suivantes :
RotatedString o1 = new RotatedString("EPFL", 0); RotatedString o2 = new RotatedString("FLEP", 2);
2 Collisions de hachage
Pour déterminer les collisions de hachage dans une collection de chaînes de manière efficace, on peut dans un premier temps construire une table associant à chaque valeur de hachage différente l'ensemble des éléments de la collection ayant cette valeur de hachage là. Cela fait, il suffit de retourner la liste contenant tous les ensembles de plus d'un élément.
List<Set<String>> hashCollisions(Collection<String> ss) { Map<Integer, Set<String>> hash = new HashMap<>(); for (String s: ss) { if (!hash.containsKey(s.hashCode())) hash.put(s.hashCode(), new HashSet<>()); hash.get(s.hashCode()).add(s); } List<Set<String>> collisions = new ArrayList<>(); for (Set<String> s: hash.values()) { if (s.size() > 1) collisions.add(s); } return collisions; }
A noter qu'en utilisant judicieusement les méthodes computeIfAbsent
, forEach
et removeIf
, et la vue sur les valeurs fournie par la méthode values
, il est possible d'obtenir une autre version de cette méthode, beaucoup plus concise :
List<Set<String>> hashCollisions(Collection<String> ss) { Map<Integer, Set<String>> hash = new HashMap<>(); ss.forEach(s -> hash .computeIfAbsent(s.hashCode(), k -> new HashSet<>()) .add(s)); hash.values().removeIf(s -> s.size() < 2); return new ArrayList<>(hash.values()); }
3 Tri par base
Partie 1
Le premier problème à résoudre pour mettre en œuvre le tri par base est de savoir comment itérer sur les chiffres des nombres, du moins significatif au plus significatif. Pour ce faire, il faut bien entendu utiliser la division entière et son reste : le ne chiffre (depuis la droite) d'un nombre s'obtient en le divisant par 10n puis en obtenant le reste de la division de cette valeur par 10. Dans le code ci-dessous, la variable d
vaut 10n à la ne itération de la boucle.
Pour stocker les sous-listes correspondant aux différents chiffres, on peut utiliser soit un tableau dynamique (comme ci-dessous), soit une table associative, la première solution étant probablement plus rapide.
void sort(List<Integer> ints) { for (int d = 1; d <= 1000000000; d *= 10) { List<List<Integer>> subLists = new ArrayList<>(10); for (int i = 0; i < 10; ++i) subLists.add(new ArrayList<>()); for (int i: ints) { int digit = (i / d) % 10; subLists.get(digit).add(i); } ints.clear(); for (List<Integer> subList: subLists) ints.addAll(subList); } }
Partie 2
Pour utiliser la base 256 (ou n'importe quelle autre base), il faut changer la formule à utiliser pour extraire le ne chiffre, et le nombre de sous-listes. L'intérêt d'utiliser une base qui soit une puissance de deux est que l'extraction du ne chiffre peut se faire au moyen du décalage à droite et du masquage, plutôt qu'au moyen de la division entière et de son reste, plus coûteuse.
La variante ci-dessous de la méthode sort
utilise cette constatation pour effectuer le tri en base 256.
void sortBase256(List<Integer> ints) { for (int b = 0; b < Integer.SIZE; b += 8) { List<List<Integer>> subLists = new ArrayList<>(0xFF); for (int i = 0; i <= 0xFF; ++i) subLists.add(new ArrayList<>()); for (int i: ints) { int digit = (i >> b) & 0xFF; subLists.get(digit).add(i); } ints.clear(); for (List<Integer> subList: subLists) ints.addAll(subList); } }