Slide: 1


Une application concrète des arbres


Diversion
Lire et écrire les fichiers.


Slide: 2


Du général au particulier
Prenons un peu d'avance sur le cours ! Les automates (finis) sont caractérisés par : Voici par exemple l'automate qui reconnaît le mot coucou.


    

Slide: 3


Plusieurs mots à la fois




Mots reconnus
cas, ce, ces, ci, sa, sac, sec.

Tous les chemins qui mènent à un état « final » (sommet gris).

(Un tel arbre s'appelle parfois un trie).


Slide: 4


Réalisation des tries
Il y a plusieurs techniques, certaines très sophistiquées. Ici, pour simplifier la programmation :

Slide: 5


Arbre en machine
class Tree { char val ; Tree down, next ; Tree(char val) { this.val = val ; } Tree(char val, Tree down, Tree next) { this.val = val ; this.down = down ; this.next = next ; } }


Cette structure ressemble fortement à un arbre binaire, mais attention les deux fils (gauche et droit) jouent des rôles distincts :

Slide: 6


Une autre interprétation, parfois utile
Un dictionnaire est une liste de paires (char × dictionnaire), (c1, D1) ; ⋯ ; (cn, Dn) avec :

Dictionnaire vide
Il n'existe pas (i.e. notre construction va le garantir).

Slide: 7


Test d'appartenance
Cette dernière interprétation est particulièrement utile pour programmer les tests d'appartenance :

En toute logique, pour savoir si un mot w est dans un dictionnaire (c1, D1) ; ⋯ ; (cn, Dn), on doit (surtout) :

Slide: 8


Chaînes terminées par une marque
La marque ne peut plus apparaître dans les chaînes. Choisissons le charactère de code zéro (comme en C).

On veut extraire les caractères simplement.
class Tree { … final static char EOW = '\0' static char getChar(String s, int i) { return i < s.length() ? s.charAt(i) : EOW ; } }
Donc, au lieu de s.charAt(i) on fera getChar(si).


Slide: 9


Test d'appartenance, première version
static boolean mem(String s, Tree t) { char c = getChar(s,0) ; if (c == EOW) { return t.val == EOW ; // car liste triée (et non-vide) } else { for ( ; t != null ; t = t.next) { if (c == t.val) { return mem(s.substring(1), t.down) ; // s.substring(1) renvoie s moins son premier caractère. } else if (c < t.val) { return false ; // car liste triée } } return false ; } }



Slide: 10


Diversion

Slide: 11


Appartenance, seconde version
static boolean mem(String s, int i, Tree t) { char c = getChar(s,i) ; // premier caractère du suffixe if (c == EOW) { return t.val == EOW ; // car liste triée } else { for ( ; t != null ; t = t.next) { if (c == t.val) return mem(s, i+1, t.down) ; else if (c < t.val) return false ; // car liste triée } return false ; } } /* Conserver une interface simple */ static boolean mem(String s, Tree t) { return mem(s, 0, t) ; }



Slide: 12


Appartenance, code itératif
static boolean mem(String s, int i, Tree t) { char c = getChar(s, i) ; while (t != null) { if (c < t.val) { return false } ; // aucun espoir else if (c > t.val) { t = t.next ; } // à droite else { // c == t.val, finir ou descendre if (c == EOW) { return true ; } else { i++ ; c = getChar(s, i) ; t = t.down ; } } } return false ; }



Slide: 13


Si on adopte le point de vue « arbre binaire »
Le code est peut-être moins simple à comprendre...
static boolean mem(String s, int i, Tree t) { if (t == null) { // Cas possible (fin de liste de paires) return false ; } else { char c = getChar(s, i) ; if (c < t.val) { return false ; } else if (c > t.val) { // à droite, i inchangé return mem(s, i, t.next) ; } else { // c == t.val // en bas, i incrémenté return c == EOW || mem(s, i+1, t.down) ; } } }



Slide: 14


Fabriquer le dictionnaire
Pour fabriquer le dictionnaire nous allons lire une liste de mots, à raison d'un mot par ligne. Par exemple :
# cat /usr/share/dict/words
a
à
abaissa
abaissaient
abaissais
abaissait
abaissant
abaisse
abaissé
....

Slide: 15


Diversion, comment lire un fichier
En Java, pour lire un fichier...

Slide: 16


Entrées/Sorties bufferisées
Que veut dire « Buffered » dans BufferedReader ?

Il faut savoir qu'un accès au disque est beaucoup plus coûteux qu'un accès en mémoire, et à peu près indépendant du nombre de caractères transmis, dans une certaine limite.

Dans le BufferedReader il y a une zone de mémoire appelée buffer (tampon). Quand on veut lire, disons, n caractères : Avantage : efficacité.

Inconvénient : quand on écrit, on écrit en mémoire et pas sur le disque. Il faut penser à vider le tampon (à la fin du traitement).


Slide: 17


Diversion, encore
Comment fabriquer les Reader etc. Bonus ! Il y a du sous-typage ici : les divers, FileReader, InputStreamReader etc. sont considérés comme de type Reader (de même tout objet est un Object).


Slide: 18


Création du dictionnaire
Assez classiquement, on va ajouter tous les mots lus un par un.
static Tree build(BufferedReader in) { Tree r = null ; String line ; try { while ((line = in.readLine()) != null) { // Idiome ! r = add(r, line) ; } } catch (IOException e) { System.err.println(e.getMessage()) ; System.exit(2) ; } return r ; } // Utiliser une méthode auxiliaire (cf. mem) static Tree add(Tree t, String s) { return add(t, s, 0) ; }

Slide: 19


Principe de l'addition
On veut ajouter le mot w à un dictionnaire D = (c1, D1) ; ⋯ ; (cn, Dn).
Code
static Tree add(Tree t, String s, int i) {// Attention! t peut être vide char c = getChar(s, i) ; if (c == EOW) { if (t != null && t.val == EOW) { // Déjà là. return t ; } else { // Ajouter effectivement le mot vide. return new Tree(EOW, null, t) ; } } else { return addInList(t, c, s, i+1) ; }} // ne renvoie jamais null



Slide: 20


Ajouter dans la liste de paires
Un principe, ajouter cw' dans D, démarche inductive.
Code
static Tree addInList(Tree t, char c, String s, int i) { if (t == null || c < t.val) { // Ajouter devant return new Tree(c, add(null, s, i), t) ; } else if (c == t.val) { // Completer un sous-dictionnaire return new Tree(c, add(t.down, s, i), t.next) ; } else { // ajouter plus loin return new Tree(t.val, t.down, addInList(t.next, c, s, i)) ; }}



Slide: 21


Afficher le dictionnaire
Il « suffit » d'afficher tous les chemins qui mènent à '\0'.

Avec le point de vue « arbre binaire », code très concis.
static void print(PrintWriter out, Tree t, CharList path) { if (t != null) { char c = t.val ; if (c == EOW) { CharList.revPrintln(out, w) ; } else { print(out, t.down, new CharList(c, path)) ; } print(out, t.next, path) ; } }



Slide: 22


Comment afficher sur la sortie standard

Slide: 23


Afficher le dictionnaire, avec une pile de caractères
private static Stack path = new Stack () ; // NB: private private static void doPrint(PrintWriter out, Tree t) { if (t != null) { char c = t.val ; if (c == EOW) { out.println(path) ; // affiche le contenu de la pile } else { path.push(c) ; doPrint(out, t.down) ; path.pop() ; } doPrint(out, t.next) ; } }
Avantage :

Slide: 24


Mission de la méthode d'affichage
Quand on lui donne l'argument D et que la pile contient le mot p, l'appel de print doit : Voyons l'appel récursif.
// La pile contient p path.push(c) ; // La pile contient p⋅c doPrint(out, t.down) ; // La pile contient p⋅c (induction) path.pop() ; // La pile contient p, CQFD.



Slide: 25


Initialisation de la pile
La pile est une variable globale.
private static Stack path = new Stack () ;


La pile étant créée vide, on peut écrire.
static print(PrintStream o, Tree t) { // Pas de private PrintWriter out = new PrintWriter (o) ; // La pile est vide. doPrint(out, t) ; // La pile est vide (rendue vide par doPrint). out.flush() ; }


Mais, on peut juger prudent d'écrire. Pourquoi ? exception dans doPrint attrapée par l'appelant de print
static print(PrintStream o, Tree t) { path = new Stack () ; … // Comme ci-desuss



Slide: 26


Au fait, on aurait pu écrire...
En prenant le point de vue D = (c1, D1) ; ⋯ ; (cn, Dn).
private static void doPrint(PrintWriter out, Tree t) { if (t.val == EOW) { out.println(path) ; t = t.next ; } for ( ; t != null ; t = t.next) { path.push(t.val) ; doPrint(out, t.down) ; path.pop() ; } } }
Une boucle dans une récursion n'a rien d'effrayant.

Une question simple pour finir
Que se passe-t-il si on compose build et print :

Slide: 27


Sauvegarder/Relire le dictionnaire
On pourrait relire la liste de mots et construire l'arbre pour chaque usage.

Mais on peut procéder plus directement En outre le dictionnaire sera (un peu) compressé.
# ls -l fr fr.z
-rw-r--r--    1 maranget moscova   1639367 Oct 14 12:39 fr.z
-rw-r--r--    1 maranget moscova   2830676 Oct  8 19:41 fr

Slide: 28


Sauvegarder


final static char NULL = '\001' ; static void save(Writer out, Tree t) throws IOException { if (t == null) { out.write(NULL) ; } else { out.write(t.val) ; save(out, t.down) ; save(out, t.next) ; } }


Note Pour écrire dans un fichier caractère par caractère, on utilisera un Writer et sa méthode write, fabriqué tout à fait comme les Reader (Il y a des BufferedWriter, FileWriter etc, cf. la Doc).


Slide: 29


Relire
static Tree read(Reader in) throws IOException { int c = in.read() ; if (c < 0) { System.err.println("Fichier tronqué") ; System.exit(2) ; } char x = (char)c ; if (x == NULL) { return null ; } else { Tree down = read(in) ; Tree next = read(in) ; return new Tree (x, down, next) ; } }



Slide: 30


Première application : assistant de mots croisés
Quand on remplit une grille de mots croisés, on cherche les mots : Nous sommes en mesure de traiter les deux premiers points.

Un motif est un mot dont on ignore certaines lettres (remplacées par '?'). Par ex :

Slide: 31


Une méthode
Un exemple (dictionnaire français de ∼ 260000 mots) :
#java Tree -pat 'ab????z'
abattez
abjurez
ablatez
abondez
abonnez
abordez
aboutez
aboyiez
abrasez
abritez
abrogez
abrégez
abusiez
abîmiez
On peut faire bien mieux : parcourir l'arbre (cf. print) en se limitant aux mots filtrés.


Slide: 32


Assistant de mots croisés
static void match(String pat, Tree t) { match(pat, 0, t) ; } private static void match(String pat, int i, Tree t) { char c = getChar(pat, i) ; if (c == EOW) { if (t.val == EOW) { System.out.println(path) ; } } else { if (t.val == EOW) { t = t.next ; } if (c == '?') { // Joker for ( ; t != null ; t = t.next) { path.push(t.val) ; match(pat, i+1, t.down) ; path.pop() ; } } else { // Lettre ordinaire



Slide: 33


Assistant de mots croisés, lettre ordinaire
… } else { // Lettre ordinaire for ( ; t != null ; t = t.next) { if (c < t.val) { return ; } else if (c == t.val) { path.push(t.val) ; match(pat, i+1, t.down) ; path.pop() ; return ; } } } } }


Le coût est quelque part entre celui de mem (pas de '?') et de print (que des '?').


Slide: 34


Deuxième application : suggestion de corrections
Lorsque le mot demandé n'est pas dans le dictionnaire, les bons correcteurs orthographiques proposent une liste de mots du dictionnaire « approchant » le mot faux demandé.

Nous allons faire de même en nous limitant à tenter de corriger une faute de frappe : Un exemple :
# java Tree -error anticonstitutionellement
anticonstitutionnellement
anticonstitutionnellement
Pourquoi deux fois ? Il y a deux endroits où insérer « n ».


Slide: 35


Corriger une faute de frappe


private static void error(String w, Tree t) { error(w, 0, t) ; } private static void error(String w, int i, Tree t) { char c = getChar(w, i) ; if (c == EOW) { // Ajouter une lettre à la fin de w if (t.val == EOW) { t = t.next ; } for ( ; t != null ; t = t.next) { path.push(t.val) ; print(w, i, t.down) ; path.pop() ; } } else { // c est une lettre ordinaire



Slide: 36


Corriger une faute de frappe, lettre ordinaire
// c est une lettre ordinaire // Supprimer c print(w, i+1, t) ; // Changer et ajouter if (t.val == EOW) { t = t.next ; } for ( ; t != null ; t = t.next) { path.push(t.val) ; if (c == t.val) { error(w, i+1, t.down) ;// pas (encore!) d'erreur } else { print(w, i+1, t.down) ; // Changer } print(w, i, t.down) ; // Inserer t.val ici path.pop() ; } } }



Slide: 37


Intersection de deux dictionnaires
Comment procéder simplement à partir des listes de mots ? On peut d'ailleurs procéder avec très peu de mémoire. En supposant deux fichiers de mots triés selon l'ordre compareTo des String.

On peut produire ce type de fichiers triés par la composition de build et de print. En effet, l'ordre compareTo est l'ordre lexicographique qui étend l'ordre sur les char ainsi.

Slide: 38


Intersection de deux listes de mots
Les « listes » sont en des fichiers, structurés en lignes, à raison d'un mot par ligne.
static void inter(BufferedReader in1, BufferedReader in2) throws IOException { String buff1 = in1.readLine(), buff2 = in2.readLine() ; while (buff1 != null && buff2 != null) { int c = buff1.compareTo(buff2) ; if (c < 0) { buff1 = in1.readLine() ; } else if (c > 0) { buff2 = in2.readLine() ; } else { // c == 0, ie buff1 et buff2 égaux System.out.println(buff1) ; buff1 = in1.readLine() ; buff2 = in1.readLine() ; }



Slide: 39


Intersection de deux dictionnaires
Un code très intéressant (à mon avis) qui mélange listes et arbres...

D'abord intersection de deux dictionnaires non-vides.
static Tree inter(Tree a, Tree b) { if (a.val == EOW && b.val == EOW) { return new Tree(EOW, null, interList(a.next, b.next)) ; } else { return interList(a, b) ; } }



Slide: 40


Intersection de deux dictionnaires
Puis intersection de deux listes de paires (c,D).
static Tree interList(Tree a, Tree b) { if (a == null || b == null) { return null ; } else if (a.val < b.val) { return interList(a.next, b) ; } else if (b.val < a.val) { return interList(a, b.next) ; } else { // a.val == b.val Tree d = inter(a.down, b.down) ; if (d == null) { return interList(a.next, b.next) ; } else { return new Tree(a.val, d, interList(a.next, b.next)) ; } } }



Ce document a été traduit de LATEX par HEVEA