3.2- TP - Trie et autocomplétion
Dans ce TP, on va étudier un type d'arbre (appelé "trie" ou "arbre préfixe") servant à stocker une collection de mots.
Dans le trie, on associe à chaque arrête une lettre, et à chaque noeud un booléen pour signaler si on est à la fin d'un mot ou non.

Dans l'exemple ci-dessus, le trie représente les mots "ami", "ton", "tas", "tasse", "tape", "tir", "tire", "tirer", "but" et "bon". Ce sont tous les chemins de la racine à un noeud orange.
En revanche, les mots "am" ou "tass" ne sont pas des mots, car le chemin pour y arriver termine sur un noeud vert.
I. Représentation en OCaml
On va utiliser un type d'arbre général, avec
- pour le type de donnée stocké dans les noeuds, un booléen (
bool) - dans chaque arrête, on ajoute un caractère (
char)
On va supposer que le trie ne peut jamais être vide.
Loading editor...
type trie =
|N of bool * (char * trie) list;;Par exemple, on peut représenter un trie stockant les mots "ta", "tu", et "tas".
Loading editor...
let ex_1 : trie =
N (false, [
('t', N (false, [
('a', N (true, [
('s', N (true, []))
]));
('u', N (true, []))
]))
]);;Je vous fourni une fonction print_trie qui permet d'afficher un trie de manière un peu plus lisible. Les (X) sont les noeuds qui ne sont pas des mots, et les (O) ceux qui sont des mots.
N'essayez pas de lire ou de comprendre la fonction : non seulement c'est complètement en dehors du cadre du cours, mais je l'ai aussi codé n'importe comment.
Cette fonction renvoie le type unit. Cela veut dire qu'elle ne renvoie rien.
Loading editor...
let print_trie (t: trie): unit (* *) = let rec aux (N (b, l)) (p) = let cb = if b then "(O)" else "(X)" in print_string cb; let np = p ^ " " in let f = ref true in List.iter (fun (c, t2) -> ((if !f then (f:=false; print_string "-")else (print_string np; print_string "\\"));print_char '-'; print_char c; print_string "--";aux t2 (np ^ " "))) l in aux t "\n";;
Loading editor...
print_trie ex_1;;
Question 1
Créez un nouveau trie ex_2 qui est le trie du premier exemple, auquel vous aurez ajouté les mots "tac" et "tact".
Loading editor...
Question 2
Écrivez une fonction compter_mots: trie -> int qui renvoie le nombre de mots contenus dans le trie. Votre fonction doit renvoyer 3 sur ex_1 et 5 sur ex_2.
Loading editor...
Question 3a
Écrivez une fonction existe_liste: (char * trie) list -> char -> bool, qui cherche la présence d'un caractère dans une liste de couples (caractère, trie) et renvoie true si le caractère est trouvé, false sinon.
Loading editor...
Question 3b
Écrivez une fonction recherche_liste: (char * trie) list -> char -> trie, qui, en supposant qu'un caractère est présent dans la liste donnée en entrée, renvoie le trie correspondant.
Loading editor...
Question 3c
Écrivez une fonction recherche: trie -> char list -> bool, qui cherche la présence d'un mot dans un trie, et renvoie true si elle la trouve, false sinon.
Notez bien que la fonction prend en argument un char list et pas une chaine de caractère. Cela vous simplifiera la tâche. Je vous fourni une fonction s2l: string -> char list qui réalise la conversion, ce sera ainsi plus simple d'écrire des tests.
Loading editor...
let s2l (s: string): char list (* *)= List.of_seq (String.to_seq s);;
Loading editor...
s2l "bonjour"
Loading editor...
Comment « modifier » un trie ?
En OCaml, la plupart des structures de données sont immutables, c'est à dire qu'on ne peut pas les modifier. Il faut donc renvoyer une nouvelle version de la structure.
Je vous donne un exemple sur une structure simple : une fonction qui insère un élément à sa place dans une liste croissante.
let rec insere (l: int list) (x: int): int list = match l with
|[] -> [x]
|y::q -> if x < y then x::l
else y::(insere q x)
;; Ainsi, la fonction insere renvoie toujours une nouvelle liste, construite à partir de la liste originelle.
Question 4
Écrivez une fonction ajout: trie -> char list -> trie qui prend un trie et une chaine de caractère en argument, et renvoie le trie auquel on a ajouté la chaine de caractère.
Loading editor...
Question 5
En déduire une fonction creer_trie: string list -> trie qui créé un trie contenant tous les mots d'une liste de mots. Quelle est la complexité ce creer_trie, en fonction de L la taille maximale d'un mot et de n le nombre de mots ?
Loading editor...
La cellule de code suivante charge un fichier contenant les 10000 premiers mots du dictionnaire français dans une liste de mots. La fonction lire_fichier (fournie par le site) renvoie la liste des lignes d'un fichier.
Loading editor...
let mots = List.filteri (fun i _ -> i < 10000) (lire_fichier "mots.txt");; List.length mots;;
Question 6
Créez le trie correspondant et vérifiez avec vos fonctions compter_mots et recherche que c'est cohérent.
Loading editor...
Loading editor...
Loading editor...
Loading editor...
Question 7
Écrivez une fonction liste_mots: trie -> string list qui renvoie la liste de tous les mots d'un trie. Je vous fourni pour cela la fonction l2s, qui fait l'inverse de s2l, et rev_l2s, qui inverse l'ordre des lettres en plus.
Loading editor...
let rev_l2s (l: char list): string = (* *) String.of_seq (List.to_seq (List.rev l));; let l2s (l: char list): string = (* *) String.of_seq (List.to_seq l);; l2s (s2l "bonjour");; rev_l2s (s2l "bonjour");;
Loading editor...
Question 8
Écrivez une fonction liste_suite: trie -> char list -> string list qui prend en argument un préfixe et qui renvoie tous les mots suffixes possibles permettant de terminer ce mot.
Loading editor...
Loading editor...
Question 9
Quelle est la complexité de cette opération, en fonction de k la taille du préfixe et de p le nombre de suffixes possibles ?
Loading editor...
C'est très avantageux car, en particulier, la complexité ne dépend pas du nombre de mots dans notre dictionnaire.
Question bonus
Les trie ne sont pas toujours le meilleur outil pour ce genre d'opérations, car on est obligé de procéder lettre par lettre. Quand tous les mots possibles ont le même groupement de lettres, on pourrait "compresser" ce groupement en un seul noeud.
La structure devient alors un « arbre radix ».
Réfléchissez à comment vous pourriez implémenter un tel arbre.
Vous pouvez utiliser le module Map, qui réalise une sorte d'équivalent de dictionnaire :
module StringMap = Map.Make(String);;pour "importer" le typeStringMap, un dictionnaire dont les clés sont desstringStringMap.empty: le dictionnaire vide, de type'a StringMap.toù'aest le type des valeurs du dictionnaire.StringMap.add : string -> 'a -> 'a StringMap.t -> 'a StringMap.t: ajoute une nouvelle association au dictionnaire (mais sans le modifier : c'est un nouveau dictionnaire qui est renvoyé)StringMap.mem : string -> 'a StringMap.t -> bool: teste si un élément est présent dans la mapStringMap.find : string -> 'a StringMap.t -> 'a: récupère un élément présent dans la map
Voir la documentation du module Map.
Loading editor...
Loading editor...
Entrez le code donné en cours pour accéder à la solution :