a.LD编辑距离算法

发布时间 2023-08-13 08:13:45作者: 昵称已经被使用

LD算法

参考文档:https://www.cnblogs.com/grenet/archive/2010/06/03/1750454.html

原理

LD算法(Levenshtein Distance)又成为编辑距离算法(Edit Distance)。它是以字符串A通过插入字符、删除字符、替换字符变成另一个字符串B,那么操作的过程的次数表示两个字符串的差异。

例如:字符串A:kitten如何变成字符串B:sitting。

​ 第一步:kitten——》sitten。k替换成s

 第二步:sitten——》sittin。e替换成i

 第三步:sittin——》sitting。在末尾插入g

故kitten和sitting的编辑距离为3

定义说明

LD(A,B)表示字符串A和字符串B的编辑距离。很显然,若LD(A,B)=0表示字符串A和字符串B完全相同

Rev(A)表示反转字符串A

Len(A)表示字符串A的长度

A+B表示连接字符串A和字符串B

  

性质

LD(A,A)=0

LD(A,"")=Len(A)

LD(A,B)=LD(B,A)

0≤LD(A,B)≤Max(Len(A),Len(B))

LD(A,B)=LD(Rev(A),Rev(B))

LD(A+C,B+C)=LD(A,B)

LD(A+B,A+C)=LD(B,C)

LD(A,B)≤LD(A,C)+LD(B,C)(注:像不像“三角形,两边之和大于第三边”)

LD(A+C,B)≤LD(A,B)+LD(B,C)

讲解定义

为了讲解计算LD(A,B),特给予以下几个定义

A=a1a2……aN,表示A是由a1a2……aN这N个字符组成,Len(A)=N

B=b1b2……bM,表示B是由b1b2……bM这M个字符组成,Len(B)=M

定义LD(i,j)=LD(a1a2……ai,b1b2……bj),其中0≤i≤N,0≤j≤M

故:  LD(N,M)=LD(A,B)

    LD(0,0)=0

    LD(0,j)=j

    LD(i,0)=i

对于1≤i≤N,1≤j≤M,有公式一

若ai=bj,则LD(i,j)=LD(i-1,j-1)

若ai≠bj,则LD(i,j)=Min(LD(i-1,j-1),LD(i-1,j),LD(i,j-1))+1

举例说明:A=GGATCGA,B=GAATTCAGTTA,计算LD(A,B)

第一步:初始化LD矩阵  

G A A T T C A G T T A
0 1 2 3 4 5 6 7 8 9 10 11
G 1
G 2
A 3
T 4
C 5
G 6
A 7

第二步:利用上述的公式一,计算第一行

G A A T T C A G T T A
0 1 2 3 4 5 6 7 8 9 10 11
G 1 0 1 2 3 4 5 6 7 8 9 10
G 2
A 3
T 4
C 5
G 6
A 7

第三步,利用上述的公示一,计算其余各行

G A A T T C A G T T A
0 1 2 3 4 5 6 7 8 9 10 11
G 1 0 1 2 3 4 5 6 7 8 9 10
G 2 1 1 2 3 4 5 6 6 7 8 9
A 3 2 1 1 2 3 4 5 6 7 8 8
T 4 3 2 2 1 2 3 4 5 6 7 8
C 5 4 3 3 2 2 2 3 4 5 6 7
G 6 5 4 4 3 3 3 3 3 4 5 6
A 7 6 5 4 4 4 4 3 4 4 5 5

则最右下角的值就是编辑距离:

LD(A,B)=LD(7,11)=5

我们往往不仅仅是计算出字符串A和字符串B的编辑距离,还要能得出他们的匹配结果。

以上面为例A=GGATCGA,B=GAATTCAGTTA,LD(A,B)=5

他们的匹配为:

A:GGA_TC_G__A

B:GAATTCAGTTA

如上面所示,蓝色表示完全匹配,黑色表示编辑操作,_表示插入字符或者是删除字符操作。如上面所示,黑色字符有5个,表示编辑距离为5。

