2010-07-06 25 views
20

Acabo de implementar un algoritmo de búsqueda de archivos de mejor coincidencia para encontrar la coincidencia más cercana a una cadena en un diccionario. Después de crear un perfil de mi código, descubrí que la gran mayoría del tiempo se dedica a calcular la distancia entre la consulta y los posibles resultados. Actualmente estoy implementando el algoritmo para calcular la distancia de Levenshtein usando una matriz 2-D, que hace que la implementación sea una operación O (n^2). Esperaba que alguien pudiera sugerir una forma más rápida de hacer lo mismo.La forma más eficiente de calcular la distancia de Levenshtein

Aquí está mi aplicación:

public int calculate(String root,String query) 
{ 
    int arr[][] = new int[root.length()+2][query.length()+2]; 

    for(int i=2;i<root.length()+2;i++) 
    { 
    arr[i][0] = (int)root.charAt(i-2); 
    arr[i][1] = (i-1); 
    } 

    for(int i=2;i<query.length()+2;i++) 
    { 
    arr[0][i] = (int)query.charAt(i-2); 
    arr[1][i] = (i-1); 
    } 

    for(int i=2;i<root.length()+2;i++) 
    for(int j=2;j<query.length()+2;j++) 
    { 
    int diff=0; 
    if(arr[0][j]!=arr[i][0]) 
    diff = 1; 
    arr[i][j]=min((arr[i-1][j]+1),(arr[i][j-1]+1),(arr[i-1][j-1]+diff)); 
    } 

    return arr[root.length()+1][query.length()+1]; 
} 

public int min(int n1, int n2, int n3) 
{ 
    return (int)Math.min(n1,Math.min(n2,n3)); 
} 
+0

sí, las otras cosas son lo suficientemente eficientes. Hice un perfil de mi código y descubrí que el cuello de botella estaba calculando la distancia levenshtein, por lo que estoy tratando de optimizar ese bit en este momento. Estoy implementando la mejora mencionada en el artículo de wikipedia y la seguiré con una implementación del VP-tree para ver cuál es más eficiente. – efficiencyIsBliss

+3

sobre "usar una matriz 2-D, que hace que la implementación sea una operación O (n^2)": calcular una distancia Levenshtein entre dos secuencias sin restricciones ya es una operación O (n^2) independientemente de la cantidad de memoria uso: usar una matriz 2-D solo reduce la velocidad y desperdicia memoria; solo se requiere memoria O (n). –

Respuesta

20

El wikipedia entry en la distancia Levenshtein tiene sugerencias útiles para optimizar el cálculo - el más aplicable en su caso es que si se puede poner una cota k de la distancia máxima de interés (¡cualquier cosa más allá de eso podría ser infinito!) puede reducir el cálculo a O(n times k) en lugar de O(n squared) (básicamente renunciando tan pronto como la distancia mínima posible sea > k).

Puesto que usted está buscando la coincidencia más cercana, puede disminuir progresivamente k a la distancia de la mejor coincidencia encontrada hasta el momento - esto no afectará el comportamiento del peor caso (como los partidos poder estar en la disminución orden de distancia, lo que significa que nunca saldrás de apuros antes) pero el caso promedio debería mejorar.

creo que, si usted necesita para obtener sustancialmente un mejor rendimiento, es posible que tenga que aceptar alguna fuerte compromiso que calcula una distancia más aproximada (y así se pone "una gran coincidencia" en lugar de necesariamente la óptima) .

+0

Acabo de encontrar lo mismo y estaba a punto de publicarlo aquí. ¡Gracias! – efficiencyIsBliss

+0

Siempre puede crear con un pequeño conjunto de coincidencias utilizando la métrica de "distancia aproximada" que se le ocurra, luego use Levenshtein real para clasificarlas. – kibibu

+0

Leí la optimización de la que habla la publicación anterior, pero honestamente no puedo decir que la recibo. No estoy seguro de cómo implementarlo tampoco. ¿Alguien puede ofrecer algo de ayuda? – efficiencyIsBliss

2

El Wikipedia article analiza su algoritmo y varias mejoras. Sin embargo, parece que, al menos en el caso general, O (n^2) es lo mejor que puede obtener. Sin embargo

todavía hay que mejorarla si se puede restringir su problema (por ejemplo, si usted está interesado sólo en la distancia si es más pequeño que d, la complejidad es O (dn) - esto podría tener sentido como un partido cuya la distancia es cercana a la longitud de la cuerda probablemente no sea muy interesante). Ver si se puede explotar las características específicas de su problema ...

+0

Cualquier referencia al algoritmo de estudio O (dn) para la d fija? – Kshitij

7

De acuerdo con un comentario en este blog, Speeding Up Levenshtein, puede usar VP-árboles y lograr O (nlogn). Otro comentario en el mismo blog apunta a python implementation of VP-Trees and Levenshtein. Por favor, háganos saber si esto funciona.

