Nous savons que la structure d'un arbre binaire est récursive. Nous allons voir comment on peut implémenter cette structure récursive dans une structure statique (c'est à dire un tableau de taille fixe). Voici d'abord la structure dynamique de l'arbre binaire (telle que nous l'avons vue dans le chapitre précédent) :
type Noeud =
class
private
Fils: array['A'..'Z'] of Noeud; //liste de fils
x, y: integer; // pour dessin arbre
Gauche, Droite: Noeud;
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 }
Nous allons maintenant transformer cette structure dynamique en une structure statique, donc linéaire. Pour cela nous allons utiliser un tableau, dont chaque case a une structure représentée par :
- "lettre" : une variable de type "char"
- "fin" : une variable de type booléen, mais représentée ici
par un byte, qui indique que le mot est entier
-
"droite","bas"
: représentent les deux fils gauche et droit de la structure dynamique.
type trec = record
lettre: char;
fin: byte;
droite, bas: longint;
end;
dictionnaire = record
t: array[1..5000] of trec;
derniere_case: longint;
end;
var dico: dictionnaire;
Nous avons utilisé un tableau de 5.000 cases; mais en plus le dictionnaire contient aussi une variable "derniere_case", de façon à ce qu'on sache où on doit implémenter dans le tableau tous les nouveaux mots.
Nous allons utiliser dans le programme à
télécharger des mots de la même longueur, mais néanmoins, pour expliquer le
principe de l'implémentation dans un tableau, nous utiliserons des mots de
longueur différente. Voici une image illustrant parfaitement les types
prédéfinis ci-dessus, ainsi que l'implémentation d'un petit arbre n-aire :
Nous allons décrire brièvement une recherche de mot dans ce tableau. Supposons qu'on veuille trouver le mot "EPRIS". Nus sommes au début sur la case 1, donc sur la lettre E. On cherche ses fils. Le premier est à la case 2 et c'est M. Mais nous, on cherche la lettre P, donc on va se positionner sur le frère de M (M,3,9) qui est à la case 9. C'est bien un P, donc on descend maintenant sur son premier fils qui se trouve à la case 10 (car P,10,0). On trouve un E, qui ne nous intéresse pas, donc on va sur le frère de E qui se trouve à la case 12 (car E,11,12). C'est un I, il ne nous intéresse donc pas et on se positionne sur son frère qui se trouve à la case 15 (car I,13,15) et c'est bien un R. Le fils de R se trouve à la case 16 (car R,16,0) et c'est un I; le fils de I est à la case 17 et c'est un S. On a donc bien le mot EPRIS, car derrière le S il n'y a plus de fils, sinon le mot aurait été incomplet. On utilise aussi la variable "fin" pour indiquer que le mot est complet, auquel cas on peut ajouter d'autres lettres derrière ce mot (exemple : EPI et EPIER; les cases I et R ont la variable "fin" positionnée à 1, les autres à 0).
Voici la liste des procédures que nous allons utiliser :
1) - procédure introduis_mot_fin_dico(mot : string;p : longint;bas :
boolean);
2) - function positionnement_sur_lettre(lettre : char;courant : longint;
var dernier : longint ) : longint;
3) - procédure rajoute_mot(mot : string);
4) - function appartient(mot : string) : boolean;
La procédure 1) introduit un mot (ou la fin d'un mot) à la fin du
dictionnaire. Elle a plusieurs paramètres :
- le mot à introduire dans le tableau
- la position à partir de laquelle on introduit le mot dans les cases
du tableau
- une variable booléenne indiquant si le mot sera introduit en tant que
fils de la case en cours (c'est donc la case "droite" qui est
initialisée, cela correspondant à la lettre suivante dans le mot) ou bien en
tant que frère (c'est donc la case "bas" qui sera initialisée);
exemple 1 : si on est sur la
lettre I de EPIER et si on veut introduire la mot EPRIS, alors la lettre R sera
introduite à droite de I en tant que
fils de E. Si par la suite on cherche le mot EPRIS, on tombe
premièrement sur le I, et comme il ne nous intéresse pas on prend le fils
suivant de son père, donc R, suivi de I et de S.
exemple 2 : si on a
introduit le mot EMIS, et si on est sur la lettre E et si on veut introduire le
mot ROND, alors la lettre R sera mise en "bas" du E, en tant que frère de E.
Il faut savoir que dans tous les cas où l'on introduit un mot ou la fin
d'un mot (pour le mot EPRIS on n'a introduit que RIS, car EP avait été
introduit avec le mot EPEE), cette insertion se fera automatiquement à la fin
du tableau (c'est à dire là où il y a des cases non remplies), donc à partir de
la case numéro "derniere_case".
procedure introduis_mot_fin_dico(mot: string; p: longint; bas: boolean);
var l, courant: longint;
begin
if bas
then dico.t[p].bas := dico.derniere_case
else dico.t[p].droite := dico.derniere_case;
l := length(mot);
for courant := 1 to l do
with dico.t[dico.derniere_case + courant - 1] do
begin
lettre := mot[courant];
fin := 0;
droite := dico.derniere_case + courant;
bas := 0;
end;
dico.t[dico.derniere_case + l - 1].droite := 0; // plus rien derrière
dico.t[dico.derniere_case + l - 1].fin := 1; // c'est un mot
inc(dico.derniere_case, l);
end;
La fonction 2) sert de positionnement sur une lettre, c'est à dire à trouver la case à partir de laquelle un mot sera introduit dans le tableau. Cette fonction a 3 paramètres :
- la lettre sur laquelle on veut se positionner
- la position à partir de laquelle on commence la recherche dans les cases
- la position de la dernière lettre trouvée (on a EPEE et on veut rajouter EPRIS; la dernière lettre trouvée est P et sa position dans le tableau sera retournée, car on va rajouter un R en tant que fils de P)
function positionnement_sur_lettre(lettre: char; courant: longint;
var dernier: longint): longint;
var l: char;
begin
l := dico.t[courant].lettre;
if (courant > 0) and (l = lettre) then dernier := courant;
while (courant <> 0) and (l <> lettre) do
begin
courant := dico.t[courant].bas;
l := dico.t[courant].lettre;
if (courant > 0)
and (l = lettre)
then dernier := courant; // mémorise la dernière position trouvée où l=lettre}
end;
positionnement_sur_lettre := courant;
end;
La procédure 3) représente le rajout d'un mot dans le tableau. Cette procédure utilise la procédure 1) et la fonction 2). Lors d'un rajout de mot dans notre tableau, nous devons réaliser dans l'ordre les opérations suivantes:
a) - nous positionner sur le noeud dont la lettre est identique à la
première lettre du mot. Si le fils existe, alors on doit récupérer son numéro
de case dans le tableau et recommencer avec la lettre suivante, sinon on doit
récupérer le numéro de la case qui sera pointée par la case en cours.
exemple : dans le tableau
décrit au dessin précédent, on a rempli les huit premières cases avec les mots
EMUE,EMIS,EMET. La variable "derniere_case" est donc égale à 9. Si on
veut rajouter EPEE, on doit récupérer le numéro 2, car P est le frère de M, et
ainsi le numéro 9 se retrouve dans la case 2 : M,3,9. La case 9 est pointée par
la case 2.
b) - arrivés ici, nous avons un choix à faire: ou bien nous avons
trouvé au moins un début de mot qui était déjà dans le tableau (comme EP pour
EPEE et EPRIS) et dans ce cas la variable "courant" est positive, ou
bien on doit rajouter le mot en entier auquel cas la variable
"courant" est nulle.
Si on a trouvé un début de mot, alors on est sur une case quelconque du
tableau et on la fait pointer sur la case de numéro "derniere_case" où
l'on rajoutera la suite du mot (exemple : on a dans le tableau le mot EPEE et
on veut rajouter EPRIS. Le P pointe sur E et E n'a pas de frère, donc sa
variable "bas" va pointer sur "derniere_case" où on va
introduire le reste du mot, à savoir RIS).
Voici l'algorithme correspondant :
procedure rajoute_mot(mot: string);
var i, l, courant, p, precedent, dernier: longint;
dedans: boolean;
begin
l := length(mot); courant := 1; i := 1; dernier := 0;
p := positionnement_sur_lettre(mot[1], 1, dernier);
dedans := true;
while (i <= l) and (p > 0) and
(dedans) and
(courant > 0) and
(i < dico.derniere_case) do
begin
p := positionnement_sur_lettre(mot[i], courant, dernier);
if p > 0 then courant := dico.t[p].droite;
dedans := (dernier + 1) < dico.derniere_case;
inc(i);
end;
if courant > 0 then //donc p=0
begin
dec(i);
if dernier = 0 then // cas spécial
begin
if dico.derniere_case = 1
then introduis_mot_fin_dico(mot, 1, false)
else
begin // positionnement sur la dernière lettre de la colonne d'un arbre
p := courant;
while p <> 0 do
begin
p := dico.t[p].bas;
if p > 0 then courant := p;
end;
introduis_mot_fin_dico(mot, courant, true)
end;
end
else
if dernier + 1 = dico.derniere_case then
if mot[i] = dico.t[dernier].lettre then
introduis_mot_fin_dico(copy(mot, i, l), dernier, false)
else
introduis_mot_fin_dico(copy(mot, i, l), dernier, true)
else
begin // positionnement sur la dernière lettre de la colonne d'un arbre
p := courant;
while p <> 0 do
begin
p := dico.t[p].bas;
if p > 0 then courant := p;
end;
introduis_mot_fin_dico(copy(mot, i, l), courant, true);
end;
end
else // donc p<>0
introduis_mot_fin_dico(copy(mot, i, l), dernier, false)
end;
Et maintenant il ne nous reste plus qu'à faire une fonction d'appartenance d'un mot. Elle est simple à réaliser; en effet, pour chacune des lettres du mot, à partir de la première et jusqu'à la dernière on appelle la fonction de positionnement sur une lettre donnée; si cette fonction échoue à un instant t, alors c'est que l'une des lettres du mot n'est pas dans les fils ou frères de la dernière lettre rencontrée; si, par contre, on arrive à la fin du mot, alors on doit tester que ce mot est bien terminé (la variable "fin" positionné à 1).
function appartient(mot: string): boolean;
var i, l, courant, p: longint;
dernier: longint;
begin
courant := 1; i := 1; p := 1; l := length(mot);
while (i <= l) and (p > 0) do
begin
p := positionnement_sur_lettre(mot[i], courant, dernier);
if p > 0 then courant := dico.t[p].droite;
inc(i);
end;
appartient := (i > l) and (p > 0) and (dico.t[p].fin = 1);
end;
Télécharger le code source Delphi