利用上面的LD矩阵,通过回溯,能找到匹配字串

第一步:定位在矩阵的右下角  

G A A T T C A G T T A
0 1 2 3 4 5 6 7 8 9 10 11
G 1 0 1 2 3 4 5 6 7 8 9 10
G 2 1 1 2 3 4 5 6 6 7 8 9
A 3 2 1 1 2 3 4 5 6 7 8 8
T 4 3 2 2 1 2 3 4 5 6 7 8
C 5 4 3 3 2 2 2 3 4 5 6 7
G 6 5 4 4 3 3 3 3 3 4 5 6
A 7 6 5 4 4 4 4 3 4 4 5 5

第二步:回溯单元格,至矩阵的左上角

  若ai=bj,则回溯到左上角单元格

G A A T T C A G T T A
0 1 2 3 4 5 6 7 8 9 10 11
G 1 0 1 2 3 4 5 6 7 8 9 10
G 2 1 1 2 3 4 5 6 6 7 8 9
A 3 2 1 1 2 3 4 5 6 7 8 8
T 4 3 2 2 1 2 3 4 5 6 7 8
C 5 4 3 3 2 2 2 3 4 5 6 7
G 6 5 4 4 3 3 3 3 3 4 5 6
A 7 6 5 4 4 4 4 3 4 4 5 5

若ai≠bj,回溯到左上角、上边、左边中值最小的单元格,若有相同最小值的单元格,优先级按照左上角、上边、左边的顺序

G A A T T C A G T T A
0 1 2 3 4 5 6 7 8 9 10 11
G 1 0 1 2 3 4 5 6 7 8 9 10
G 2 1 1 2 3 4 5 6 6 7 8 9
A 3 2 1 1 2 3 4 5 6 7 8 8
T 4 3 2 2 1 2 3 4 5 6 7 8
C 5 4 3 3 2 2 2 3 4 5 6 7
G 6 5 4 4 3 3 3 3 3 4 5 6
A 7 6 5 4 4 4 4 3 4 4 5 5

若当前单元格是在矩阵的第一行,则回溯至左边的单元格

若当前单元格是在矩阵的第一列,则回溯至上边的单元格

G A A T T C A G T T A
0 1 2 3 4 5 6 7 8 9 10 11
G 1 0 1 2 3 4 5 6 7 8 9 10
G 2 1 1 2 3 4 5 6 6 7 8 9
A 3 2 1 1 2 3 4 5 6 7 8 8
T 4 3 2 2 1 2 3 4 5 6 7 8
C 5 4 3 3 2 2 2 3 4 5 6 7
G 6 5 4 4 3 3 3 3 3 4 5 6
A 7 6 5 4 4 4 4 3 4 4 5 5

依照上面的回溯法则,回溯到矩阵的左上角

第三步:根据回溯路径,写出匹配字串

  若回溯到左上角单元格,将ai添加到匹配字串A,将bj添加到匹配字串B

  若回溯到上边单元格,将ai添加到匹配字串A,将_添加到匹配字串B

  若回溯到左边单元格,将_添加到匹配字串A,将bj添加到匹配字串B

  搜索晚整个匹配路径,匹配字串也就完成了

从上面可以看出,LD算法在不需要计算出匹配字串的话,时间复杂度为O(MN),空间复杂度经优化后为O(M)

  不过,如果要计算匹配字符串的话,时间复杂度为O(MN),空间复杂度由于需要利用LD矩阵计算匹配路径,故空间复杂度仍然为O(MN)。这个在两个字符串都比较短小的情况下,能获得不错的性能。不过,如果字符串比较长的情况下,就需要极大的空间存放矩阵。

代码

参考文档:https://blog.csdn.net/koibiki/article/details/83031788?utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromMachineLearnPai2~default-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromMachineLearnPai2~default-1.control

import numpy as np