+3

Me doy cuenta de que este es un hilo viejo, pero me confundiste por un minuto. ¡No estás hablando de la misma 'n'!En el OP 'n' es la longitud de la palabra y estamos interesados ​​en el tiempo para calcular la distancia entre 2 palabras. Mientras que para ti 'n' es la cantidad de palabras en el diccionario, y' n log n' es la cantidad de veces que llamas a la función de distancia. Si combina ambos, es 'W k D log D' con' D' tamaño dict, 'W' tamaño de palabra,' k' umbral de distancia. – Antoine

2

Commons-lang tiene una implementación bastante rápida. Ver http://web.archive.org/web/20120526085419/http://www.merriampark.com/ldjava.htm.

Aquí es mi traducción de esto en Scala:

// The code below is based on code from the Apache Commons lang project. 
/* 
* Licensed to the Apache Software Foundation (ASF) under one or more 
* contributor license agreements. See the NOTICE file distributed with this 
* work for additional information regarding copyright ownership. The ASF 
* licenses this file to You under the Apache License, Version 2.0 (the 
* "License"); you may not use this file except in compliance with the 
* License. You may obtain a copy of the License at 
* 
* http://www.apache.org/licenses/LICENSE-2.0 
* 
* Unless required by applicable law or agreed to in writing, software 
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 
* License for the specific language governing permissions and limitations 
* under the License. 
*/ 
/** 
* assert(levenshtein("algorithm", "altruistic")==6) 
* assert(levenshtein("1638452297", "444488444")==9) 
* assert(levenshtein("", "") == 0) 
* assert(levenshtein("", "a") == 1) 
* assert(levenshtein("aaapppp", "") == 7) 
* assert(levenshtein("frog", "fog") == 1) 
* assert(levenshtein("fly", "ant") == 3) 
* assert(levenshtein("elephant", "hippo") == 7) 
* assert(levenshtein("hippo", "elephant") == 7) 
* assert(levenshtein("hippo", "zzzzzzzz") == 8) 
* assert(levenshtein("hello", "hallo") == 1) 
* 
*/ 
def levenshtein(s: CharSequence, t: CharSequence, max: Int = Int.MaxValue) = { 
import scala.annotation.tailrec 
def impl(s: CharSequence, t: CharSequence, n: Int, m: Int) = { 
    // Inside impl n <= m! 
    val p = new Array[Int](n + 1) // 'previous' cost array, horizontally 
    val d = new Array[Int](n + 1) // cost array, horizontally 

    @tailrec def fillP(i: Int) { 
    p(i) = i 
    if (i < n) fillP(i + 1) 
    } 
    fillP(0) 

    @tailrec def eachJ(j: Int, t_j: Char, d: Array[Int], p: Array[Int]): Int = { 
    d(0) = j 
    @tailrec def eachI(i: Int) { 
     val a = d(i - 1) + 1 
     val b = p(i) + 1 
     d(i) = if (a < b) a else { 
     val c = if (s.charAt(i - 1) == t_j) p(i - 1) else p(i - 1) + 1 
     if (b < c) b else c 
     } 
     if (i < n) 
     eachI(i + 1) 
    } 
    eachI(1) 

    if (j < m) 
     eachJ(j + 1, t.charAt(j), p, d) 
    else 
     d(n) 
    } 
    eachJ(1, t.charAt(0), d, p) 
} 

val n = s.length 
val m = t.length 
if (n == 0) m else if (m == 0) n else { 
    if (n > m) impl(t, s, m, n) else impl(s, t, n, m) 
} 

}

+0

El enlace está muerto –

1

Sé que esto es muy tarde pero que es relevante para la discusión que nos ocupa.

Como han mencionado otros, si todo lo que desea hacer es comprobar si la distancia de edición entre dos cadenas está dentro de algún umbral k, puede reducir la complejidad de tiempo a O (kn). Una expresión más precisa sería O ((2k + 1) n).Usted toma una tira que abarca celdas k a cada lado de la celda diagonal (longitud de la tira 2k + 1) y calcula los valores de las celdas que se encuentran en esta banda.

Curiosamente, ha habido un improvement por Li et. Alabama. y esto se ha reducido aún más a O ((k + 1) n).

+0

Explique cómo sucede esto. – sudeepdino008

+0

Puede buscar el papel. Hay un enlace en mi respuesta. –

+1

Ese documento representa la vanguardia de 2012. Ahora es 2016. ¿Hay alguna implementación de referencia práctica de este trabajo, o ha sido reemplazada por una nueva investigación? –

3

Modifiqué la función de distancia VBA de Levenshtein que se encuentra en este post para utilizar una matriz unidimensional. Se realiza mucho más rápido.

