Wie berechnet man die Levenshtein-Distanz in Java?

Wie Levenshtein Abstand in Java berechnen?

1. Einführung

In diesem Artikel beschreiben wir die Levenshtein-Distanz, auch als Edit-Distanz bekannt. Der hier erläuterte Algorithmus wurde 1965 von dem russischen Wissenschaftler Vladimir Levenshtein entwickelt.

Wir werden eine iterative und eine rekursive Java-Implementierung dieses Algorithmus bereitstellen.

2. Was ist die Levenshtein Entfernung?

Der Levenshtein-Abstand ist ein Maß für die Unähnlichkeit zwischen zweiStrings.. Mathematisch gesehen misst der Abstand bei zweiStringsx undy die minimale Anzahl von Zeichenänderungen, die zur Transformation vonx iny.

In der Regel sind drei Arten von Bearbeitungen zulässig:

  1. Einfügen eines Zeichensc

  2. Löschen eines Zeichensc

  3. Ersetzen eines Zeichensc durchc

Beispiel:If x = ‘shot' and y = ‘spot', the edit distance between the two is 1 because ‘shot' can be converted to ‘spot' by substituting ‘h‘ to ‘p‘.

In bestimmten Unterklassen des Problems können die mit den einzelnen Bearbeitungstypen verbundenen Kosten unterschiedlich sein.

Zum Beispiel weniger Kosten für die Ersetzung durch ein Zeichen, das sich in der Nähe der Tastatur befindet, und mehr Kosten ansonsten. Der Einfachheit halber werden in diesem Artikel alle Kosten als gleich angesehen.

Einige der Anwendungen für die Bearbeitung von Entfernungen sind:

  1. Rechtschreibprüfung - Rechtschreibfehler im Text erkennen und die genaueste Rechtschreibung im Wörterbuch finden

  2. Plagiatserkennung (siehe -IEEE Paper)

  3. DNA-Analyse - Auffinden der Ähnlichkeit zwischen zwei Sequenzen

  4. Spracherkennung (refer -Microsoft Research)

3. Algorithmusformulierung

Nehmen wir zweiStrings x undy der Längenm bzw.n. Wir können jedesString alsx[1:m] undy[1:n]. bezeichnen

Wir wissen, dass am Ende der Transformation beideStrings gleich lang sind und an jeder Position übereinstimmende Zeichen haben. Wenn wir also das erste Zeichen jedesString,betrachten, haben wir drei Optionen:

  1. Auswechslung:

    1. Bestimmen Sie die Kosten (D1) für das Ersetzen vonx[1] durchy[1]. Die Kosten für diesen Schritt wären Null, wenn beide Zeichen gleich wären. Wenn nicht, dann wären die Kosten eins

    2. Nach Schritt 1.1 wissen wir, dass beideStringsmit demselben Zeichen beginnen. Daher wären die Gesamtkosten nun die Summe der Kosten von Schritt 1.1 und der Kosten für die Umwandlung der restlichenString x[2:m] iny[2:n]

  2. Einfügung:

    1. Fügen Sie ein Zeichen inx ein, um mit dem ersten Zeichen iny übereinzustimmen. Die Kosten für diesen Schritt wären eins

    2. Nach 2.1 haben wir ein Zeichen ausy verarbeitet. Daher wären die Gesamtkosten nun die Summe der Kosten von Schritt 2.1 (d. H. 1) und der Kosten für die Umwandlung der vollenx[1:m] in verbleibendey (y[2:n])

  3. Streichung:

    1. Löschen Sie das erste Zeichen ausx, die Kosten für diesen Schritt wären eins

    2. Nach 3.1 haben wir ein Zeichen ausx verarbeitet, aber das volley muss noch verarbeitet werden. Die Gesamtkosten wären die Summe der Kosten von 3,1 (d. H. 1) und der Kosten für die Umwandlung der verbleibendenx in die volleny

Der nächste Teil der Lösung besteht darin, herauszufinden, welche Option Sie aus diesen drei auswählen können. Da wir nicht wissen, welche Option am Ende zu minimalen Kosten führen würde, müssen wir alle Optionen ausprobieren und die beste auswählen.

4. Naive rekursive Implementierung

Wir können sehen, dass der zweite Schritt jeder Option in Abschnitt 3 größtenteils das gleiche Problem mit dem Bearbeitungsabstand ist, jedoch mit Teilzeichenfolgen der ursprünglichenStrings.. Dies bedeutet, dass wir nach jeder Iteration das gleiche Problem haben, jedoch mit kleinerenStrings.

Diese Beobachtung ist der Schlüssel zur Formulierung eines rekursiven Algorithmus. Die Wiederholungsbeziehung kann wie folgt definiert werden:

D (x [1: m], y [1: n]) = min \ {

D (x [2: m], y [2: n]) + Kosten für das Ersetzen von x [1] durch y [1],

D (x [1: m], y [2: n]) + 1,

D (x [2: m], y [1: n]) + 1

}

