【ZH奶酪】如何用Python实现编辑距离?
1. 什么是编辑距离?
编辑距离(Edit Distance),又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。一般来说,编辑距离越小,两个串的相似度越大。
举个例子,给定 2 个字符串str_a=“yes”, str_b=“yeah”. 编辑距离是将 str_a 转换为 str_b 的最少操作次数,操作只允许如下 3 种:
- 插入一个字符,例如:ab
c
-> ab - 删除一个字符,例如:ab -> ab
c
- 替换一个字符,例如:ab
c
-> 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
来保存由a
向b
的编辑距离,其中D[i][j]
表示字符串a[1]-a[i]
转换为b[1]-b[i]
的编辑距离.
2.1 递归算法
递归的思想需要可以将问题拆解,假设a[i]
和b[j]
分别是字符串a
和b
的最后一位,那么要把问题拆解,有三种选择:
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]
时,比如abc
和bbc
,那么D[i][j] = D[i-1][j-1]
, 即等于ab
和bb
的编辑距离; - 当
a[i]
不等于b[j]
时,D[i][j]
等于如下3项的最小值:D[i-1][j] + 1
,即删除a[i]
, 比如abcd -> abc的编辑距离 = abc -> abc 的编辑距离 + 1
D[i][j-1] + 1
,即插入b[j]
, 比如ab -> abc 的编辑距离 = abc -> abc 的编辑距离 + 1
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
【ZH奶酪】如何用Python实现编辑距离?的更多相关文章
- 【ZH奶酪】为什么Python不需要函数重载?
函数重载的作用是什么? 函数重载主要是为了解决两个问题 可变参数类型 可变参数个数 另外,一个基本的设计原则是,仅仅当两个函数除了参数类型和参数个数不同以外,其功能是完全相同的,此时才使用函数重载,如 ...
- ZH奶酪:【Python】random模块
Python中的random模块用于随机数生成,对几个random模块中的函数进行简单介绍.如下:random.random() 用于生成一个0到1的随机浮点数.如: import random ra ...
- 如何用python“优雅的”调用有道翻译?
前言 其实在以前就盯上有道翻译了的,但是由于时间问题一直没有研究(我的骚操作还在后面,记得关注),本文主要讲解如何用python调用有道翻译,讲解这个爬虫与有道翻译的js“斗争”的过程! 当然,本文仅 ...
- 如何用python抓取js生成的数据 - SegmentFault
如何用python抓取js生成的数据 - SegmentFault 如何用python抓取js生成的数据 1赞 踩 收藏 想写一个爬虫,但是需要抓去的的数据是js生成的,在源代码里看不到,要怎么才能抓 ...
- 如何用python下载一张图片
如何用python下载一张图片 这里要用到的主要工具是requests这个工具,需要先安装这个库才能使用,该库衍生自urllib这个库,但是要比它更好用.多数人在做爬虫的时候选择它,是个不错的选择. ...
- ZH奶酪:Ubuntu 14.04配置LAMP(Linux、Apache、MySQL、PHP)
ZH奶酪:Ubuntu 14.04安装LAMP(Linux,Apache,MySQL,PHP) 之前已经介绍过LAMP的安装,这边文章主要讲解一下LAMP的配置. 1.配置Apache (1)调整Ke ...
- [置顶]
如何用PYTHON代码写出音乐
如何用PYTHON代码写出音乐 什么是MIDI 博主本人虽然五音不全,而且唱歌还很难听,但是还是非常喜欢听歌的.我一直在做这样的尝试,就是通过人工智能算法实现机器自动的作词和编曲(在这里预告下,通过深 ...
- 以下三种下载方式有什么不同?如何用python模拟下载器下载?
问题始于一个链接https://i1.pixiv.net/img-zip-...这个链接在浏览器打开,会直接下载一个不完整的zip文件 但是,使用下载器下载却是完整文件 而当我尝试使用python下载 ...
- 小姐姐带你一起学:如何用Python实现7种机器学习算法(附代码)
小姐姐带你一起学:如何用Python实现7种机器学习算法(附代码) Python 被称为是最接近 AI 的语言.最近一位名叫Anna-Lena Popkes的小姐姐在GitHub上分享了自己如何使用P ...
随机推荐
- django----过滤器和自定义标签
模板语法之过滤器 1.default:如果一个变量是false或者为空,使用给定的默认值.否则,使用变量的值.例如: <p>default过滤器:{{ li|default:"如 ...
- 区间dp好题cf149d 括号匹配
见题解链接https://blog.csdn.net/sdjzping/article/details/19160013 #include<bits/stdc++.h> using nam ...
- python3笔记(二)Python语言基础
缩进 要求严格的代码缩进是python语法的一大特色,就像C语言家族(C.C++.Java等等)中的花括号一样重要,在大多数场合还非常有必要.在很多代码规范里面也都有要求代码书写按照一定的规则进行换行 ...
- Sony笔记本
关机的情况下按键盘 f2键.进菜单选更改 bios设置 修改 3个地方 进bios右移 boot上 第一项 ufei改成 legacy external device改成enabled 下面启动顺序改 ...
- 基于Linux平台的自动化运维Devops-----之自动化系统部署
一.自动化运维的背景网站业务上线,需要运维人员在短时间内完成几百台服务器部署,包括系统安装.系统初始化.软件的安装与配置.性能的监控......所谓运维自动化,即在最少的人工干预下,利用脚本与第三方工 ...
- jquery.Inputmask 插件用法(中文API文档)
jquery.Inputmask 可以算是input文本输入限制的神器了,内部融合了多种输入限制, 如金额,电话号码,身份证号,网关等..,并且还可以自定义规则. inputmask 据说最早起源 ...
- 里氏代换原则(Liskov Substitution Principle,LSP)
第一种定义: 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换为o2,程序P的行为没有发生变化,那么类型S是类型T的子类型. 第二种定义: 所有引 ...
- hdu 1575 求一个矩阵的k次幂 再求迹 (矩阵快速幂模板题)
Problem DescriptionA为一个方阵,则Tr A表示A的迹(就是主对角线上各项的和),现要求Tr(A^k)%9973. Input数据的第一行是一个T,表示有T组数据.每组数据的第一行有 ...
- 【BZOJ4773】负环 [SPFA][二分]
负环 Time Limit: 100 Sec Memory Limit: 256 MB[Submit][Status][Discuss] Description 在忘记考虑负环之后,黎瑟的算法又出错 ...
- 【Java】 剑指offer(66) 构建乘积数组
本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集 题目 给定一个数组A[0, 1, …, n-1],请构建一个数组B[ ...