Adloun

Probleme – L'arbre préfixe, ou pourquoi chercher un mot ne dépend pas du dictionnaire

Exercice · niveau 3 (difficile) · informatique (MP2I/MPI), chapitre 10 — Arbres

Énoncé

Un arbre préfixe (trie) range un ensemble de mots dans un arbre où chaque nœud porte une lettre et un drapeau « ici finit un mot ».

Corrigé

1. Le type et les deux opérations. C'est un arbre général : un nœud a autant de fils que de lettres différentes qui peuvent le suivre.


(* lettre portee, « un mot finit ici », fils *)
type trie = Noeud of char * bool * trie list

let vide = Noeud ('*', false, [])                 (* la racine ne porte rien *)

(* Insere le suffixe mot[i..] sous t. Renvoie le trie modifie.
   Precondition : 0 <= i <= String.length mot. *)
let rec insere (Noeud (c, fin, fils)) mot i =
  if i = String.length mot then Noeud (c, true, fils)
  else
    let l = mot.[i] in
    let rec dans = function
      | [] -> [insere (Noeud (l, false, [])) mot (i+1)]      (* branche NEUVE *)
      | (Noeud (l', _, _) as f) :: r ->
          if l' = l then insere f mot (i+1) :: r else f :: dans r
    in Noeud (c, fin, dans fils)

(* Renvoie true ssi mot[i..] est un mot complet range sous t. *)
let rec appartient (Noeud (_, fin, fils)) mot i =
  if i = String.length mot then fin
  else
    let l = mot.[i] in
    let rec cherche = function
      | [] -> false
      | (Noeud (l', _, _) as f) :: r ->
          if l' = l then appartient f mot (i+1) else cherche r
    in cherche fils

2. Correction et coût.

Terminaison. Variant : , qui décroît de à chaque descente. La fonction auxiliaire cherche termine par l'ordre induit sur la liste de fils.

Invariant de la structure, et c'est lui qu'il faut énoncer avant tout : le chemin de la racine à un nœud épelle un préfixe, et le drapeau fin y vaut true exactement quand ce préfixe est un mot de l'ensemble. L'insertion le préserve — elle ne pose true qu'au bout du chemin du mot inséré — et la recherche l'exploite : elle descend le long du chemin épelé par le mot et lit le drapeau.

Complexité. La recherche visite au plus nœuds, et examine à chaque étage au plus fils, où est la taille de l'alphabet. Le coût est — il ne dépend pas du nombre de mots rangés. C'est le fait remarquable de la structure, et il vaut aussi pour l'insertion.

3. Les mesures. Sur le dictionnaire de dix mots arbre, arc, arche, art, bas, base, basique, but, a, arbrisseau :

mots insérés
somme des longueurs lettres
nœuds de l'arbre (racine comprise)

nœuds portant une lettre — avec la racine — pour lettres écrites : lettres ont été partagées entre mots à préfixe commun — le ar de arbre, arc, arche, art, arbrisseau, le bas de bas, base, basique. C'est le second intérêt de la structure : elle compresse les préfixes.

Comparaisons de lettres effectuées par une recherche :

mot cherchéréponsecomparaisonslongueur
`arbre`vrai
`arb`faux
`arbrisseau`vrai
`basique`vrai
`basiques`faux
`zut`faux

Trois lignes méritent un commentaire. arb est faux bien que le chemin existe : c'est un préfixe, pas un mot, et c'est précisément à cela que sert le drapeau — sans lui, la structure ne distinguerait pas les deux. zut coûte comparaisons seulement : la racine n'a que les fils a et b, et l'échec est immédiat. basiques coûte autant que basique : on s'arrête faute de fils s, sans lire la fin du mot.

Comparaison avec une liste de mots. Chercher dans une liste de mots demande comparaisons de chaînes, soit caractères dans le pire cas. Avec , l'écart est mince ; avec — la taille d'un dictionnaire français — le trie fait toujours , soit quelques dizaines de comparaisons, et la liste plusieurs millions. Le coût du trie est indépendant de , et c'est tout ce qui compte.

4. Le lien avec le cours. Un trie est un arbre d'arité quelconque : sa réalisation naturelle en OCaml est la liste de fils écrite ci-dessus, et cette liste est la conversion « fils gauche, frère droit ». Le premier fils est le fils gauche du binaire, les frères suivants forment la chaîne droite. La fonction cherche qui parcourt la liste des fils est exactement le parcours de cette chaîne de frères.

On y retrouve le défaut mesuré à l'exercice 10.7 : la fratrie devient un peigne. Un nœud à fils a une hauteur binaire de , et la recherche d'une lettre y coûte jusqu'à comparaisons — c'est le facteur de la complexité. Deux remèdes classiques : remplacer la liste de fils par un tableau de cases indexé par la lettre, qui ramène le coût à au prix de pointeurs par nœud ; ou par un arbre binaire de recherche sur les lettres, qui donne en espace linéaire. Le choix se fait sur la taille de l'alphabet, et c'est un exemple parfait de ce que le cours annonçait : la structure de données se taille à la mesure des opérations qu'on répète.

Les autres exercices de ce chapitre Le cours du chapitre

Un blocage sur cet exercice ? Le tuteur d'Adloun guide par questions, sans donner la réponse.