'Calculate the Levenshtein Distance between two strings (the number of insertions, 
'deletions, and substitutions needed to transform the first string into the second) 

Public Function LevenshteinDistance2(ByRef s1 As String, ByRef s2 As String) As Long 
Dim L1 As Long, L2 As Long, D() As Long, LD As Long 'Length of input strings and distance matrix 
Dim i As Long, j As Long, ss2 As Long, ssL As Long, cost As Long 'loop counters, loop step, loop start, and cost of substitution for current letter 
Dim cI As Long, cD As Long, cS As Long 'cost of next Insertion, Deletion and Substitution 
Dim L1p1 As Long, L1p2 As Long 'Length of S1 + 1, Length of S1 + 2 

L1 = Len(s1): L2 = Len(s2) 
L1p1 = L1 + 1 
L1p2 = L1 + 2 
LD = (((L1 + 1) * (L2 + 1))) - 1 
ReDim D(0 To LD) 
ss2 = L1 + 1 

For i = 0 To L1 Step 1: D(i) = i: Next i    'setup array positions 0,1,2,3,4,... 
For j = 0 To LD Step ss2: D(j) = j/ss2: Next j  'setup array positions 0,1,2,3,4,... 

For j = 1 To L2 
    ssL = (L1 + 1) * j 
    For i = (ssL + 1) To (ssL + L1) 
     If Mid$(s1, i Mod ssL, 1) <> Mid$(s2, j, 1) Then cost = 1 Else cost = 0 
     cI = D(i - 1) + 1 
     cD = D(i - L1p1) + 1 
     cS = D(i - L1p2) + cost 

     If cI <= cD Then 'Insertion or Substitution 
      If cI <= cS Then D(i) = cI Else D(i) = cS 
     Else 'Deletion or Substitution 
      If cD <= cS Then D(i) = cD Else D(i) = cS 
     End If 
    Next i 
Next j 

LevenshteinDistance2 = D(LD) 
End Function 

He probado esta función con cadena 's1' de longitud 11.304 y 's2' de longitud 5.665 (> 64 millones de comparaciones de caracteres). Con la versión de una dimensión anterior de la función, el tiempo de ejecución es ~ 24 segundos en mi máquina. La función bidimensional original a la que hice referencia en el enlace de arriba requiere ~ 37 segundos para las mismas cadenas. He optimizado la función dimensional única adicionalmente como se muestra a continuación y requiere ~ 10 segundos para las mismas cadenas.

'Calculate the Levenshtein Distance between two strings (the number of insertions, 
'deletions, and substitutions needed to transform the first string into the second) 
Public Function LevenshteinDistance(ByRef s1 As String, ByRef s2 As String) As Long 
Dim L1 As Long, L2 As Long, D() As Long, LD As Long   'Length of input strings and distance matrix 
Dim i As Long, j As Long, ss2 As Long      'loop counters, loop step 
Dim ssL As Long, cost As Long        'loop start, and cost of substitution for current letter 
Dim cI As Long, cD As Long, cS As Long      'cost of next Insertion, Deletion and Substitution 
Dim L1p1 As Long, L1p2 As Long        'Length of S1 + 1, Length of S1 + 2 
Dim sss1() As String, sss2() As String      'Character arrays for string S1 & S2 

L1 = Len(s1): L2 = Len(s2) 
L1p1 = L1 + 1 
L1p2 = L1 + 2 
LD = (((L1 + 1) * (L2 + 1))) - 1 
ReDim D(0 To LD) 
ss2 = L1 + 1 

For i = 0 To L1 Step 1: D(i) = i: Next i     'setup array positions 0,1,2,3,4,... 
For j = 0 To LD Step ss2: D(j) = j/ss2: Next j   'setup array positions 0,1,2,3,4,... 

ReDim sss1(1 To L1)           'Size character array S1 
ReDim sss2(1 To L2)           'Size character array S2 
For i = 1 To L1 Step 1: sss1(i) = Mid$(s1, i, 1): Next i 'Fill S1 character array 
For i = 1 To L2 Step 1: sss2(i) = Mid$(s2, i, 1): Next i 'Fill S2 character array 

For j = 1 To L2 
    ssL = (L1 + 1) * j 
    For i = (ssL + 1) To (ssL + L1) 
     If sss1(i Mod ssL) <> sss2(j) Then cost = 1 Else cost = 0 
     cI = D(i - 1) + 1 
     cD = D(i - L1p1) + 1 
     cS = D(i - L1p2) + cost 
     If cI <= cD Then 'Insertion or Substitution 
      If cI <= cS Then D(i) = cI Else D(i) = cS 
     Else 'Deletion or Substitution 
      If cD <= cS Then D(i) = cD Else D(i) = cS 
     End If 
    Next i 
Next j 

LevenshteinDistance = D(LD) 
End Function 
Cuestiones relacionadas