Nous allons étudier dans ce Chapitre la façon de coder un dictionnaire dans un arbre n-aire (chaque noeud de l'arbre possède n branches).
Pour commencer, nous allons étudier la structure d'un noeud. On suppose que le dictionnaire est en majuscules, c'est pourquoi nous n'avons que 26 lettres possibles pour un noeud. Bien évidemment, cette structure peut être élargie si on veut aussi prendre en compte les minuscules et les chiffres ou autres signes. Nous voulons aussi afficher l'arbre n-aire à l'écran (sur un exemple comportant seulement quelques mots), ce qui implique que chaque noeud aura une position à l'écran, matérialisée par deux coordonnées x et y. Chaque noeud contient une lettre, donc une variable de type char, et il doit contenir aussi un indicateur boolean utilisé pour indiquer si un mot est entier : par exemple, supposons qu'on code le mot "CARTE" dans un arbre; l'arbre aura donc 5 niveaux. La dernière lettre du mot (le "E") aura son indicateur boolean positionné à true. Par contre, la lettre "A" n'aura pas son indicateur positionné, car le mot "CA" n'est pas un mot du dictionnaire. La lettre "R" aura son indicateur positionné, car le mot "CAR" fait partie du dictionnaire de la langue française.
Un noeud possède donc:
- 26 fils, les lettres possibles de 'A' à 'Z'
- les coordonnées (x,y) qui seront utilisées pour l'affichage graphique de l'arbre
- une lettre
- un indicateur boolean
Noeud =
class
private
Fils: array['A'..'Z'] of Noeud; //liste de fils
x, y: integer; // pour dessin arbre
Lettre: char; // lettre contenue dans ce noeud de l'arbre
Entier: boolean; //flag indiquant si le mot est entier
public
constructor create;
destructor destroy;
end; { Noeud }
Etudions maintenant la structure d'un arbre. Comme nous l'avons déjà vu, un arbre est constitué par sa tête, et par une autre variable nous permettant de parcourir l'arbre. Les opérations relatives à un arbre sont les suivantes :
- ajout d'un mot
- chargement d'un fichier dans l'arbre en utilisant l'opération d'ajout
- compter le nombre de noeuds terminaux se trouvant en-dessous d'un
noeud quelconque
- l'affichage graphique et l'affichage texte (création d'une liste de
mots du dictionnaire)
- recherche d'un mot
Dans notre exemple, nous avons choisi une liste de 13 mots de 5 lettres
chacun. A l'aide de la fonction qui compte le nombre de noeuds terminaux, on
peut donc dire combien de mots comporte l'arbre.
//arbre d'ordre 26
Arbre =
class
private
tete, courant: noeud;
public
constructor create;
destructor destroy; override;
procedure rajouter_mot(LeMot: string);
procedure charger(NomFic: string);
procedure Affiche(S, prefixe: string);
procedure dessine(en_cours: noeud; limg, old_x, old_y: integer);
function Trouve(LeMot: string): boolean;
function compter_terminaux(en_cours: noeud): integer;
end;
Nous allons voir, une par une, toutes les procédures décrites ci-dessus. Mais auparavant, voici un exemple concret d'arbre n-aire qui contient des mots de longueur 3 (EMU,EPI), de longueur 4 (EMUE, EMIS, EMET, EPIE, PRIS) et de longueur 5 (EPIER, EPRIS), dont l'indicateur est positionné à true, et qui est représenté sur ce schéma par un petit triangle noir dans le coin des cases en bas à droite.
On part du sommet de l'arbre. La procédure est récursive, et comme
toute procédure récursive, elle a un point d'appui (ou d'arrêt). On va ajouter
le mot dans l'arbre lettre par lettre, en descendant dans l'arbre d'un niveau à
chaque lettre. Le mot est donc "mangé" progressivement du début vers
la fin.
1) Le point d'arrêt est atteint quand on n'a plus de lettres à ajouter dans l'arbre. Dans ce cas, on positionne le noeud courant à true, pour indiquer que c'est un mot complet et on quitte la procédure.
2) On a encore des lettres à ajouter dans l'arbre. Prenons le cas du mot "EPI". On est au sommet de l'arbre. Si la lettre "E" existe déjà dans l'arbre en tant que noeud fils du sommet, alors :
- on "mange" le "E"; le mot à rajouter devient "PI" mais il faut le mettre sous le "E"
- on descend sur le fils "E"
- on rajoute (récursivement) le mot "PI"
Si la lettre "E" n'existe pas, alors :
- on crée le noeud fils "E"
- on exécute les étapes ci-dessus.
Voici le programme correspondant :
procedure arbre.rajouter_mot(LeMot: string);
var lettre: char;
begin
if LeMot = '' then
begin
courant.entier := true;
exit;
end;
lettre := LeMot[1];
if courant.fils[lettre] <> nil then // si la lettre existe déjà
courant := courant.fils[lettre] // alors on se positionne sur la lettre suivante
else // sinon il faut créer cette lettre dans l'arbre
begin
courant.fils[lettre] := noeud.create;
courant := courant.fils[lettre];
courant.lettre := lettre; // la lettre est maintenant dans l'arbre
end;
delete(LeMot, 1, 1); // on efface la lettre du mot puisqu'elle est déjà dans l'arbre
rajouter_mot(LeMot); // et on rajoute le reste
end;
Le fichier étant un simple fichier texte, il suffit de le lire ligne par ligne et d'ajouter au fur et à mesure les mots lus dans l'arbre.
procedure arbre.charger(NomFic: string);
var s: string;
chemin: string; // le chemin de l'appli, AVEC l'antislash final
f: textfile;
begin
chemin := ExtractFilePath(Application.ExeName);
assignFile(f, chemin + nomFic);
reset(f);
repeat
readln(f, s);
courant := tete; // on se positionne en tête de l'arbre
rajouter_mot(s); // et on rajoute le mot
until eof(f);
closeFile(f);
end;
Pour dessiner un arbre, nous avons besoin de cette fonction. En effet, si un père a deux noeuds terminaux, sa position à l'écran sera au milieu de ses deux fils (il s'agit de la position sur l'axe des abscisses, les "x"; en tant que père il sera positionné au dessus de ses fils sur l'axe des ordonnées, les "y"). Cela est valable aussi pour un père qui n'a pas directement de noeuds terminaux, mais dont les fils ou sous-fils en ont. Il faut donc parcourir récursivement le sous-arbre dont le père est le noeud courant pour arriver jusqu'aux noeuds terminaux.
Une solution serait la suivante :
- la somme des noeuds terminaux ayant pour ancêtre un noeud donné est :
- égale à un si le noeud en question n'a pas de fils (point d'arrêt)
- égale à la somme des noeuds terminaux de ses fils sinon
Mais, dans ce cas, on peut supprimer le point d'arrêt et faire fusionner les deux conditions. En effet, on n'a qu'à parcourir tous les fils du noeud en cours et, s'ils ne sont pas égaux à nil, incrémenter alors une variable locale avec leurs propres noeuds terminaux. Si, à la fin, le total obtenu est égal à zéro, alors aucun des fils du noeud en cours n'existe (point d'arrêt de la récursivité) donc on initialise la variable locale à 1 et on sort.
function arbre.compter_terminaux(en_cours: noeud): integer;
var i: char;
total: integer;
begin
total := 0;
for i := 'A' to 'Z' do
if en_cours.fils[i] <> nil then
inc(total, compter_terminaux(en_cours.fils[i]));
if total = 0 then inc(total);
compter_terminaux := total;
end;
La procédure d'affichage d'un arbre nécessite, pour commencer, une image et ensuite quelques petits calculs mathématiques, ainsi que le nombre total de noeuds terminaux qui se trouvent en-dessous du noeud courant. La première chose à faire est de compter le nombre de noeuds terminaux dérivant du noeud en cours, afin de décider où il sera placé graphiquement, ceci nous amenant donc au calcul de ses coordonnées. Ensuite pour chaque noeud dessiné, on doit tracer une ligne le reliant à son père, ce qui nous amène à l'utilisation de 2 paramètres (old_x,old_y) de type integer. On suppose que chaque noeud occupe 50 pixels en largeur et 80 en hauteur. Ainsi, si un noeud a 4 fils, sa position sera à la moitié de 4*50, soit " (4*50) div 2". S'il n'a que 2 fils, il sera à " (2*50) div 2". Mais comment faire pour que les noeuds d'un même niveau se décalent vers la droite ? En effet dans le dessin suivant, au niveau 1, nous avons deux fils, le premier ayant 2 fils terminaux et le deuxième trois fils terminaux.
Si on applique la formule ci-dessus, alors le premier noeud du niveau 1 est situé à "x:=(2*50)/2" (soit x=50) et le deuxième est situé à "x:=(3*50)/2" (soit x=75). On constate alors que ces deux noeuds se chevauchent, puisque le premier est situé à x=50 et il occupe 50 pixels (jusqu'à cent; or le deuxième est situé à x=75). Donc, à chaque dessin d'une partie de l'arbre, il faut voir quelle est la largeur qu'il requiert et la passer en paramètre à ses frères, de façon à ce que ceux-ci commencent leur dessin avec un décalage vers la gauche. Ainsi dans l'exemple ci-dessus, le premier noeud nécessite 100 pixels (car il a deux fils terminaux, qui seront dessinés à partir de la position 0 ), et le deuxième noeud nécessite 150 pixels (car il a trois noeuds terminaux, qui seront dessinés à partir de la position 100). Ceci nous amène à un nouveau paramètre, appelé "limg" qui indique la position à partir de laquelle on doit dessiner le sous arbre du noeud en cours. On effectue ensuite les opérations suivantes :
- on calcule la position du noeud en cours (x et y)
- on relie ce noeud à son père
-
on
parcourt tous ses fils et pour chaque fils non vide on dessine le sous-arbre
correspondant en faisant un appel récursif.
procedure arbre.dessine(en_cours: noeud; limg, old_x, old_y: integer);
var x, y, nb: integer;
i: char;
begin
//nb:=compter_terminaux(courant); // effet joli
nb := compter_terminaux(en_cours);
x := limg + (50 * nb) div 2;
y := old_y + 80;
if en_cours <> tete then
with form1.image1.picture.bitmap.canvas do
begin
textout(x, y, en_cours.lettre);
en_cours.x := x; en_cours.y := y;
moveto(x, y - 5); lineto(old_x, old_y);
end;
for i := 'A' to 'Z' do
if en_cours.fils[i] <> nil then
begin
nb := compter_terminaux(en_cours.fils[i]);
dessine(en_cours.fils[i], limg, x, y + 20);
limg := limg + nb * 50;
end;
end;
Pour réaliser un affichage de tous les mots de l'arbre, on n'a qu'à parcourir ses noeuds et quand on arrive à un noeud dont l'indicateur boolean est positionné, on ajoute le mot à une liste. Ensuite, on parcourt tous les fils du noeud en cours en faisant un appel récursif. Comme les mots se constituent au fur et à mesure qu'on descend dans l'arbre, on utilise un paramètre à cet effet.
procedure arbre.Affiche(S: string);
var i: char;
aux: noeud;
begin
if courant.Entier then Form1.Memo1.Lines.Add(s);
for i := 'A' to 'Z' do
if courant.fils[i] <> nil then
begin
aux := courant;
courant := courant.fils[i];
Affiche(S + i);
courant := aux;
end;
end;
La recherche se fait par une descente récursive dans l'arbre; à chaque descente, on regarde si le noeud en cours a un fils qui est identique à la première lettre du mot. Si c'est le cas, alors on "mange" le début du mot (la première lettre) et on cherche à partir de la position courante le mot restant. Si, à un moment, pendant l'exploration de l'arbre, on se retrouve avec un mot vide, alors le mot appartient à l'arbre. Attention : cette condition n'est valable que pour les arbres dont tous les mots ont la même longueur. Sinon, il faut rajouter une condition supplémentaire, et cette tâche vous revient.
function arbre.trouve(LeMot: string): boolean;
var lettre: char;
begin
if LeMot = '' then
trouve := true
else
begin
lettre := LeMot[1];
delete(LeMot, 1, 1);
if courant.fils[lettre] = nil then
trouve := false
else
begin
courant := courant.fils[lettre];
trouve := trouve(LeMot);
end;
end;
end;
Télécharger le code source Delphi