Implémentation d'un Trie simple pour un calcul efficace de la distance de Levenshtein-Java

Mise à jour 3

Fait. Voici le code qui a finalement passé tous mes tests. Encore une fois, ceci est calqué sur la version modifiée de Murilo Vasconcelo de L'algorithme de Steve Hanov. Merci à tout ce qui a aidé!

/**
 * Computes the minimum Levenshtein Distance between the given word (represented as an array of Characters) and the
 * words stored in theTrie. This algorithm is modeled after Steve Hanov's blog article "Fast and Easy Levenshtein
 * distance using a Trie" and Murilo Vasconcelo's revised version in C++.
 * 
 * http://stevehanov.ca/blog/index.php?id=114
 * http://murilo.wordpress.com/2011/02/01/fast-and-easy-levenshtein-distance-using-a-trie-in-c/
 * 
 * @param ArrayList<Character> word - the characters of an input word as an array representation
 * @return int - the minimum Levenshtein Distance
 */
private int computeMinimumLevenshteinDistance(ArrayList<Character> word) {

    theTrie.minLevDist = Integer.MAX_VALUE;

    int iWordLength = word.size();
    int[] currentRow = new int[iWordLength + 1];

    for (int i = 0; i <= iWordLength; i++) {
        currentRow[i] = i;
    }

    for (int i = 0; i < iWordLength; i++) {
        traverseTrie(theTrie.root, word.get(i), word, currentRow);
    }
    return theTrie.minLevDist;
}

/**
 * Recursive helper function. Traverses theTrie in search of the minimum Levenshtein Distance.
 * 
 * @param TrieNode node - the current TrieNode
 * @param char letter - the current character of the current word we're working with
 * @param ArrayList<Character> word - an array representation of the current word
 * @param int[] previousRow - a row in the Levenshtein Distance matrix
 */
private void traverseTrie(TrieNode node, char letter, ArrayList<Character> word, int[] previousRow) {

    int size = previousRow.length;
    int[] currentRow = new int[size];
    currentRow[0] = previousRow[0] + 1;

    int minimumElement = currentRow[0];
    int insertCost, deleteCost, replaceCost;

    for (int i = 1; i < size; i++) {

        insertCost = currentRow[i - 1] + 1;
        deleteCost = previousRow[i] + 1;

        if (word.get(i - 1) == letter) {
            replaceCost = previousRow[i - 1];
        } else {
            replaceCost = previousRow[i - 1] + 1;
        }

        currentRow[i] = minimum(insertCost, deleteCost, replaceCost);

        if (currentRow[i] < minimumElement) {
            minimumElement = currentRow[i];
        }
    }

    if (currentRow[size - 1] < theTrie.minLevDist && node.isWord) {
        theTrie.minLevDist = currentRow[size - 1];
    }

    if (minimumElement < theTrie.minLevDist) {

        for (Character c : node.children.keySet()) {
            traverseTrie(node.children.get(c), c, word, currentRow);
        }
    }
}

Mise à jour 2

Enfin, j'ai réussi à faire fonctionner cela pour la plupart de mes cas de test. Mon implémentation est pratiquement une traduction directe de la version C++ de Murilo de l'algorithme de Steve Hanov . Alors, comment devrais-je refactoriser cet algorithme et / ou faire des optimisations? Ci-dessous est le code...

