zoukankan      html  css  js  c++  java
  • 【ZH奶酪】如何用Python实现编辑距离?

    1. 什么是编辑距离?

    编辑距离(Edit Distance),又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。一般来说,编辑距离越小,两个串的相似度越大。

    举个例子,给定 2 个字符串str_a=“yes”, str_b=“yeah”. 编辑距离是将 str_a 转换为 str_b 的最少操作次数,操作只允许如下 3 种:

    • 插入一个字符,例如:abc -> ab
    • 删除一个字符,例如:ab -> abc
    • 替换一个字符,例如:abc -> abd

    那么从str_a到str_b的转换过程总共需要两步:yes > yeas > yeah 或者 yes > yea > yeah,所以str_a和str_b的编辑距离为2。

    2. 如何计算编辑距离?

    假设字符串a, 共m位,从a[1]a[m], 字符串b, 共m位, 从b[1]b[m]. 用二维数组D来保存由ab的编辑距离,其中D[i][j]表示字符串a[1]-a[i]转换为b[1]-b[i]的编辑距离.

    2.1 递归算法

    递归的思想需要可以将问题拆解,假设a[i]b[j]分别是字符串ab的最后一位,那么要把问题拆解,有三种选择:

    • a[i-1], b[j],即用a[1:i-1]继续和b[1:j]比较,删除了a[i],需要额外一步代价;
    • a[i-1], b[j-1],即用a[1:i-1]继续和b[1:j-1]比较,如果a[i]b[j]相等,那么无需额外代价,否则需要额外一步代价将a[i]修改为b[j]
    • a[i], b[j-1],即用a[1:i]继续和b[1:j-1]比较,删除了b[j],需要额外一步代价;

    换一种说法,也就是说具体要拆解为哪一种,需要考虑a[i]b[j]的比值,以及这三种方法的代价。即如下递归规律:

    • a[i]等于b[j]时,比如 abcbbc,那么D[i][j] = D[i-1][j-1], 即等于abbb的编辑距离;
    • a[i]不等于b[j]时,D[i][j]等于如下3项的最小值:
      1. D[i-1][j] + 1,即删除a[i], 比如abcd -> abc的编辑距离 = abc -> abc 的编辑距离 + 1
      2. D[i][j-1] + 1,即插入b[j], 比如ab -> abc 的编辑距离 = abc -> abc 的编辑距离 + 1
      3. D[i-1][j-1] + 1,将a[i]替换为b[j], 比如abd -> abc 的编辑距离 = abc -> abc 的编辑距离 + 1

    那么递归边界如何设定呢?

    递归边界就是a[1:i]或者b[1:j]'为空的时候,即:

    a[i][0] = i, b字符串为空,那么需要将a[1]-a[i]全部删除,所以编辑距离为i
    a[0][j] = j, a字符串为空,那么需要向a插入b[1]-b[j],所以编辑距离为j

    Python代码:

    def recursive_edit_distance(str_a, str_b):
      if len(str_a) == 0:
        return len(str_b)
      elif len(str_b) == 0:
        return len(str_a)
      elif str_a[len(str_a)-1] == str_b[len(str_b)-1]:
        return recursive_edit_distance(str_a[0:-1], str_b[0:-1])
      else:
        return min([
          recursive_edit_distance(str_a[:-1], str_b),
          recursive_edit_distance(str_a, str_b[:-1]),
          recursive_edit_distance(str_a[:-1], str_b[:-1])
        ]) + 1
    str_a = "yes"
    str_b = "yeah"
    print(recursive_edit_distance(str_a, str_b))
    # output is : 2
    

    算法分析:该算法逻辑清晰,可读性较高,但是对于计算机而言却很不友好,时间复杂度高,随字符串长度呈指数级增长,而且递归算法的通病就是调用栈太深的时候,需要占用较多计算机资源。

    2.2 动态规划

    如果熟悉动态规划的同学,从上边的思路可以很容易推理出动态规划的递推公式:

    if a[i] == b[j]:
        edit_distance(a[i], b[j]) = edit_distance(a[i-1], b[j-1]) 
    if a[i] != b[j]:
        edit_distance(a[i], b[j]) = MIN (
            edit_distance(a[i-1], b[j]) + 1,   # 从a中删除a[i]
            edit_distance(a[i], b[j-1]) + 1,  # 向a中插入b[j]
            edit_distance(a[i-1], b[j-1]) + 1  # 将a[i]修改为b[j]
        )
    

    转换为Python,也就是用二维数组D来记录从a向b的转换过程:

    def edit_distance(str_a, str_b):
      if str_a == str_b:
        return 0
      if len(str_a) == 0:
        return len(str_b)
      if len(str_b) == 0:
        return len(str_a)
    # 初始化dp矩阵
      dp = [[0 for _ in range(len(str_a) + 1)] for _ in range(len(str_b) + 1)]
    # 当a为空,距离和b的长度相同
      for i in range(len(str_b) + 1):
        dp[i][0] = i
    # 当b为空,距离和a和长度相同
      for j in range(len(str_a) + 1):
        dp[0][j] = j
    # 递归计算
      for i in range(1, len(str_b) + 1):
        for j in range(1, len(str_a) + 1):
          dp[i][j] = dp[i-1][j-1]
          if str_a[j-1] != str_b[i-1]:
            dp[i][j] = min([dp[i-1][j-1], dp[i-1][j], dp[i][j-1]]) + 1
      return dp[len(str_b)][len(str_a)]
    str_a = "yes"
    str_b = "yeah"
    print(edit_distance(str_a, str_b))
    # output is : 2
    

    2.3 动态规划, 优化空间复杂度

    上边的算法中用二维数组来存储从a到b的距离,从递推公式来看,其实每一步dp[i][j]的计算只依赖a[i]和b[j]是否相等以及矩阵中的三个值

    • 左边的值,left = dp[i-1][j]
    • 左上角的值,left_up = dp[i-1][j-1]
    • 上边的值,up = dp[i][j-1]

    其实我们可以用一维数组来达到上述目的,具体可以看Python代码:

    def edit_distance(str_a, str_b):
      if str_a == str_b:
        return 0
      if len(str_a) == 0:
        return len(str_b)
      if len(str_b) == 0:
        return len(str_a)
      dp = [x for x in range(len(str_b) + 1)]
      for i in range(1, len(str_a) + 1):
        # 注意每次left_up和dp[0]的初始化
        left_up = i - 1
        dp[0] = i # 当前轮最左的left
        for j in range(1, len(str_b) + 1):
          up= dp[j]  # j是上一轮的值,即up
          left = dp[j-1]  # j-1是当前轮的值,即left
          if str_a[i-1] == str_b[j-1]:
            dp[j] = left_up
          else:
            dp[j] = min([left, up, left_up]) + 1
          left_up = up # 每移动一步,上一轮的up就变成了left_up
      return dp[len(str_b)]
    str_a = "yes"
    str_b = "yeah"
    print(edit_distance(str_a, str_b))
    # output is : 2
    

    2.4 打印编辑过程

    def edit_distance_Omn(str_a, str_b):
      if str_a == str_b:
        return 0
      if len(str_a) == 0:
        return len(str_b)
      if len(str_b) == 0:
        return len(str_a)
      dp = [[0 for _ in range(len(str_a) + 1)] for _ in range(len(str_b) + 1)]
      for i in range(len(str_b) + 1):
        dp[i][0] = i
      for j in range(len(str_a) + 1):
        dp[0][j] = j
      for i in range(1, len(str_b) + 1):
        for j in range(1, len(str_a) + 1):
          dp[i][j] = dp[i-1][j-1]
          if str_a[j-1] != str_b[i-1]:
            dp[i][j] = min([dp[i-1][j-1], dp[i-1][j], dp[i][j-1]]) + 1
    
      #打印完整路径矩阵(这一步非必要)
      for i in range(len(str_b) + 1):
        for j in range(len(str_a) + 1):
          print dp[i][j],
        print
      # 准备倒着查询编辑路径,从右下角开始
      i , j = len(str_b), len(str_a)
      op_list = []  # 记录编辑操作
      while i > 0 and j > 0:
        if dp[i][j] == dp[i-1][j-1]:
          op_list.append("keep [ {} ]".format(str_b[i-1]))
          i, j = i-1, j-1
          continue
        if dp[i][j] == dp[i-1][j]  + 1:
          op_list.append("remove [ {} ]".format(str_b[i-1]))
          i, j = i-1, j
          continue
        if dp[i][j] == dp[i-1][j-1] + 1:
          op_list.append("change [ {} ] to [ {} ]".format(str_b[i-1], str_a[j-1]))
          i, j = i-1, j-1
          continue
        if dp[i][j] == dp[i][j-1] + 1:
          op_list.append("insert [ {} ]".format(str_a[j-1]))
          i, j = i, j-1
      for i in range(len(op_list)):
        print op_list[len(op_list)-i-1]
      return dp[len(str_b)][len(str_a)]
    str_a = "yesxxxxxx"
    str_b = "yeahxxxxxhh"
    print(edit_distance(str_a, str_b))
    

    输出

    0 1 2 3 4 5 6 7 8 9
    1 0 1 2 3 4 5 6 7 8
    2 1 0 1 2 3 4 5 6 7
    3 2 1 1 2 3 4 5 6 7
    4 3 2 2 2 3 4 5 6 7
    5 4 3 3 2 2 3 4 5 6
    6 5 4 4 3 2 2 3 4 5
    7 6 5 5 4 3 2 2 3 4
    8 7 6 6 5 4 3 2 2 3
    9 8 7 7 6 5 4 3 2 2
    10 9 8 8 7 6 5 4 3 3
    11 10 9 9 8 7 6 5 4 4
    keep [ y ]
    keep [ e ]
    change [ a ] to [ s ]
    change [ h ] to [ x ]
    keep [ x ]
    keep [ x ]
    keep [ x ]
    keep [ x ]
    keep [ x ]
    remove [ h ]
    remove [ h ]
    4
    
  • 相关阅读:
    java文件的读写程序代码
    C#多线程总结
    动态调用WebService接口的几种方式
    Net中Attribute特性的高级使用及自定义验证实现
    进程、线程、多线程
    C#设计模式之单例模式
    C# HttpClient 请求转发
    webapi Model Validation 模型验证
    加密解密方法
    手把手教Electron+vue的使用
  • 原文地址:https://www.cnblogs.com/CheeseZH/p/8821282.html
Copyright © 2011-2022 走看看