Dictionnaires, anagrammes : algos efficaces

Description

Salut a tous

Cela fait quelques temps, j'ai remarqué qu'il y avait beaucoup de
sources sur les dictionnaires, générateurs de dictionnaires,
anagrammes ...

Cependant aucune n'implémente la méthodes des arbres pour indexer les
dictionnaires, ou générer des anagrammes. C'est pourtant extremement
rapide et efficace.

Je vais donc essayer de vous présenter cette méthode du mieu possible
afin de vous initier à l'un des fondements (à mon avis ;) ) de
l'algorithmique en programmation : les arbres et quelques
applications.

Source / Exemple :


'*************************************
' Dans un module de classe nommé "Index" :  
'*************************************
Public FinMot As Byte

' 0 -> "-"
' 1..26 -> lettres non accentuées
' 27..38 -> è é ê ë à â î ï ù ô ç û (dans l'ordre)

Private ListeAlpha(0 To 38) As Index

Public Sub AddCar(car As Byte)
    If ListeAlpha(car) Is Nothing Then
        Set ListeAlpha(car) = New Index
    End If
End Sub

Public Function CarExists(car As Byte) As Boolean
    CarExists = Not (ListeAlpha(car) Is Nothing)
End Function

Public Function SousArbre(car As Byte) As Index
    Set SousArbre = ListeAlpha(car)
End Function

Public Sub KillTree()
    Dim i As Byte
    For i = 0 To 38
        Set ListeAlpha(i) = Nothing
    Next
End Sub

' ***********************************
' Dans un module
' ***********************************
Option Explicit

Sub sauvegarderIndex(idx As Index, file As Long, Optional KILL As Boolean, Optional mot As String)
    ' Sauvegarde l 'index dans le fichier spécifié. Attention, ce n'est pas un nom de fichier qui est passé,
    '  mais un identificateur de fichier : c'est le 'n' dans "open filename for output as #n"
    '  KILL indique si l'index doit etre detruit au fur et à mesure qu'on le sauvegarde
    ' Mot ne doit pas etre remplit, ou alors avec la chaine vide ""
    Dim i As Byte, car As String, n As Long
    n = FreeFile
    
    'i=0:cas "-"
    If Not (idx.SousArbre(0) Is Nothing) Then
        car = "-"
        If idx.SousArbre(0).FinMot Then
            Print #file, mot & car
        End If
        sauvegarderIndex idx.SousArbre(0), file, KILL, mot & car
    End If
    
    ' cas lettres non accentuees
    For i = 1 To 26
        If Not (idx.SousArbre(i) Is Nothing) Then
            car = Chr(i + 96)
            If idx.SousArbre(i).FinMot Then
                Print #file, mot & car
            End If
            sauvegarderIndex idx.SousArbre(i), file, KILL, mot & car
        End If
    Next
    
    ' cas lettres accentuees
    For i = 27 To 37
        If Not (idx.SousArbre(i) Is Nothing) Then
            car = Chr(convertFromIndex(i) + 96)
            If idx.SousArbre(i).FinMot Then
                Print #file, mot & car
            End If
            sauvegarderIndex idx.SousArbre(i), file, KILL, mot & car
        End If
    Next
    If KILL Then idx.KillTree
End Sub

Sub chargerIndex(idx As Index, file As String)
    'charge l'index depuis un fichier qui contient 1 mot par ligne
    Dim n As Long, str As String
    n = FreeFile
    Open file For Input As #n
    While Not EOF(n)
        Input #n, str
        ajouterMot str, idx
    Wend
    Close #n
End Sub

Sub lireIndex(idx As Index, Optional mot As String)
    ' lit l'index et, pour  l'exemple, l'affiche dans la fenetre du debuggeur
    Dim i As Byte, car As String
    
    'i=0 : "-"
    If Not (idx.SousArbre(0) Is Nothing) Then
        car = "-"
        If idx.SousArbre(0).FinMot Then
            Debug.Print (mot & car) ' l'expression (mot & car) correspond au mot devant etre affiché...
        End If
        lireIndex idx.SousArbre(0), mot & car
    End If
    
    ' lettres sans accent
    For i = 1 To 26
        If Not (idx.SousArbre(i) Is Nothing) Then
            car = Chr(i + 96)
            If idx.SousArbre(i).FinMot Then
                Debug.Print (mot & car)
            End If
            lireIndex idx.SousArbre(i), mot & car
        End If
    Next
    
    ' lettres accentuees
    For i = 27 To 37
        If Not (idx.SousArbre(i) Is Nothing) Then
            car = Chr(convertFromIndex(i) + 96)
            If idx.SousArbre(i).FinMot Then
                Debug.Print (mot & car)
            End If
            lireIndex idx.SousArbre(i), mot & car
        End If
    Next
    
End Sub