public int search(String word) {

    theTrie.minLevDist = Integer.MAX_VALUE;

    int size = word.length();
    int[] currentRow = new int[size + 1];

    for (int i = 0; i <= size; i++) {
        currentRow[i] = i;
    }
    for (int i = 0; i < size; i++) {
        char c = word.charAt(i);
        if (theTrie.root.children.containsKey(c)) {
            searchRec(theTrie.root.children.get(c), c, word, currentRow);
        }
    }
    return theTrie.minLevDist;
}
private void searchRec(TrieNode node, char letter, String word, int[] previousRow) {

    int size = previousRow.length;
    int[] currentRow = new int[size];
    currentRow[0] = previousRow[0] + 1;

    int insertCost, deleteCost, replaceCost;

    for (int i = 1; i < size; i++) {

        insertCost = currentRow[i - 1] + 1;
        deleteCost = previousRow[i] + 1;

        if (word.charAt(i - 1) == letter) {
            replaceCost = previousRow[i - 1];
        } else {
            replaceCost = previousRow[i - 1] + 1;
        }
        currentRow[i] = minimum(insertCost, deleteCost, replaceCost);
    }

    if (currentRow[size - 1] < theTrie.minLevDist && node.isWord) {
        theTrie.minLevDist = currentRow[size - 1];
    }

    if (minElement(currentRow) < theTrie.minLevDist) {

        for (Character c : node.children.keySet()) {
            searchRec(node.children.get(c), c, word, currentRow);

        }
    }
}

Merci à tous ceux qui ont contribué à cette question. J'ai essayé de faire fonctionner les automates Levenshtein, mais je n'ai pas pu y arriver.

Donc, je cherche des suggestions sur le refactoring et / ou les optimisations concernant le code ci-dessus. Faites-moi savoir s'il y a confusion. Comme toujours, je peux fournir le reste du code source au besoin.


Mise à jour 1

Donc j'ai implémenté une structure de données trie simple et j'ai essayé de suivre le tutoriel Python de Steve Hanov pour calculer la Distance de Levenshtein. En fait, je suis intéressé par le calcul du minimum Levenshtein Distance entre un mot donné et les mots dans le Trie, ainsi j'ai suivi la version de Murilo Vasconcelos de L'algorithme de Steve Hanov . Cela ne fonctionne pas très bien, Mais voici ma classe Trie:

public class Trie {

    public TrieNode root;
    public int minLevDist;

    public Trie() {
        this.root = new TrieNode(' ');
    }

    public void insert(String word) {

        int length = word.length();
        TrieNode current = this.root;

        if (length == 0) {
            current.isWord = true;
        }
        for (int index = 0; index < length; index++) {

            char letter = word.charAt(index);
            TrieNode child = current.getChild(letter);

            if (child != null) {
                current = child;
            } else {
                current.children.put(letter, new TrieNode(letter));
                current = current.getChild(letter);
            }
            if (index == length - 1) {
                current.isWord = true;
            }
        }
    }
}

... et la classe TrieNode:

public class TrieNode {

    public final int ALPHABET = 26;

    public char letter;
    public boolean isWord;
    public Map<Character, TrieNode> children;

    public TrieNode(char letter) {
        this.isWord = false;
        this.letter = letter;
        children = new HashMap<Character, TrieNode>(ALPHABET);
    }

    public TrieNode getChild(char letter) {

        if (children != null) {
            if (children.containsKey(letter)) {
                return children.get(letter); 
            }
        }
        return null;
    }
}

Maintenant, J'ai essayé d'implémenter la recherche comme Murilo Vasconcelos l'A, mais quelque chose est éteint et j'ai besoin d'aide pour déboguer cela. Veuillez donner des suggestions sur la façon de refactoriser cela et / ou indiquer où se trouvent les bogues. La toute première chose que je voudrais refactoriser est la variable globale "minCost", mais c'est la plus petite des choses. De toute façon, voici le code...

public void search(String word) {

    int size = word.length();
    int[] currentRow = new int[size + 1];

    for (int i = 0; i <= size; i++) {
        currentRow[i] = i;
    }
    for (int i = 0; i < size; i++) {
        char c = word.charAt(i);
        if (theTrie.root.children.containsKey(c)) {
            searchRec(theTrie.root.children.get(c), c, word, currentRow);
        }
    }
}

