一、編輯距離(Levenshtein距離)
簡介
這個距離是1965年一個戰斗名族叫Levenshtein的一個發明的一個算法。
這個所謂的距離,實際是指兩個字串之間,由一個轉成另一個所需的最少編輯操作次數。這些編輯操作包括將一個字符替換成另一個字符,插入一個字符,刪除一個字符。
維基百科
基本原理
算法就是簡單的線性動態規劃(最長上升子序列就屬于線性動態規劃)。
設我們要將s1變成s2
定義狀態矩陣edit[len1][len2],len1和len2分別是要比較的字符串s1和字符串s2的長度+1(+1是考慮到動歸中,一個串為空的情況)
然后,定義edit[i][j]是s1中前i個字符組成的串,和s2中前j個字符組成的串的編輯距離
具體思想是,對于每個i,j從0開始依次遞增,對于每一次j++,由于前j-1個字符跟i的編輯距離已經求出,所以只用考慮新加進來的第j個字符即可
插入操作:在s1的前i個字符后插入一個字符ch,使得ch等于新加入的s2[j]。于是插入字符ch的編輯距離就是edit[i][j-1]+1
刪除操作:刪除s1[i],以期望s1[i-1]能與s2[j]匹配(如果s1[i-1]前邊的幾個字符能與s2[j]前邊的幾個字符有較好的匹配,那么這么做就能得到更好的結果)。另外,對于s1[i-1]之前的字符跟s2[j]匹配的情況,edit[i-1][j]中已經考慮過。于是刪除字符ch的編輯距離就是edit[i-1][j]+1
替換操作:期望s1[i]與s2[j]匹配,或者將s1[i]替換成s2[j]后匹配。于是替換操作的編輯距離就是edit[i-1][j-1]+f(i,j)。其中,當s1[i]==s2[j]時,f(i,j)為0;反之為1
于是動態規劃公式如下:
if i == 0 且 j == 0,edit(i, j) = 0
if i == 0 且 j > 0,edit(i, j) = j
if i > 0 且j == 0,edit(i, j) = i
if 0 < i ≤ 1 且 0 < j ≤ 1 ,edit(i, j) == min{ edit(i-1, j) + 1, edit(i, j-1) + 1, edit(i-1, j-1) + f(i, j) },當第一個字符串的第i個字符不等于第二個字符串的第j個字符時,f(i, j) = 1;否則,f(i, j) = 0。
上面的描述并不形象,換個圖形化表示:
下圖為計算字符“beauty”與“batyu”編輯距離的二維圖解:
步驟詳解:
幾點說明:
(1) 坐標軸垂直向下為x軸正向,水平向右為y軸正向。因此紅圈1的坐標為(1,1),紅圈2的坐標為(1,2)。
(2) 這個矩陣中的點含義是兩個字符要經過多少編輯操作才能完成轉換。編輯操作就是上面的3類操作。
(3) 在更新矩陣中距離的時候,對應參照的向量中表示左側(相當于上面講到的刪除操作)和上測(相當于上面講到的插入操作)的值,不論它對應的字母是否相同,左側和上測向量值均加1。
推算步驟:
(1) 先建立一張二維表(矩陣),如上圖所示。矩陣中(0,0)位置不對應字母。
(2) 計算矩陣(1,1)位置(即:紅圈1所在的置)的值,此處為方便描述,將該位置定義為A點。
A點的值需要由A點左上方、左邊和上邊的值共同決定。為方便描述先將A點所需要的三個值賦給向量a,則a=(0,1,1)。A點對應的字母分別為(b,b),字母相同,則A點左上角的值加0(不同則加1),A點左邊與上邊的值分別加1。此時a=(0,2,2),取a中做小值填入A點位置,見右圖所示。
計算矩陣(1,2)位置(即:紅圈2所在的位置),定義為B點。B點對應向量為b=(1,0,2)。由于B點對應的字母為(b,e),字母不同,則B點左上角的值加1,同時,B點左側上側分別加1。此時b=(2,1,3),取b中最小值賦給B點。
(3) 按照步驟2)求出每個格子中的值。所有值求出后,右下角的值為最小編輯距離。
Python實現
這里給出了一個別人實現的代碼,同時,有人指出了它的缺陷,再此進行了修正。
#!/user/bin/env python
# -*- coding: utf-8 -*-
class Levenshtein(object):
def __init__(self):
pass
def get_distance(self,first,second):
"""編輯距離的求算過程。"""
# 如果字符串1的長度大于字符串2的長度,則交換兩個字符串。
if len(first) > len(second):
first,second = second,first
# 如果兩個字符串中有一個是空串,則另一個串可以通過字符串長度轉換為該串(刪除或者插入字符)。
if len(first) == 0:
return len(second)
if len(second) == 0:
return len(first)
first_length = len(first) + 1
second_length = len(second) + 1
# 初始化基礎字符串
distance_matrix = [[0 for y in range(second_length)] for x in range(first_length)]
# 初始化x軸初值
for x in range(first_length):
distance_matrix[x][0] = x
# 初始化y軸初值
for y in range(second_length):
distance_matrix[0][y] = y
# 開始計算距離
for i in range(1,first_length):
for j in range(1,second_length):
del_edtion = distance_matrix[i-1][j] + 1
ins_edtion = distance_matrix[i][j-1] + 1
sub_edtion = distance_matrix[i-1][j-1]
if first[i-1] != second[j-1]:
sub_edtion += 1
distance_matrix[i][j] = min(ins_edtion,del_edtion,sub_edtion)
return distance_matrix[first_length-1][second_length-1]
if __name__ == "__main__":
leven_obj = Levenshtein()
print leven_obj.get_distance('beauty','batyu')
結果矩陣
[0, 1, 2, 3, 4, 5, 6]
[1, 0, 1, 2, 3, 4, 5]
[2, 1, 1, 1, 2, 3, 4]
[3, 2, 2, 2, 2, 2, 3]
[4, 3, 3, 3, 3, 3, 2]
[5, 4, 4, 4, 3, 4, 3]