news 2026/5/2 0:38:23

【笔面试算法学习专栏】编辑距离:72.编辑距离

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【笔面试算法学习专栏】编辑距离:72.编辑距离

摘要

编辑距离是衡量两个字符串相似度的经典指标,也是动态规划领域的核心问题之一。本文深入解析LeetCode第72题,详细推导状态转移方程,从递归暴力解法逐步优化到自顶向下的记忆化搜索和自底向上的动态规划。通过图解和代码对比,帮助读者彻底理解这一经典问题的本质,并延伸探讨编辑距离在拼写检查、DNA序列比对等领域的实际应用。


一、问题定义与背景

1.1 问题描述

给你两个单词word1word2,请计算将word1转换成word2所需的最少操作数。

你可以对一个单词进行以下三种操作:

  1. 插入(Insert):插入一个字符
  2. 删除(Delete):删除一个字符
  3. 替换(Replace):将一个字符替换成另一个字符

示例 1:

输入:word1 = "horse", word2 = "ros" 输出:3 解释: horse → rorse (将 'h' 替换为 'r') rorse → rose (删除 'r') rose → ros (删除 'e')

示例 2:

输入:word1 = "intention", word2 = "execution" 输出:5 解释: intention → inention (删除 't') inention → exention (将 'i' 替换为 'e') exention → exection (将 'n' 替换为 'c') exection → execution (插入 'u') execution → execution (将 'i' 替换为 'e')

1.2 问题的本质

编辑距离问题的本质是字符串对齐操作成本优化。我们需要找到一条从word1word2的最优"转换路径",使得总操作次数最少。

1.3 应用场景

  • 拼写纠错:计算用户输入与正确单词的编辑距离
  • DNA序列比对:衡量两条DNA链的相似度
  • 论文查重:文本相似度检测
  • 机器翻译:评估翻译质量

二、问题分析

2.1 问题分解

word1[0...i]转换为word2[0...j]的问题可以分解为:

情况分析(考虑最后一个字符):

  1. word1[i] == word2[j]

    • 问题退化为:word1[0...i-1]word2[0...j-1]
    • 无需额外操作
  2. word1[i] != word2[j]

    • 替换操作:将word1[i]替换为word2[j]
      • 问题转化为:word1[0...i-1]word2[0...j-1]
    • 插入操作:在word1[0...i]末尾插入word2[j]
      • 问题转化为:word1[0...i]word2[0...j-1]
    • 删除操作:删除word1[i]
      • 问题转化为:word1[0...i-1]word2[0...j]

2.2 状态定义

定义dp[i][j]为:word1[0...i-1](前 i 个字符)转换为word2[0...j-1](前 j 个字符)所需的最少操作数

注意:这里使用ij表示前缀长度,便于处理空串情况。


三、动态规划解法

3.1 状态转移方程

根据问题分解,可以得到状态转移方程:

dp[i][j]={dp[i−1][j−1]if word1[i−1]==word2[j−1]min⁡(dp[i−1][j]+1,if word1[i−1]≠word2[j−1]dp[i][j−1]+1,dp[i−1][j−1]+1)dp[i][j] = \begin{cases} dp[i-1][j-1] & \text{if } word1[i-1] == word2[j-1] \\ \min(dp[i-1][j] + 1, & \text{if } word1[i-1] \neq word2[j-1] \\ \quad dp[i][j-1] + 1, & \\ \quad dp[i-1][j-1] + 1) & \end{cases}dp[i][j]=dp[i1][j1]min(dp[i1][j]+1,dp[i][j1]+1,dp[i1][j1]+1)ifword1[i1]==word2[j1]ifword1[i1]=word2[j1]

其中:

  • dp[i-1][j] + 1:删除word1[i-1],然后将word1[0...i-2]转换为word2[0...j-1]
  • dp[i][j-1] + 1:在word1[0...i-1]后插入word2[j-1],然后将word1[0...i-1]转换为word2[0...j-2]
  • dp[i-1][j-1] + 1:将word1[i-1]替换为word2[j-1]

3.2 初始化

  • dp[0][j] = j:将空串转换为word2[0...j-1],需要 j 次插入操作
  • dp[i][0] = i:将word1[0...i-1]转换为空串,需要 i 次删除操作
  • dp[0][0] = 0:空串到空串,0 次操作

3.3 完整代码实现

defminDistance(word1:str,word2:str)->int:""" 动态规划解决编辑距离问题 时间复杂度:O(mn),其中 m, n 分别是两个字符串的长度 空间复杂度:O(mn) """m,n=len(word1),len(word2)# 创建 dp 表,(m+1) x (n+1)dp=[[0]*(n+1)for_inrange(m+1)]# 初始化第一行:将空串转换为 word2[0...j-1]forjinrange(n+1):dp[0][j]=j# 初始化第一列:将 word1[0...i-1] 转换为空串foriinrange(m+1):dp[i][0]=i# 填充 dp 表foriinrange(1,m+1):forjinrange(1,n+1):ifword1[i-1]==word2[j-1]:# 最后一个字符相同,无需操作dp[i][j]=dp[i-1][j-1]else:# 三种操作取最小值 + 1dp[i][j]=min(dp[i-1][j]+1,# 删除dp[i][j-1]+1,# 插入dp[i-1][j-1]+1# 替换)returndp[m][n]

3.4 算法图解

word1 = "horse",word2 = "ros"为例:

"" r o s ┌─────┬─────┬─────┬─────┐ "" │ 0 │ 1 │ 2 │ 3 │ ← dp[0][j] = j ├─────┼─────┼─────┼─────┤ h │ 1 │ 1 │ 2 │ 3 │ ← dp[1][0] = i ├─────┼─────┼─────┼─────┤ ho│ 2 │ 2 │ 1 │ 2 │ ├─────┼─────┼─────┼─────┤ hor│ 3 │ 3 │ 2 │ 2 │ ├─────┼─────┼─────┼─────┤ hors│ 4 │ 4 │ 3 │ 2 │ ├─────┼─────┼─────┼─────┤ horse│ 5 │ 5 │ 4 │ 3 │ ← 最终答案 └─────┴─────┴─────┴─────┘

填充过程(以dp[5][3]为例):

  • word1[4] = 'e',word2[2] = 's'(不匹配)
  • dp[4][3] + 1 = 3 + 1 = 4(删除 ‘e’)
  • dp[5][2] + 1 = 4 + 1 = 5(插入 ‘s’)
  • dp[4][2] + 1 = 3 + 1 = 4(将 ‘e’ 替换为 ‘s’)
  • min(4, 5, 4) = 3

四、空间优化

4.1 滚动数组优化

观察 DP 表可知,dp[i][j]只依赖于dp[i-1][j-1]dp[i-1][j]dp[i][j-1],即只与上一行和当前行的前一个元素有关。

因此可以将空间优化到O(n)O(n)O(n)

defminDistance_optimized(word1:str,word2:str)->int:""" 空间优化的动态规划 时间复杂度:O(mn) 空间复杂度:O(n) """m,n=len(word1),len(word2)# 只保留上一行和当前行prev=list(range(n+1))# dp[0][*]foriinrange(1,m+1):curr=[i]+[0]*n# dp[i][0] = iforjinrange(1,n+1):ifword1[i-1]==word2[j-1]:curr[j]=prev[j-1]else:curr[j]=min(prev[j]+1,# 从上方来(删除)curr[j-1]+1,# 从左方来(插入)prev[j-1]+1# 从左上方来(替换))prev=currreturnprev[n]

4.2 优化原理图解

原始二维表: j→ ┌─────┬─────┬─────┐ i ↓ │ ← ← │ ← ← │ ← ← │ ├─────┼─────┼─────┤ │ ↖ ↖ │ ↖ ↖ │ ↖ ↖ │ ├─────┼─────┼─────┤ │ ← ← │ ← ← │ ← ← │ └─────┴─────┴─────┘ 优化后(只保留两行): prev: dp[i-1][*] curr: dp[i][*]

五、递归 + 记忆化

5.1 自顶向下的思路

我们也可以从递归的角度思考:定义minDist(i, j)为将word1[i...]转换为word2[j...]的最少操作数。

fromfunctoolsimportlru_cachedefminDistance_recursive(word1:str,word2:str)->int:""" 递归 + 记忆化搜索 时间复杂度:O(mn) 空间复杂度:O(mn) """m,n=len(word1),len(word2)@lru_cache(maxsize=None)defminDist(i:int,j:int)->int:# base case:空串处理ifi==m:returnn-j# 需要插入剩余的字符ifj==n:returnm-i# 需要删除剩余的字符ifword1[i]==word2[j]:returnminDist(i+1,j+1)else:return1+min(minDist(i+1,j),# 删除 word1[i]minDist(i,j+1),# 插入 word2[j]minDist(i+1,j+1)# 替换)returnminDist(0,0)

5.2 递归树与记忆化

minDist(0, 0) / | \ del(1,0) ins(0,1) rep(1,1) | | | minDist(1,0) minDist(0,1) minDist(1,1) ... ... ... 使用 @lru_cache 后,重复的子问题只计算一次

六、变种与扩展

6.1 打印编辑路径

不仅要知道最小操作数,还要知道具体的编辑操作:

defminDistanceWithPath(word1:str,word2:str)->tuple:"""返回最小编辑距离和编辑操作序列"""m,n=len(word1),len(word2)# 构建 DP 表dp=[[0]*(n+1)for_inrange(m+1)]foriinrange(m+1):dp[i][0]=iforjinrange(n+1):dp[0][j]=jforiinrange(1,m+1):forjinrange(1,n+1):ifword1[i-1]==word2[j-1]:dp[i][j]=dp[i-1][j-1]else:dp[i][j]=1+min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])# 回溯找出编辑路径operations=[]i,j=m,nwhilei>0orj>0:ifi>0andj>0andword1[i-1]==word2[j-1]:operations.append(f"保持: '{word1[i-1]}'")i-=1j-=1elifi>0anddp[i][j]==dp[i-1][j]+1:operations.append(f"删除: '{word1[i-1]}'")i-=1elifj>0anddp[i][j]==dp[i][j-1]+1:operations.append(f"插入: '{word2[j-1]}'")j-=1else:operations.append(f"替换: '{word1[i-1]}' → '{word2[j-1]}'")i-=1j-=1operations.reverse()returndp[m][n],operations# 示例distance,ops=minDistanceWithPath("horse","ros")print(f"编辑距离:{distance}")print("编辑操作:")foropinops:print(f"{op}")

6.2 编辑距离阈值

有时我们只需要知道编辑距离是否小于某个阈值:

defwithinEditDistance(word1:str,word2:str,threshold:int)->bool:""" 判断两个字符串的编辑距离是否在阈值内 如果编辑距离大于阈值,提前终止搜索 """m,n=len(word1),len(word2)# 剪枝:如果长度差大于阈值,直接返回 Falseifabs(m-n)>threshold:returnFalse# 优化的一维 DPprev=list(range(n+1))foriinrange(1,m+1):curr=[i]+[0]*n min_val=i# 当前行最小值的下界forjinrange(1,n+1):ifword1[i-1]==word2[j-1]:curr[j]=prev[j-1]else:curr[j]=1+min(prev[j],curr[j-1],prev[j-1])min_val=min(min_val,curr[j])# 剪枝:如果当前行最小值已超过阈值ifmin_val>threshold:returnFalseprev=currreturnprev[n]<=threshold

6.3 带权重的编辑距离

不同操作可以有不同的成本:

defweightedEditDistance(word1:str,word2:str,insert_cost:int=1,delete_cost:int=1,replace_cost:int=1)->int:""" 带权重的编辑距离 insert_cost: 插入成本 delete_cost: 删除成本 replace_cost: 替换成本(可设为0表示允许不匹配) """m,n=len(word1),len(word2)dp=[[0]*(n+1)for_inrange(m+1)]forjinrange(n+1):dp[0][j]=j*insert_costforiinrange(m+1):dp[i][0]=i*delete_costforiinrange(1,m+1):forjinrange(1,n+1):ifword1[i-1]==word2[j-1]:dp[i][j]=dp[i-1][j-1]else:dp[i][j]=min(dp[i-1][j]+delete_cost,dp[i][j-1]+insert_cost,dp[i-1][j-1]+replace_cost)returndp[m][n]

七、复杂度分析总结

解法时间复杂度空间复杂度特点
暴力递归O(3m+n)O(3^{m+n})O(3m+n)O(m+n)O(m+n)O(m+n)指数级,不可行
记忆化递归O(mn)O(mn)O(mn)O(mn)O(mn)O(mn)代码直观
二维 DPO(mn)O(mn)O(mn)O(mn)O(mn)O(mn)表格清晰
一维 DPO(mn)O(mn)O(mn)O(min⁡(m,n))O(\min(m,n))O(min(m,n))空间优化
阈值剪枝最好O(k)O(k)O(k)O(min⁡(m,n))O(\min(m,n))O(min(m,n))k 为阈值

八、面试总结

核心要点

  1. 状态定义dp[i][j]表示将word1[0...i-1]转换为word2[0...j-1]的最少操作数
  2. 状态转移:分字符相同/不同两种情况,字符不同时取删除/插入/替换的最小值
  3. 初始化:空串的处理(第一行和第一列)
  4. 空间优化:从二维表优化到一维数组

面试话术

“编辑距离是经典的二维动态规划问题。我定义dp[i][j]表示将word1的前i个字符转换为word2的前j个字符的最少操作数。当最后一个字符相同时,问题退化为子问题;当不同时,需要考虑删除、插入、替换三种操作,取最小值加一。时间复杂度O(mn)O(mn)O(mn),空间可以用滚动数组优化到O(n)O(n)O(n)。”

延伸问题

面试官可能追问:

  1. 如何打印具体的编辑操作?(回溯 DP 表)
  2. 如何优化到O(k)O(k)O(k)的提前终止?(阈值剪枝)
  3. 如何处理带权重的操作成本?(修改转移方程)

参考文献

  1. LeetCode. (2024). 72. Edit Distance. https://leetcode.com/problems/edit-distance/
  2. Levenshtein, V. I. (1966). Binary codes capable of correcting deletions, insertions, and reversals. Soviet Physics Doklady.
  3. Wagner, R. A., & Fischer, M. J. (1974). The string-to-string correction problem. Journal of the ACM.
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/12 5:20:23

CSS如何设置背景图片尺寸覆盖_使用background-size-cover

background-size: cover未填满容器主因是父容器宽高未设置&#xff0c;导致无参考系&#xff1b;cover等比缩放覆盖容器可能裁剪&#xff0c;contain则完整显示可能留白&#xff1b;移动端失效常因DPR或viewport缩放&#xff0c;需配合center定位、大图源及真机验证。backgroun…

作者头像 李华
网站建设 2026/5/1 22:28:46

国产安路FPGA以太网开发避坑指南:PH1A100SFG676的GMII时序约束详解

国产安路FPGA以太网开发实战&#xff1a;PH1A100SFG676的GMII时序约束与调试技巧 在高速以太网开发中&#xff0c;时序问题往往是工程师最头疼的"隐形杀手"。特别是当使用国产FPGA进行GMII接口设计时&#xff0c;PH1A100SFG676这颗芯片的PLL配置和时钟约束有着自己独…

作者头像 李华
网站建设 2026/4/12 13:54:21

雨滴谱数据深度解析——从原始变量到科学产品的Python实现【下篇】

二、计算原理&#xff08;物理概念解释&#xff09; 2.1 激光雨滴谱仪的工作原理 激光雨滴谱仪采用消光法测量雨滴。仪器发射一道水平方向的近红外激光束&#xff08;波长780 nm&#xff09;&#xff0c;当雨滴垂直穿过光束时&#xff0c;会遮挡部分光线。通过分析接收器上光强…

作者头像 李华
网站建设 2026/4/18 15:47:03

GitHub中文界面终极指南:3步轻松实现界面汉化,告别英文困扰

GitHub中文界面终极指南&#xff1a;3步轻松实现界面汉化&#xff0c;告别英文困扰 【免费下载链接】github-chinese GitHub 汉化插件&#xff0c;GitHub 中文化界面。 (GitHub Translation To Chinese) 项目地址: https://gitcode.com/gh_mirrors/gi/github-chinese 你…

作者头像 李华
网站建设 2026/4/12 2:14:19

【AI原生软件合规性红宝书】:20年监管实战总结的7大高危雷区与GDPR/《生成式AI服务管理暂行办法》双轨落地 checklist

第一章&#xff1a;AI原生软件合规性治理的范式跃迁 2026奇点智能技术大会(https://ml-summit.org) 传统软件合规治理以静态代码审查、人工策略映射和周期性审计为核心&#xff0c;其方法论在AI原生软件面前迅速失效——模型权重不可读、推理路径非确定、训练数据来源隐匿、提…

作者头像 李华
网站建设 2026/4/10 22:31:17

bypass-paywalls-chrome-clean:突破内容限制的智能浏览器扩展完全指南

bypass-paywalls-chrome-clean&#xff1a;突破内容限制的智能浏览器扩展完全指南 在信息爆炸的数字时代&#xff0c;优质内容常常被付费墙阻隔&#xff0c;而开源工具bypass-paywalls-chrome-clean为合法合规地获取信息提供了创新解决方案。这款轻量级浏览器扩展通过灵活的规则…

作者头像 李华