Function estDansIndex(mot As String, idx As Index) As Boolean
    Dim car As Byte, c As Byte
    ' Algorithme de recherche ...
    ' renvoit TRUE si le mot est dans l'index

    If Len(mot) = 0 Then
        estDansIndex = idx.FinMot
    Else
        c = Asc(Left(mot, 1))
                
        If c = 45 Then ' tiret "-"
            car = 0
        Else ' autres lettres
            car = c - 96
        End If
        
        If car > 26 Then ' lettres accentuees
            convertToIndex car
        End If
        
        If idx.CarExists(car) Then
            estDansIndex = estDansIndex(Mid(mot, 2), idx.SousArbre(car))
        Else
            estDansIndex = False
        End If
    End If
End Function

Function ajouterMot(mot As String, idx As Index) As Boolean
    ' Insère un mot dans l'index et renvoi True si l'ajout a bien été effectué,
    '  False sinon (dans le cas ou le mot existait déjà)
    Dim car As Byte, c As Byte
    c = Asc(Left(mot, 1))
    
    
    If c = 45 Then 'le tiret "-"
        car = 0
    Else ' les autres caractères
        car = c - 96
    End If
    
    
    'Suppression des accents : les c aractères ayant une valeur superieur à 26 sont des lettres
    ' accentuées
    If car > 26 Then
        convertToIndex car
    End If
    
    'ajout du mot. fonction récursive
    idx.AddCar car
    If Len(mot) = 1 Then
        If idx.SousArbre(car).FinMot Then
            ajouterMot = False
        Else
            ajouterMot = True
            idx.SousArbre(car).FinMot = True
        End If
    Else
        ajouterMot = ajouterMot((Mid(mot, 2)), idx.SousArbre(car))
    End If
    
    
End Function

Sub effacerIndex(idx As Index)
    ' exemple de fonction qui purge la mémoire de manière "propre"
    ' En fait je ne sais pas si ça nettoie vraiement de supprimer directement
    ' les objets de la racine de l'arbre, sans parcourir les feuilles, vous pouvez toujours essayer ...
    Dim i As Byte
    For i = 1 To 37
        If Not (idx.SousArbre(i) Is Nothing) Then
            effacerIndex idx.SousArbre(i)
        End If
    Next
    idx.KillTree
End Sub

' Les trois fonctions suivantes servent à la reconnaissonce des lettres
Private Function EstLettre(str As String) As Boolean
    EstLettre = (str >= "a" And str <= "z") Or (str >= "A" And str <= "Z") Or _
                str = "é" Or str = "è" Or str = "ï" Or "û" Or str = "ù" Or str = "î" Or str = "ë" Or _
                str = "à" Or str = "ê" Or str = "ç" Or str = "â" Or str = "ô" Or str = "-"
End Function

Sub convertToIndex(car As Byte)
    Select Case car
        Case 136 To 139
            car = car - 109   'é, è, ê, ë
        Case 128
            car = 31 'à
        Case 130
            car = 32 'â
        Case 142 To 143
            car = car - 109 ' ï î
        Case 153
            car = 35 ' ù
        Case 148
            car = 36 ' ô
        Case 135
            car = 37 ' ç
        Case 155
            car = 38
        Case Else
            MsgBox car
    End Select
End Sub

Function convertFromIndex(i As Byte) As Byte
    Select Case i
        Case 27 To 30, 33, 34
            convertFromIndex = i + 109
        Case 31
            convertFromIndex = 128
        Case 32
            convertFromIndex = 130
        Case 35
            convertFromIndex = 153
        Case 36
            convertFromIndex = 148
        Case 37
            convertFromIndex = 135
        Case 38
            convertFromIndex = 155
    End Select
End Function

Conclusion :


Tout d'abord, donnons nous un premier but :

Supposons que l'on ai un dictionnaire sous la main, sous forme de
fichier texte, contenant TOUS les mots de la langue française.
Il contient plus de 350 000 mots !!!(et pèse environ 3.8 Mo non
compressé et environ 800ko compressé).

On supposera aussi que l'on a de quoi a travailler sur environ 200 Mo
en memoire centrale (ce sera certainement utopique pour certains ...)
et que le dico que l'on a est en un seul bloc.

On souhaite réaliser une application qui va utiliser ce dictionnaire
(du style de MOTUS, DES CHIFFRES ET DES LETTRES - pour les lettres,
évidemment ! -, LE PENDU ...)

Il faut donc rendre ce projet viable en terme de temps de reponse.

Donc ce qu'il faut c'est utiliser une structure optimisée pour la
recherche dans le dico. S'offrent à nous au moins 3 solutions (ce sont
les trois principales qui me viennent a l'esprit) :

- Une recherche "brute" dans le dico :
1/ on parcours tous les mots, un à un, jusqu'àce que l'on trouve
celui qui nous interesse ...
problemes : bien que ce soit très simple à mettre en oeuvre,
c'est très long !!!