private void searchRec(TrieNode node, char letter, String word, int[] previousRow) {

    int size = previousRow.length;
    int[] currentRow = new int[size];
    currentRow[0] = previousRow[0] + 1;

    int replace, insertCost, deleteCost;

    for (int i = 1; i < size; i++) {

        char c = word.charAt(i - 1);

        insertCost = currentRow[i - 1] + 1;
        deleteCost = previousRow[i] + 1;
        replace = (c == letter) ? previousRow[i - 1] : (previousRow[i - 1] + 1);

        currentRow[i] = minimum(insertCost, deleteCost, replace);
    }

    if (currentRow[size - 1] < minCost && !node.isWord) {
        minCost = currentRow[size - 1];
    }
    Integer minElement = minElement(currentRow);
    if (minElement < minCost) {

        for (Map.Entry<Character, TrieNode> entry : node.children.entrySet()) {
            searchRec(node, entry.getKey(), word, currentRow);
        }
    }
}

Je m'excuse pour le manque de commentaires. Donc, ce que je fais mal?

POSTE INITIAL

J'ai lu un article, Distance de Levenshtein rapide et facile en utilisant un Trie , dans l'espoir de trouver un moyen efficace de calculer la Distance de Levenshtein entre deux chaînes. Mon objectif principal avec ceci est, étant donné un grand ensemble de mots, de pouvoir trouver la Distance minimale de Levenshtein entre un mot d'entrée et cet ensemble de mots.

Dans mon implémentation triviale, Je calcule la Distance de Levenshtein entre un mot d'entrée et l'ensemble des mots, pour chaque mot d'entrée, et renvoie le minimum. Cela fonctionne, mais ce n'est pas efficace...

J'ai cherché des implémentations d'un Trie, en Java, et j'ai rencontré deux sources apparemment bonnes:

Cependant, ces implémentations semblent trop compliquées pour ce que j'essaie de faire. Comme je les ai lus pour comprendre comment ils fonctionnent et comment les structures de données Trie fonctionnent en général, je suis seulement devenu plus confus.

Alors, comment implémenterais-je une structure de données trie simple en Java? Mon intuition me dit que chaque TrieNode devrait stocker la chaîne qu'il représente et aussi des références aux lettres de l'alphabet, pas nécessairement toutes les lettres. Mon intuition est-elle correcte?

Une fois que cela est implémenté, la tâche suivante consiste à calculer la Distance de Levenshtein. J'ai lu l'exemple de code Python dans l'article ci-dessus, mais je ne parle pas Python, et mon implémentation Java manque de mémoire de tas une fois que j'ai frappé la recherche récursive. Alors, comment pourrais-je calculer la Distance de Levenshtein en utilisant la structure de données Trie? J'ai une implémentation triviale, modélisée d'après ce code source, mais il n'utilise pas de Trie... il est inefficace.

Ce serait vraiment bien de voir du code en plus de vos commentaires et suggestions. Après tout, c'est un processus d'apprentissage pour moi... Je n'ai jamais mis en œuvre un Trie... j'ai donc beaucoup à apprendre de cette expérience.

Merci.

P. s. je peux fournir n'importe quel code source si nécessaire. De plus, j'ai déjà lu et essayé d'utiliser un arbre BK comme suggéré dans Le blog de Nick Johnson, mais ce n'est pas aussi efficace que je le pense... ou peut-être que ma mise en œuvre est fausse.

36
demandé sur Hristo 2011-02-02 02:01:30

11 réponses

J'ai implémenté l'algo décrit sur l'article" fast and Easy Levenshtein distance using a trie " en C++ et c'est vraiment rapide. Si vous voulez (comprendre C++ mieux que Python), je peux dépasser le code quelque part.

Modifier: Je l'ai posté sur mon blog .

8
répondu Murilo Vasconcelos 2011-02-02 00:57:59

D'après ce que je peux dire, vous n'avez pas besoin d'améliorer l'efficacité de Levenshtein Distance, vous devez stocker vos chaînes dans une structure qui vous empêche d'exécuter des calculs de distance tant de fois, c'est-à-dire en élaguant l'espace de recherche.

