Archivo de Etiquetas de 'array'

Distancia de Levenshtein en VisualBasic.NET.

Hace un rato se me ha planteado un problema que quien más quien menos habrá tenido en alguna ocasión. Necesitaba poder comparar dos cadenas y obtener algún indicador de hasta qué punto podría tratarse de la misma cadena escrita de maneras ligeramente diferentes. He estado dándole vueltas sobre cómo podría jugar con los distintos métodos de la clase String para conseguir algún valor. Me he planteado utilizar análisis de frecuencias, generar números ponderados en función de carácter y posición dentro de la cadena… Pero nada me convencía, así que he insistido un poco más en buscar si ya alguien había desarrollado alguna función parecida y para mi mayúscula sorpresa me he dado de bruces con la Distancia de Levenshtein.

Resulta que esto que andaba buscando es exactamente esta distancia. Y tal como reza la Wikipedia… “se llama Distancia de Levenshtein o distancia de edición al número mínimo de operaciones requeridas para transformar una cadena de caracteres en otra. Se entiende por operación, bien una inserción, eliminación o la substitución de un carácter. Esta distancia recibe ese nombre en honor al científico ruso Vladimir Levenshtein, quien se ocupara de esta distancia en 1965. Es útil en programas que determinan cuán similares son dos cadenas de caracteres, como es el caso de los correctores de ortografía.”

Y para mayor regocijo, en la propia página de la Wikipedia aparece el algoritmo en pseudocódigo de la función:

int LevenshteinDistance(char str1[1..lenStr1], char str2[1..lenStr2])
   // d is a table with lenStr1+1 rows and lenStr2+1 columns
   declare int d[0..lenStr1, 0..lenStr2]
   // i and j are used to iterate over str1 and str2
   declare int i, j, cost
 
   for i from 0 to lenStr1
       d[i, 0] := i
   for j from 0 to lenStr2
       d[0, j] := j
 
   for i from 1 to lenStr1
       for j from 1 to lenStr2
           if str1[i] = str2[j] then cost := 0
                                else cost := 1
           d[i, j] := minimum(
                                d[i-1, j] + 1,     // deletion
                                d[i, j-1] + 1,     // insertion
                                d[i-1, j-1] + cost   // substitution
                            )
 
   return d[lenStr1, lenStr2]

Incluso viene la implementación en algunos lenguajes de programación (C++, Java, Perl, Python, Ruby, Delphi y ColdFusion). Lástima que no viniera también en VisualBasic.NET que es el lenguaje en el que yo desarrollo… pero bueno, como codificar un algoritmo que ya nos viene dado en pseudocódigo tampoco es tarea infernal, a ello me he dedicado:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20081212
' Description: Calculates Levenshtein distance between two different 
'              strings.
'--------------------------------------------------------------------
Public Function LevenshteinDistance(ByVal STR1 As String, _
ByVal STR2 As String) As Integer
    'Variables to iterate (i, j) and get cost for any needed 
    'operation.
    Dim i As Integer
    Dim j As Integer
    Dim Cost As Integer

    'Creating array with string's lengths as bounds.
    Dim D(STR1.Length, STR2.Length) As Integer

    'Initializing array's values.
    For i = 0 To STR1.Length
        D(i, 0) = i
    Next
    For j = 0 To STR2.Length
        D(0, j) = j
    Next

    'Calculating Levenshtein distance.
    For i = 1 To STR1.Length
        For j = 1 To STR2.Length
            If STR1(i - 1) = STR2(j - 1) Then
                Cost = 0
            Else
                Cost = 1
            End If
            'First compared element: deletion.
            'Second compared element: insertion.
            'Third compared element: substitution.
            D(i, j) = Math.Min(Math.Min(D(i - 1, j) + 1, _
                                        D(i, j - 1) + 1), _
                                        D(i - 1, j - 1) + Cost)
        Next
    Next

    'Returning result.
    Return D(STR1.Length, STR2.Length)
End Function

Creo que está bien. Y el resultado de algunas pruebas así parece demostrarlo:

Debug.Print(Me.LevenshteinDistance("Albert Mata", "Albert Mata"))
0
Debug.Print(Me.LevenshteinDistance("Albert Mata", "Alber Matas"))
2
Debug.Print(Me.LevenshteinDistance("Albert Mata", "Mr. Albert Mata"))
4

No es ni mucho menos un sistema infalible, pero nos puede resultar muy útil para estimar si dos cadenas algo distintas pueden ser (o pretender ser) en realidad la misma. No obstante hay que utilizarla con cabeza, ya que por ejemplo si comparamos…

Albert Mata
Antoni Pons

…obtendremos una distancia de 9. Y si comparamos…

Berberechos y enlatados del norte, S.A.
Berberechos y enlatd. norte, SA

…obtendremos también una distancia de 9. Sin embargo en el primer caso las personas son claramente distintas y en el segundo caso el cliente parece ser el mismo. Para corregir esto se podrían buscar varias alternativas, pero yo propongo algo tan sencillo como ponderar la distancia entre la longitud de alguna de las cadenas:

Dim S1 As String = "Albert Mata"
Dim S2 As String = "Antoni Pons"
Dim LD As Integer = Me.LevenshteinDistance(S1, S2)
Dim Similar As Double = (S1.Length - LD) / S1.Length

Esto nos da un coeficiente de similitud del 0,181818181818182 (escaso, escaso). En cambio esto otro…

Dim S1 As String = "Berberechos y enlatados del norte, S.A."
Dim S2 As String = "Berberechos y enlatd. norte, SA"
Dim LD As Integer = Me.LevenshteinDistance(S1, S2)
Dim Similar As Double = (S1.Length - LD) / S1.Length