2/ on effectue une recherche dichotomique... IL FAUT QUE LE DICO
SOIT TRIE !!! sinon, pas de recherche dichotomique !
C'est rapide : sur un dico de 350 000 mots, il faut en moyenne
18 comparaisons (log(350 000) / log(2)).
Mais y a un blème : les temps d'accès et l'ajout de mots dans
le dictionnaire.
-> acceder directement au fichier nous evitera d'avoir
a charger le dico, mais le temps d'acces sera élevé
-> créer une structure (un tableau de preference) va
prendre peu de memoire :
Souvenez vous: 3.8 Mo le fichier (environ 4 000 000
octets) pour 350 000 mots, soit ONZE lettres environ par mot
DONC en memoire : 24 octets (pour le tableau -
merci VB ;) ) + ( 10 octets(fixes) + 11 octets par mot dans
le tableau) * 350 000 = 7 350 024 octets
Ce qui est somme toute raisonnable !
-> là où ça se complique c'est lorsque l'on veut fair
évoluer le dictionnaire, c'est pas évident d'insérer de
nouveaux mots au bon endroit, lorsqu'on travaille avec un gros
tableau, il faut faire du tri. Et la ça peut être long, voir
très long ! En plus le code devient très vite complexe (dans
le sens où il n'est pas evident a débroussailler), comparé à
la solution que je vais vous proposer.

3/ LA solution qui est le thème de ce tutorial : charger le dico
dans un arbre.
"Mais kêkeucé ?" , certains vont me dire.
Pour ce qui ne savent pas ce qu'est un arbre (en info ;) ):
C'est une structure de donnée qui possède, dans notre
cas, 2 éléments différents : des noeuds et des branches. Et
accessoirement des feuilles, qui sont en fait des noeuds sans
descendants (et sans branches). Un exemple de représentation
d'arbre : l'arbre généalogique. Dans un arbre généalogique, un
noeud est considéré comme une feuille dés lors qu'il n'a pas
de descendant.

Voila pour la brève description.

Attaquons nous maintenant à la description du dico.
En fait, chaque noeud de l'arbre correspondra à l'une des 26
lettres de l'alphabet. Les fils de chaque noeud seront donc
d'autres lettres de l'alphabet.
Par exemple on souhaite rentrer le mot "jeu" dans le
dictionnaire.
Le premier niveau contiendra la lettre "j", le fils (pour
l'instant unique) contiendra la lettre "e" (niv. 2) et le fils de ce
dernier la lettre "u" (niv. 3).
Jusque là pas de problème.
Maintenant on souhaite rajouter le mot "jour".
Rappelez-vous, notre dico ne contient que le mot "jeu". a la
racine on a la lettre "j". Donc pour rajouter "jour", on a pas
besoin de repeter la lettre "j" (niv. 1). Par contre on va creer un
autre fils de cette lettre : la lettre "o" (niv. 2), puis "u"
(niv. 3), puis "r" (niv. 4).
Mais attention ! la lettre "u" au niveau 3 n'est pas la même
pour le mot "jeu" et pour le mot "jour". Pour l'un c'est la
descendante du "e" et pour l'autre c'est la descendante du
"o".
Supposons maintenant qu'on veuille rajouter le mot "jeudi".
Facile !
"j" (n.1) existe déjà,"e" (n.2) aussi, "u" (n.3) aussi, et y a
plus qu'à rajouter "d" et "i" aux niveaux 4 et 5. Mais là se
pose un problème : comment savoir où s'arrète le mot au moment
de la relecture ? Il faut donc créer un indicateur permettant
de savoir si l'on a atteint la fin d'un mot ou non.
Donc pour resumer, chaque noeud contiendra 2 infos : la lettre
et un indicateur pour savoir si l'on est en fin de mot.

Ca y est, notre structure est prète. Il va maintenant falloir
la représenter en VB.
Si vous l'avez remarqué, c'est une structure récursive, donc
pas moyen de la représenter avec qqchose du style "type Dico
... End Type". Il faut utiliser une classe.

Soit la classe Index qui representera notre dico. Voici sa
descritpion :

Public FinMot As Byte

Private ListeAlpha(1 To 26) As Index

Public Sub AddCar(car As Byte)
If ListeAlpha(car) Is Nothing Then
Set ListeAlpha(car) = New Index
End If
End Sub

Public Function CarExists(car As Byte) As Boolean
CarExists = Not (ListeAlpha(car) Is Nothing)
End Function

Public Function SousArbre(car As Byte) As Index
Set SousArbre = ListeAlpha(car)
End Function

Public Sub KillTree()
Dim i As Byte
For i = 1 To 26
Set ListeAlpha(i) = Nothing
Next
End Sub

FinMot est notre indicateur de fin de mot (FinMot = 1 -> fin
du mot), et ListeAlpha est
un tableau de 26 'Index' (appel recursif de la classe), un
pour chaque caractere. On pourrait rajouter une propriété
supplémentaire pour stocker le nom du caractère du noeud, mais
lors de l'implémentations des différents algorithmes de
recherche, d'insertion, et de sauvegardes, l'on s'aperçoit que
c'est inutile, et vous verrez que cela nous permet de gagner
quelques (nombreux) precieux octets.
Vous aurez remarqué que le tableau est Private car il est
impossible en VB qu'un membre PUBLIC d'une classe soit un
tableau.
Donc il faut créer des procédures d'ajouts, et de tests pour
manipuler l'arbre, qui sont heureusement peu nombreuse.
AddCar rajoute le caractère dans l'arbre s'il n'existe pas
déjà.
CarExists renvoi vrai si le caractère existe, càd si l'objet
ListeAlpha(car) fait bien reference à un index (et est donc
différent de NULL - Nothing en VB-).
SousArbre retourne le sous-arbre correspondant au caractère
'car', c'est ce qui permet de parcourir l'arbre, c'est une
fonction vitale !
Enfin une dernière procédure, KillTree qui s'occupe en fait de
décharger l'arbre de la mémoire.

Revenons maintenant à nos stats. Ici, il est vrai, l'on a
besoin de beacoup de mémoire, car contrairement à la méthode
précédente, on ne travaille pas sur des chaines de caractères,
mais sur les caractères eux-même et lon ajoute beaucoup
d'infos supplémentaires pour chaque caractères.

En gros une instance Index requiert

FinMot = 1 octet
+
ListeAlpha = 24 + 26 octets * 4 = 128 octets

soit = 129 octets par Objet Index.

Explication du calcul :
En VB, quelque soit le tableau, il necessite 20 octets + 4
octets par dimensions (d'où le '24') + les données du tableau.
Dans notre cas, c'est un tableau d'objet, ou plutot de
references d'objet ( equivalent à une adresse mémoire, pour
ceux qui ont déjà fait du C): donc 4 octet * 26 cases.

Par exemple, si l'on avait 3 800 000 caractères, cela ferait :
3 800 000 * 129 = 490 200 000 octets (467,5 Mo) !!!

Heureusement, notre structure est optimisée et des lettres sont
créées qu'une fois alors qu'utilisées dans plusieurs mots,
cela n'empèche que le dico, en l'etat prendrait environ 150 Mo,
c'est à peu pres ce que j'ai constaté dans la pratique.

C'est beaucoup, mais les recherches en sont simplifiées :
aucune comparaison n'est nécessaire : par exemple, on
recherche un mot composé de n lettres, lorsqu'on est à l'étage
i (1<=i<=n), si la lettre existe alors on passe à l'étage
suivant et sinon cela signifie que le mot n'existe
pas. Lorsqu'on a testé toutes les lettres du mot, si
l'indicateur du niveau où l'on se trouve est à 1 alors le mot
recherché existe, sinon il n'existe pas. C'est tout ! Et
croyez moi c'est très rapide, car un mot, on l'a vu
précédemment, a 11 lettres en myenne. Et en plus ce sont des
comparaisons très simple, entre valeurs de type 'byte',
contrairement à la solution précédente qui comparait des
chaines de caractères.

De même, vous verrez dans la source que pour trier, il n'y a
rien à faire, l'insertion que l'on pratique trie implicitement
notre index.

Bon maintenant, revenons-en au code. la classe donnée
précédemment ne traite que 26 caractères, c'est à dire les
lettres non-accentuées, c'est pourquoi il faut donc la
modifier légèrement pour qu'elle ne traite plus 26 mais 38
caractères (c'est à peu près ce qu'il faut pour les lettres
accentuées et le tiret "-"). Malheureusement ça va augmenter
considérablement la place en mémoire ( je suis arrivé à près
de 200Mo !!). Mais bon après chacun fait en fonction de ses
besoins. De plus, moi j'ai divisé le dictionnaire en 26 et au
plus ça prend 50 ou 60 Mo en mémoire, ce qui est davantage
acceptable (pour moi du moins).

Enfin du code vallent mieu que des paroles, (quoique pas
toujours ;)) je joint 2 sources, l'une qui permet de gérer le
dico et l'autre qui génère des anagrammes grace aux
arbres. Comme ça vous allez pouvoir vous amuser à faire un jeu
du style 'Des Chiffres et Des Lettres' si le courage vous en
dit !

J'espère que ça vous aura aidé, pour toutes questions,
eclaircissements, n'hésitez pas !

---------------------------

PS dans la zone de code il y a ce qu'il faut pour gerer le dico, en dans le zip un generateur d'anagrammes

Codes Sources

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.