Puisque la distance de Levenshtein est une métrique, vous pouvez utiliser n'importe lequel des indices d'espaces métriques qui tirent parti de l'inégalité des triangles - vous avez mentionné BK-Trees, mais il y en a d'autres, par exemple. Arbres De Point De Vue, Arbres Fixes-Requêtes, Arbres Bissectrices, Rapprochement Des Arbres. Voici leurs descriptions:

Arbre Burkhard-Keller

Les nœuds sont insérés dans l'arborescence comme suit: Pour le nœud racine choisissez un élément arbitraire de l'espace; ajouter unique bord-marqué enfants tels que la valeur de chaque bord est la distance entre le pivot qui élément; appliquer récursivement, en sélectionnant enfant comme pivot quand un bord déjà exister.

Arbre De Requêtes Fixes

Comme avec BKTs sauf: les éléments sont stockés à feuilles; chaque feuille a plusieurs éléments; Pour chaque niveau de l'arbre le même pivot est utiliser.

Arbre Bissectrice

Chaque nœud contient deux éléments de pivot avec leur rayon de couverture (maximum distance entre l'élément central et L'un de ses éléments de sous-arbre); filtrer en deux définit les éléments les plus proches de le premier pivot et ceux les plus proches de la deuxièmement, et construire récursivement deux sous-arbres à partir de ces ensembles.

Spatiale Rapprochement Arbre

Initialement, tous les éléments sont dans un sac; choisissez un élément arbitraire pour être le pivot; construire une collection de voisins les plus proches dans plage du pivot; mettre chaque restant élément dans le sac du plus proche élément de la collection vient de construire; Former récursivement un sous-arbre de chaque élément de cette collection.

Arbre De Point De Vue

Choisissez un pivot dans l'ensemble abitrarily; Calculer la distance médiane entre cette pivot et chaque élément du restant set; éléments filtrants de l'ensemble vers la gauche et à droite sous arbres récursifs tels que ceux dont les distances sont inférieures ou égales à la médiane forme la gauche et les plus grandes formulaire de droite.

9
répondu Robert 2011-02-02 01:14:25

Voici un exemple de Levenshtein Automates en Java.Ceux - ci seront probablement également utiles:

Http://svn.apache.org/repos/asf/lucene/dev/trunk/lucene/src/java/org/apache/lucene/util/automaton/ http://svn.apache.org/repos/asf/lucene/dev/trunk/lucene/src/test/org/apache/lucene/util/automaton/

Il semble que le code Lucene expérimental soit basé sur dk.BRIC.automate paquet.

L'utilisation semble être quelque chose de similaire à ci-dessous:

LevenshteinAutomata builder = new LevenshteinAutomata(s);
Automaton automata = builder.toAutomaton(n);
boolean result1 = BasicOperations.run(automata, "foo");
boolean result2 = BasicOperations.run(automata, "bar");
3
répondu Taylor Leese 2011-02-02 00:35:27

À bien des égards, l'algorithme de Steve Hanov (présenté dans le premier article lié à la question, Distance Levenshtein rapide et facile en utilisant un Trie ), les ports de L'algorithme fait par Murilo et vous (OP), et très probablement tout algorithme pertinent impliquant un trie ou une structure similaire, fonctionnent un peu comme un automate Levenshtein]}

Given:
       dict is a dictionary represented as a DFA (ex. trie or dawg)
       dictState is a state in dict
       dictStartState is the start state in dict
       dictAcceptState is a dictState arrived at after following the transitions defined by a word in dict
       editDistance is an edit distance
       laWord is a word
       la is a Levenshtein Automaton defined for laWord and editDistance
       laState is a state in la
       laStartState is the start state in la
       laAcceptState is a laState arrived at after following the transitions defined by a word that is within editDistance of laWord
       charSequence is a sequence of chars
       traversalDataStack is a stack of (dictState, laState, charSequence) tuples

