一、编辑距离(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]