def edit_distance(word1, word2):
    len1 = len(word1)
    len2 = len(word2)
    # 新建一个全为0的矩阵
    dp = np.zeros((len1 + 1,len2 + 1))
    # 遍历列,将第一列全赋值为对应的列值
    for i in range(len1 + 1):
        dp[i][0] = i  
    # 遍历行,将第一行全赋值为对应的行值
    for j in range(len2 + 1):
        dp[0][j] = j
    # 对应的是原理中的图二 
    for i in range(1, len1 + 1):
        for j in range(1, len2 + 1):
            delta = 0 if word1[i-1] == word2[j-1] else 1
            dp[i][j] = min(dp[i - 1][j - 1] + delta, min(dp[i-1][j] + 1, dp[i][j - 1] + 1))
    return dp[len1][len2]
edit_distance("jarrry", "jerr")

优化后:

参考文档:https://blog.csdn.net/Ha_hha/article/details/78834374

# coding=utf-8
# 这里的实现过程对算法进行了稍微的优化(运算结果应该稍快一些):首先计算两个字符串开头以及结尾的共同字符子串,然后去掉相同部分,重新构造新字符串,减少迭代过程次数。
# 如:string_a = "GGATCGA" string_b = "GAATTCAGTTA"构造的新字符串为:
# new_a = "GATCG" new_b = "AATTCAGTT" 

def LD(string_a, string_b):
    '''
    定义函数实现LD算法
    '''
    # 初始化不同字符串开头的位置
    diff_start = -1
    # 初始化不同字符串结束的位置
    diff_end = -1
    # a字段的长度
    len_a = len(string_a)
    # b字段地长度
    len_b = len(string_b)
    # 找出a,b中的最小长度
    short = min(len_a, len_b)

    # 寻找开头的共同字符串,并记录位置
    for i in range(0, short):
        if string_a[i] != string_b[i]:
            diff_start = i
            break
    # 如果diff_start为-1,意味着有一个字符串为空,直接返回另一个字符串的长度
    if diff_start == -1:
        return abs(len_b - len_a)
    # 寻找结尾的共同字符串,并记录位置
    for i in range(0, short):
        if string_a[len_a - i -1] != string_b[len_b - i - 1]:
            diff_end = i
            break
    # # 如果diff_end为-1,意味着有一个字符串为空,直接返回另一个字符串的长度
    if diff_end == -1:
        return abs(len_b - len_a)

    # 因性质L(A+C, B+C) = LD(A, B)
    # 故除去开头以及结尾相同的字符串,构建新的字符串
#     long_str = str(string_a[diff_start: len_a - diff_end] if len_a >= len_b else string_b[diff_start: len_b - diff_end])
    long_str = string_a[diff_start: len_a - diff_end] if len_a >= len_b else string_b[diff_start: len_b - diff_end]
    short_str = string_a[diff_start: len_a - diff_end] if len_a < len_b else string_b[diff_start: len_b - diff_end]
    # print(long_str)
    # print(short_str)

    # 获取新字符串的长度
    long_len = len(long_str)
    short_len = len(short_str)

    # store保存迭代过程中每次计算的结果
    store = range(0, long_len + 1)

    # 对应的是原理中的每一行推导
    for i in range(short_len):
        temp = [i+1] * (long_len + 1)
        # 对应的是每个单元格的值
        for j in range(long_len):
            # 对应的是原理中当值相等的条件
            if long_str[j] == short_str[i]:
                temp[j+1] = store[j]
            # 对应的是原理中当值不相等的条件
            else:
                # 注意这时各个位置数据的对应关系
                temp[j+1] = min(store[j], store[j+1], temp[j]) + 1
        # 将每一行对应的结果赋值给store
        store = temp
        # print(store)
    # 返回最后的编辑距离
    return store[-1]


# Test
string_a = "GGATCGA"
string_b = "GAATTCAGTTA"

ld = LD(string_a, string_b)
print(ld)