Série 4 – Diagrammes tige et feuille : corrigé
Introduction
Le code du corrigé est fourni sous la forme d'une archive Zip, et des explications concernant les principales difficultés des exercices sont données ci-dessous.
Exercice 1 : Diagramme unidirectionnel
La méthode print
est composée de deux étapes distinctes :
- la création de la table associant la liste des feuilles aux tiges,
- l'affichage du diagramme correspondant à cette table.
La première étape peut être confiée à une méthode séparée, nommée stems
ci-dessous, d'une part pour des raisons de clareté, et d'autre part pour permettre sa réutilisation pour l'exercice 2.
La méthode stems
parcourt les données, et pour chacune d'entre elles calcule la tige (stem
) et la feuille (leaf
). Cela fait, elle regarde si la tige est déjà présente dans la table, et si tel est le cas, ajoute la feuille à sa liste. Sinon, elle ajoute à la table une nouvelle association entre la tige et une liste composée uniquement de la feuille. Une fois la totalité des données insérées dans la table, les listes des feuilles sont triées :
Map<Integer, List<Integer>> stems(Collection<Integer> is){ Map<Integer, List<Integer>> stems = new HashMap<>(); for (int i: is) { int stem = i / 10, leaf = i % 10; if (stems.containsKey(stem)) stems.get(stem).add(leaf); else stems.put(stem, new ArrayList<>(Arrays.asList(leaf))); } for (List<Integer> leaves: stems.values()) Collections.sort(leaves); return stems; }
En plus de la méthode stems
, on peut écrire deux autres méthodes auxiliaires réutilisables pour l'exercice 2. La première permet d'obtenir la chaîne correspondant aux feuilles d'une tige donnée, éventuellement renversée (ce qui est utile pour l'exercice 2) :
String leavesString(Map<Integer, List<Integer>> stems, int stem, boolean reversed) { StringBuilder b = new StringBuilder(); List<Integer> leaves = stems.getOrDefault(stem, Collections.emptyList()); for (Integer i: leaves) b.append(i); return (reversed ? b.reverse() : b).toString(); }
La seconde permet d'obtenir la chaîne de formattage à passer à la méthode format
pour aligner les tiges, étant donné la valeur de la plus grande tige. Cette chaîne de formattage est composée du caractère %
, suivi de la largeur maximale des tiges, suivi du caractère d
. La largeur maximale s'obtient simplement en arrondissant vers le haut le logarithme en base 10 de la plus grande tige :
String formatForMaxStem(int maxStem) { return "%" + (int)Math.ceil(Math.log10(maxStem)) + "d"; }
Ces méthodes écrites, la méthode print
est simple à réaliser. Il faut toutefois penser à énumérer toutes les tiges entre la plus petite et la plus grande (au lieu d'itérer sur les clefs de la table stems
) afin d'éviter les trous dans le diagramme :
void print(Collection<Integer> data) { Map<Integer, List<Integer>> stems = stems(data); int minStem = Collections.min(stems.keySet()); int maxStem = Collections.max(stems.keySet()); String stemFmt = formatForMaxStem(maxStem); for (int stem = minStem; stem <= maxStem; ++stem) { System.out.println(String.format(stemFmt, stem) + '|' + leavesString(stems, stem,false)); } }
Utilisation de computeIfAbsent
Comme sa documentation l'explique, la méthode computeIfAbsent
regarde si la table contient déjà la clef qu'on lui passe, et si tel est le cas, la retourne. Sinon, elle utilise la fonction qu'on lui passe, appliquée à la clef, pour obtenir une nouvelle valeur, insère l'association (clef, valeur) dans la table, puis retourne la valeur.
Ce comportement est exactement celui que nous avons exprimé plus haut au moyen d'un énoncé if
et des méthodes containsKey
, get
et put
. Dès lors, on peut simplifier le corps de la boucle de la méthode stems
au moyen de computeIfAbsent
ainsi :
Map<Integer, List<Integer>> stems(Collection<Integer> is){ Map<Integer, List<Integer>> stems = new HashMap<>(); for (int i: is) { stems .computeIfAbsent(i / 10, k -> new ArrayList<>()) .add(i % 10); } for (List<Integer> leaves: stems.values()) Collections.sort(leaves); return stems; }
Notez qu'il est de plus possible de simplifier le tri des feuilles, au moyen de la méthode forEach
déjà examinée, en récrivant la boucle de tri ainsi :
stems.values().forEach(Collections::sort);
Comme cet exemple l'illustre, les lambdas (et les références de méthodes dans ce cas précis) permettent souvent de rendre le code très concis.
Exercice 2 : Diagramme bidirectionnel
Le dessin d'un diagramme bidirectionnel n'est pas beaucoup plus compliqué que celui d'un diagramme unidirectionnel une fois les méthodes auxiliaires ci-dessus écrites.
void print(Collection<Integer> left, Collection<Integer> right) { Map<Integer, List<Integer>> lStems = stems(left); Map<Integer, List<Integer>> rStems = stems(right); int lWidth = 0; for (List<Integer> l: lStems.values()) lWidth = Math.max(lWidth, l.size()); String lFmt = "%" + lWidth + "s"; int minStem = Math.min(Collections.min(lStems.keySet()), Collections.min(rStems.keySet())); int maxStem = Math.max(Collections.max(lStems.keySet()), Collections.max(rStems.keySet())); String stemFmt = formatForMaxStem(maxStem); for (int stem = minStem; stem <= maxStem; ++stem) { String lLeaves = leavesString(lStems, stem, true); String rLeaves = leavesString(rStems, stem, false); System.out.println(String.format(lFmt, lLeaves) + '|' + String.format(stemFmt,stem) + '|' + rLeaves); } }