Define dictState as dictStartState
Define laState as laStartState
Push (dictState, laState, "") on to traversalDataStack
While traversalDataStack is not empty
    Define currentTraversalDataTuple as the the product of a pop of traversalDataStack
    Define currentDictState as the dictState in currentTraversalDataTuple
    Define currentLAState as the laState in currentTraversalDataTuple
    Define currentCharSequence as the charSequence in currentTraversalDataTuple
    For each char in alphabet
        Check if currentDictState has outgoing transition labeled by char
        Check if currentLAState has outgoing transition labeled by char
        If both currentDictState and currentLAState have outgoing transitions labeled by char
            Define newDictState as the state arrived at after following the outgoing transition of dictState labeled by char
            Define newLAState as the state arrived at after following the outgoing transition of laState labeled by char
            Define newCharSequence as concatenation of currentCharSequence and char
            Push (newDictState, newLAState, newCharSequence) on to currentTraversalDataTuple
            If newDictState is a dictAcceptState, and if newLAState is a laAcceptState
                Add newCharSequence to resultSet
            endIf
        endIf
    endFor
endWhile

L'algorithme de Steve Hanov et ses dérivés mentionnés ci-dessus utilisent évidemment un Levenshtein calcul de la matrice en place d'une structure officielle de Levenshtein Automate. assez rapide, mais un automate Levenshtein formel peut avoir ses États paramétriques (États abstraits qui décrivent les États concrets de l'automate) générés et utilisés pour la traversée, en contournant tout calcul d'exécution lié à la distance d'édition. Donc, il devrait être exécuté encore plus rapidement que les algorithmes susmentionnés.

Si vous (ou quelqu'un d'autre) est intéressé par un officiel Levenshtein Automaton solution , jetez un oeil à LevenshteinAutomaton . Il implémente l'algorithme basé sur l'état paramétrique susmentionné, ainsi qu'un algorithme basé sur la traversée d'état concret pur (décrit ci-dessus) et des algorithmes basés sur la programmation dynamique (pour la détermination de la distance d'édition et du voisin). Il est maintenu par le vôtre vraiment :).

2
répondu Kevin 2016-07-02 17:01:15

Mon intuition me dit que chaque TrieNode doit stocker la chaîne qu'il représente et aussi des références aux lettres de l'alphabet, pas nécessairement toutes les lettres. Mon intuition est-elle correcte?

Non, un trie ne représente pas une chaîne, il représente un ensemble de chaînes (et tous leurs préfixes). Un nœud trie mappe un caractère d'entrée à un autre nœud trie. Il devrait donc contenir quelque chose comme un tableau de caractères et un tableau correspondant de références TrieNode. (Peut-être pas exact représentation, en fonction de l'efficacité dans votre utilisation de l'informatique.)

1
répondu Darius Bacon 2011-02-02 01:15:02

Comme je le vois bien, vous voulez faire une boucle sur toutes les branches de la trie. Ce n'est pas si difficile d'utiliser une fonction récursive. J'utilise également un trie dans mon algorithme K-le plus proche voisin, en utilisant le même type de fonction. Je ne connais pas Java, cependant, mais voici un pseudocode:

function walk (testitem trie)
   make an empty array results
   function compare (testitem children distance)
     if testitem = None
        place the distance and children into results
     else compare(testitem from second position, 
                  the sub-children of the first child in children,
                  if the first item of testitem is equal to that 
                  of the node of the first child of children 
                  add one to the distance (! non-destructive)
                  else just the distance)
        when there are any children left
             compare (testitem, the children without the first item,
                      distance)
    compare(testitem, children of root-node in trie, distance set to 0)
    return the results

J'espère que ça aide.

1
répondu Folgert 2011-02-03 21:08:57

La marche de fonction prend un testitem (par exemple une chaîne indexable, ou un tableau de caractères) et un trie. Un trie peut être un objet avec deux emplacements. L'un spécifiant le nœud du trie, l'autre les enfants de ce nœud. Les enfants sont essaie tant bien. En python, ce serait quelque chose comme:

class Trie(object):
    def __init__(self, node=None, children=[]):
        self.node = node
        self.children = children

Ou en Lisp...

(defstruct trie (node nil) (children nil))

Maintenant, un trie ressemble à ceci:

(trie #node None
      #children ((trie #node f
                       #children ((trie #node o
                                        #children ((trie #node o
                                                         #children None)))
                                  (trie #node u
                                        #children ((trie #node n
                                                         #children None)))))))

Maintenant, la fonction interne (que vous pouvez également écrire séparément) prend le testitem, le enfants du nœud racine de l'arbre (dont la valeur du nœud est None ou autre), et une distance initiale définie sur 0.

Ensuite, nous traversons récursivement les deux branches de l'arbre, en commençant à gauche puis à droite.

1
répondu Folgert 2011-02-04 08:18:57

Je vais juste laisser ceci ici au cas où quelqu'un cherche un autre traitement de ce problème:

Http://code.google.com/p/oracleofwoodyallen/wiki/ApproximateStringMatching

1
répondu spieden 2012-03-24 17:43:37

Je regardais votre dernière mise à jour 3, l'algorithme ne semble pas bien fonctionner pour moi.

Voyons voir que vous avez des cas de test ci-dessous:

    Trie dict = new Trie();
    dict.insert("arb");
    dict.insert("area");

    ArrayList<Character> word = new ArrayList<Character>();
    word.add('a');
    word.add('r');
    word.add('c');

Dans ce cas, la distance d'édition minimale entre "arc" et le dict devrait être 1, qui est la distance d'édition entre "arc" et "arb", mais vos algorithmes retourneront 2 à la place.

Je suis passé par le morceau de code ci-dessous:

        if (word.get(i - 1) == letter) {
            replaceCost = previousRow[i - 1];
        } else {
            replaceCost = previousRow[i - 1] + 1;
        }

Au moins pour la première boucle, la lettre est l'un des caractères du mot, mais à la place, vous devriez être comparez les nœuds dans le trie, donc il y aura une ligne dupliquée avec le premier caractère dans le mot, est-ce vrai? chaque matrice DP a la première ligne en double. J'ai exécuté exactement le même code que vous avez mis sur la solution.

1
répondu zdlgrj 2014-11-13 05:38:59

Eh Bien, voici comment je l'ai fait il y a longtemps. J'ai stocké le dictionnaire comme un trie, qui est simplement une machine à états finis limitée à la forme d'un arbre. Vous pouvez l'améliorer en ne faisant pas cette restriction. Par exemple, les suffixes communs peuvent simplement être un sous-arbre partagé. Vous pourriez même avoir des boucles, pour capturer des trucs comme "nation", "national", "nationaliser", "nationalisation", ...

Gardez le trie aussi simple que possible. Ne va pas fourrer des cordes il.

Rappelez-vous, vous ne faites pas cela pour trouver la distance entre deux chaînes données. Vous l'utilisez pour trouver les chaînes dans le dictionnaire qui sont les plus proches d'une chaîne donnée. Le temps que cela prend dépend de la distance levenshtein que vous pouvez tolérer. Pour la distance zéro, c'est simplement O(n) où n est la longueur du mot. Pour la distance arbitraire, C'est O (N) où N est le nombre de mots dans le dictionnaire.

0
répondu Mike Dunlavey 2017-05-23 12:25:26

Corrigez-moi si je me trompe mais je crois que votre update3 a une boucle supplémentaire qui est inutile et rend le programme beaucoup plus lent:

for (int i = 0; i < iWordLength; i++) {
    traverseTrie(theTrie.root, word.get(i), word, currentRow);
}

Vous devriez appeler traverseTrie une seule fois parce que dans traverseTrie vous êtes déjà en boucle sur le mot entier. Le code devrait être seulement comme suit:

traverseTrie(theTrie.root, ' ', word, currentRow);
0
répondu user4980248 2015-06-06 03:05:24