Wir müssen auch Basisfälle für unseren rekursiven Algorithmus definieren. In unserem Fall werden ein oder beideStringsleer:

  1. Wenn beideStrings leer sind, ist der Abstand zwischen ihnen Null

  2. Wenn eines derStrings leer ist, entspricht der Bearbeitungsabstand zwischen ihnen der Länge des anderenString,, da wir so viele Einfügungen / Löschungen benötigen, um eines in das andere umzuwandeln:

    • Beispiel: Wenn einString“dog” und ein anderesString“” (leer) ist, benötigen wir drei Einfügungen in leerString, um“dog”zu erhalten ) s, oder wir brauchen drei Löschungen in“dog”, um es leer zu machen. Daher beträgt der Bearbeitungsabstand zwischen ihnen 3

Eine naive rekursive Implementierung dieses Algorithmus:

public class EditDistanceRecursive {

   static int calculate(String x, String y) {
        if (x.isEmpty()) {
            return y.length();
        }

        if (y.isEmpty()) {
            return x.length();
        }

        int substitution = calculate(x.substring(1), y.substring(1))
         + costOfSubstitution(x.charAt(0), y.charAt(0));
        int insertion = calculate(x, y.substring(1)) + 1;
        int deletion = calculate(x.substring(1), y) + 1;

        return min(substitution, insertion, deletion);
    }

    public static int costOfSubstitution(char a, char b) {
        return a == b ? 0 : 1;
    }

    public static int min(int... numbers) {
        return Arrays.stream(numbers)
          .min().orElse(Integer.MAX_VALUE);
    }
}

This algorithm has the exponential complexity. Bei jedem Schritt verzweigen wir uns in drei rekursive Aufrufe, um die Komplexität vonO(3^n)aufzubauen.

Im nächsten Abschnitt werden wir sehen, wie wir dies verbessern können.

5. Dynamischer Programmieransatz

Bei der Analyse der rekursiven Aufrufe stellen wir fest, dass die Argumente für Unterprobleme Suffixe der ursprünglichenStrings. sind. Dies bedeutet, dass es nurm*n eindeutige rekursive Aufrufe geben kann (wobeim undn) s sind eine Anzahl von Suffixen vonx undy). Daher sollte die Komplexität der optimalen Lösung quadratisch sein,O(m*n).

Schauen wir uns einige der Unterprobleme an (gemäß der in Abschnitt 4 definierten Wiederholungsrelation):

  1. Unterprobleme vonD(x[1:m], y[1:n]) sindD(x[2:m], y[2:n]), D(x[1:m], y[2:n]) undD(x[2:m], y[1:n])

  2. Unterprobleme vonD(x[1:m], y[2:n]) sindD(x[2:m], y[3:n]), D(x[1:m], y[3:n]) undD(x[2:m], y[2:n])

  3. Unterprobleme vonD(x[2:m], y[1:n]) sindD(x[3:m], y[2:n]), D(x[2:m], y[2:n]) undD(x[3:m], y[1:n])

In allen drei Fällen ist eines der UnterproblemeD(x[2:m], y[2:n]).. Anstatt dies dreimal wie in der naiven Implementierung zu berechnen, können wir dies einmal berechnen und das Ergebnis bei Bedarf erneut verwenden.

Dieses Problem hat viele überlappende Unterprobleme, aber wenn wir die Lösung für die Unterprobleme kennen, können wir leicht die Antwort auf das ursprüngliche Problem finden. Daher haben wir beide Eigenschaften, die zum Formulieren einer dynamischen Programmierlösung benötigt werden, d. H.Overlapping Sub-Problems undOptimal Substructure.

Wir können die naive Implementierung optimieren, indem wirmemoization einführen, d. H. Das Ergebnis der Unterprobleme in einem Array speichern und die zwischengespeicherten Ergebnisse wiederverwenden.

Alternativ können wir dies auch iterativ implementieren, indem wir einen tabellenbasierten Ansatz verwenden:

static int calculate(String x, String y) {
    int[][] dp = new int[x.length() + 1][y.length() + 1];

    for (int i = 0; i <= x.length(); i++) {
        for (int j = 0; j <= y.length(); j++) {
            if (i == 0) {
                dp[i][j] = j;
            }
            else if (j == 0) {
                dp[i][j] = i;
            }
            else {
                dp[i][j] = min(dp[i - 1][j - 1]
                 + costOfSubstitution(x.charAt(i - 1), y.charAt(j - 1)),
                  dp[i - 1][j] + 1,
                  dp[i][j - 1] + 1);
            }
        }
    }

    return dp[x.length()][y.length()];
}

Dieser Algorithmus bietet eine deutlich bessere Leistung als die rekursive Implementierung. Es ist jedoch mit einem erheblichen Speicherverbrauch verbunden.

Dies kann weiter optimiert werden, indem beobachtet wird, dass wir nur den Wert von drei benachbarten Zellen in der Tabelle benötigen, um den Wert der aktuellen Zelle zu finden.

6. Fazit

In diesem Artikel haben wir beschrieben, was Levenshtein-Abstand ist und wie er mit einem rekursiven und einem auf dynamischer Programmierung basierenden Ansatz berechnet werden kann.

Der Levenshtein-Abstand ist nur eines der Maße für die Ähnlichkeit von Zeichenfolgen. Einige der anderen Metriken sindCosine Similarity (das einen tokenbasierten Ansatz verwendet und die Zeichenfolgen als Vektoren betrachtet),Dice Coefficient usw.

Wie immer kann die vollständige Implementierung von Beispielenover on GitHub gefunden werden.