几种距离的基础研究上——编辑距离

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

图2

步骤详解:

几点说明:

(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]

参考文献(排名不分先后)

  1. "使用最小编辑距离算法求字符串相似度"
  2. "最小编辑距离_Python"
  3. "字符串相似性算法【最长公共字符串算法】 【LCS】"

推荐阅读更多精彩内容

  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    张土汪阅读 8,624评论 0 30
  • 分治方法 将问题划分成互不相交的子问题 递归地求解子问题 将子问题的解组合起来 动态规划(两个要素:最优子结构、子...
    superlj666阅读 95评论 0 0
  • 动态规划(Dynamic Programming) 本文包括: 动态规划定义 状态转移方程 动态规划算法步骤 最长...
    廖少少阅读 1,104评论 0 19
  • thiele插值算法 1点插值算法 function [C,c]=thiele(X,Y,Z)%X为插值点横坐标,Y...
    00crazy00阅读 669评论 0 4
  • 无意间看到了有人问编辑距离算法,当时对这个概念很陌生,也就去学习了下,做下总结,记录下,好记性不如烂笔头。 编辑距...
    辣条阅读 2,813评论 0 0