…nos da un coeficiente de similitud del 0,769230769230769 (¡mucho mejor!). Se tratará en cada caso de establecer dónde ponemos el límite.

ReDim Preserve para cambiar más de una dimensión en .NET.

Recientemente en grupos de .NET salió el tema de cómo se pueden redimensionar las dos dimensiones de una matriz de dos dimensiones sin perder los valores que ya se tienen almacenados en dicha matriz.

Si fuera una matriz de una dimensión no habría ningún problema, ya que la opción Preserve nos permite hacer precisamente eso:

Dim myArray(3) As Int32
myArray(0) = 2
myArray(1) = 4
myArray(2) = 6
myArray(3) = 8
ReDim Preserve myArray(5)
myArray(4) = 10
myArray(5) = 12

Así, este código no da ningún problema. Y tampoco lo da este otro:

Dim myArray(3, 0) As Int32
myArray(0, 0) = 2
myArray(1, 0) = 4
myArray(2, 0) = 6
myArray(3, 0) = 8
ReDim Preserve myArray(3, 1)
myArray(0, 1) = 10
myArray(1, 1) = 12
myArray(2, 1) = 14
myArray(3, 1) = 1

Ya que aunque es una matriz de dos dimensiones, sólo estamos redimensionando la dimensión situada más a la derecha. En cambio si intentamos hacer esto que sigue:

Dim myArray(3, 0) As Int32
myArray(0, 0) = 2
ReDim Preserve myArray(4, 1)
myArray(4, 1) = 10

Nos dará una excepción de tipo ArrayTypeMismatchException y nos dirá que…

‘ReDim’ sólo puede cambiar la dimensión situada más a la derecha

…porque un ReDim Preserve en una matriz de dos dimensiones sólo puede actuar sobre la última dimensión.

Para solventar esto podemos utilizar la siguiente función:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20081118
' Description: Simulates a ReDim Preserve action on 2-dimensions
'              arrays, allowing to change not only the last dimension
'              but both.
'--------------------------------------------------------------------
Public Function ReDimPreserve(ByVal M As Array, _
ByVal NewLimit0 As Integer, ByVal NewLimit1 As Integer) As Array
    If NewLimit0 >= M.GetUpperBound(0) _
    And NewLimit1 >= M.GetUpperBound(1) Then
        Dim NewArray(NewLimit0, NewLimit1) As [Int32]
        For i As Integer = 0 To M.GetUpperBound(0)
            For j As Integer = 0 To M.GetUpperBound(1)
                NewArray.SetValue(M.GetValue(i, j), i, j)
            Next
        Next
        Return NewArray
    Else
        Return M
    End If
End Function

De tal manera que ahora ante el siguiente código, donde creamos una matriz inicialmente de dimensiones (2,3) para luego redimensionarla a (4,5) y por tanto cambiando las dos dimensiones de la matriz, sin perder los valores que ya teníamos almacenados…

Debug.Print("Creating array with dimensions [2,3]")
Dim myArray(2, 3) As [Int32]
Debug.Print("Storing value 12 in position [0,2]")
myArray.SetValue(12, 0, 2)
Debug.Print("Storing value 15 in position [2,3]")
myArray.SetValue(15, 2, 3)
Debug.Print("Upper bound for first dimension = " _
            & (myArray.GetUpperBound(0)))
Debug.Print("Upper bound for second dimension = " _
            & (myArray.GetUpperBound(1)))
Debug.Print("Value in position [0,2] = " _
            & myArray.GetValue(0, 2).ToString)
Debug.Print("Value in position [2,3] = " _
            & myArray.GetValue(2, 3).ToString)
Try
    myArray.SetValue(24, 3, 4)
    Debug.Print("I can store values in position [3,4]")
Catch ex As Exception
    Debug.Print("I can't store values in position [3,4]")
End Try

Debug.Print("Changing array dimensions to [4,5]")
myArray = DirectCast(Me.ReDimPreserve(myArray, 4, 5), Integer(,))
Debug.Print("Upper bound for first dimension = " _
            & (myArray.GetUpperBound(0)))
Debug.Print("Upper bound for second dimension = " _
            & (myArray.GetUpperBound(1)))
Debug.Print("Value in position [0,2] = " _
            & myArray.GetValue(0, 2).ToString)
Debug.Print("Value in position [2,3] = " _
            & myArray.GetValue(2, 3).ToString)
Try
    myArray.SetValue(24, 3, 4)
    Debug.Print("I can store values in position [3,4]")
Catch ex As Exception
    Debug.Print("I can't store values in position [3,4]")
End Try

…obtenemos esta salida en la Ventana Inmediato:

Creating array with dimensions [2,3]
Storing value 12 in position [0,2]
Storing value 15 in position [2,3]
Upper bound for first dimension = 2
Upper bound for second dimension = 3
Value in position [0,2] = 12
Value in position [2,3] = 15
*** 'System.IndexOutOfRangeException' EXCEPTION ***
I can't store values in position [3,4]
Changing array dimensions to [4,5]
Upper bound for first dimension = 4
Upper bound for second dimension = 5
Value in position [0,2] = 12
Value in position [2,3] = 15
I can store values in position [3,4]

La función propuesta no es ni mucho menos perfecta. Tendría que hacerse más genérica, permitir redimensionar no dos sino N dimensiones, controlar mejor posibles errores y demás. Pero puede ser una primera aproximación para resolver situaciones de este tipo…

PD. También es posible utilizar el método Array.Copy en lugar de iterar por las dos dimensiones de la matriz, aunque en este caso lo he hecho así para que resulte más evidente el proceso.




Creative Commons License
El blog de Albert Mata by Albert Mata is licensed under a Creative Commons Reconocimiento-Compartir bajo la misma licencia 2.5 España License.