1. 编辑距离问题概述
编辑距离(Edit Distance)是计算机科学中一个经典的问题,用来衡量两个字符串之间的相似程度。具体来说,它表示将一个字符串转换成另一个字符串所需的最少单字符编辑操作次数。这些操作通常包括:
- 插入一个字符
- 删除一个字符
- 替换一个字符
这个问题由俄罗斯科学家Vladimir Levenshtein在1965年提出,因此也被称为Levenshtein距离。在实际应用中,编辑距离被广泛用于拼写检查、DNA序列比对、自然语言处理等领域。
注意:在力扣(LeetCode)平台上,编辑距离问题被标记为"困难"难度,编号为72题(而非题目中提到的100或95)。这是一个常见的笔误,实际题目编号应为72。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动态规划解法思路解析
2.1 为什么选择动态规划
编辑距离问题具有两个典型特征,使其非常适合用动态规划解决:
- 最优子结构:大问题的最优解可以由小问题的最优解推导出来
- 重叠子问题:在递归求解过程中会反复计算相同的子问题
动态规划通过存储子问题的解来避免重复计算,可以显著提高效率。对于两个长度分别为m和n的字符串,暴力解法的时间复杂度是指数级的,而动态规划可以将其降低到O(mn)。
2.2 状态定义与转移方程
我们定义dp[i][j]表示word1的前i个字符和word2的前j个字符之间的编辑距离。状态转移方程需要考虑三种操作:
-
如果word1[i-1] == word2[j-1](注意字符串索引从0开始):
dp[i][j] = dp[i-1][j-1] // 不需要操作 -
如果字符不相等,考虑三种操作的最小值:
dp[i][j] = min(
dp[i-1][j] + 1, // 删除word1[i-1]
dp[i][j-1] + 1, // 插入word2[j-1]到word1
dp[i-1][j-1] + 1 // 替换word1[i-1]为word2[j-1]
)
2.3 边界条件处理
边界条件是动态规划中容易出错的部分:
- dp[0][j] = j:将空字符串转换为word2的前j个字符需要j次插入
- dp[i][0] = i:将word1的前i个字符转换为空字符串需要i次删除
3. 完整代码实现与逐行解析
3.1 Python实现
python复制def minDistance(word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 初始化边界条件
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
# 填充dp表
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(
dp[i-1][j] + 1, # 删除
dp[i][j-1] + 1, # 插入
dp[i-1][j-1] + 1 # 替换
)
return dp[m][n]
3.2 代码关键点解析
- 二维数组初始化:创建(m+1)×(n+1)的dp表,+1是为了包含空字符串的情况
- 边界条件填充:第一行和第一列分别表示与空字符串的转换
- 双重循环填充:外层循环遍历word1,内层循环遍历word2
- 字符比较:注意索引是i-1和j-1,因为dp表比实际字符串长度大1
- 最小值选择:三种操作中选择编辑距离最小的一个
提示:在实际面试中,可以先从递归解法开始解释,然后优化为记忆化搜索,最后推导出动态规划解法,展示完整的思考过程。
4. 算法优化与空间复杂度改进
4.1 空间优化思路
原始解法使用了O(mn)的空间,但实际上我们只需要前一行和当前行的数据。因此可以优化到O(n)空间:
python复制def minDistance(word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
prev = [0] * (n + 1)
curr = [0] * (n + 1)
# 初始化第一行
for j in range(n + 1):
prev[j] = j
for i in range(1, m + 1):
curr[0] = i # 初始化当前行的第一列
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
curr[j] = prev[j-1]
else:
curr[j] = 1 + min(
prev[j], # 删除
curr[j-1], # 插入
prev[j-1] # 替换
)
prev = curr.copy()
return prev[n]
4.2 进一步优化
可以只使用一个一维数组,通过临时变量保存左上角的值:
python复制def minDistance(word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
dp = [0] * (n + 1)
# 初始化第一行
for j in range(n + 1):
dp[j] = j
for i in range(1, m + 1):
pre = dp[0] # 保存左上角的值
dp[0] = i # 更新当前行的第一个元素
for j in range(1, n + 1):
temp = dp[j] # 保存当前值,作为下一次的pre
if word1[i-1] == word2[j-1]:
dp[j] = pre
else:
dp[j] = 1 + min(
dp[j], # 上一行的值(删除)
dp[j-1], # 前一列的值(插入)
pre # 左上角的值(替换)
)
pre = temp
return dp[n]
5. 常见错误与调试技巧
5.1 典型错误案例
- 索引越界:忘记dp表比字符串长度大1,直接使用word1[i]而不是word1[i-1]
- 边界条件错误:没有正确初始化第一行和第一列
- 状态转移混淆:混淆了三种操作对应的dp表位置
- 空间优化时的更新顺序错误:在单数组优化中,没有正确保存和更新pre值
5.2 调试方法
-
打印dp表:对于小例子,打印完整的dp表检查中间结果
python复制for row in dp: print(row) -
使用简单测试用例:如("a", "b")、("", "a")、("ab", "ac")等
-
逐步验证:先验证边界条件,再验证中间状态转移
5.3 实际面试中的注意事项
- 先确认问题:明确操作的定义(是否允许替换,替换的代价等)
- 从简单例子入手:如("horse", "ros")的经典例子
- 解释清楚状态定义:确保面试官理解你的dp表含义
- 讨论优化空间:即使不要求,也可以主动提到空间优化
6. 扩展应用与变种问题
6.1 实际应用场景
- 拼写检查与纠正:计算输入单词与词典中单词的编辑距离,找出最接近的正确单词
- DNA序列比对:生物信息学中比较基因序列的相似性
- 抄袭检测:比较文本相似度
- 语音识别:匹配语音输入与候选文本
6.2 力扣上的相关题目
-
- 两个字符串的删除操作
-
- 两个字符串的最小ASCII删除和
-
- 不相交的线
-
- 最长公共子序列
6.3 变种问题
- 加权编辑距离:不同操作有不同的代价
- 只有插入和删除:不允许替换操作
- 近似字符串匹配:允许一定范围内的编辑距离
- 多字符串编辑距离:计算多个字符串之间的编辑距离
7. 个人解题心得
在实际解决编辑距离问题时,我发现以下几点特别重要:
- 画图辅助:绘制dp表格,手动填充几个单元格,有助于理解状态转移
- 从递归到动态规划:先思考递归关系,再转化为动态规划,思路更清晰
- 测试边界条件:空字符串、单字符字符串等特殊情况要单独测试
- 空间优化不是必须:在面试中,先给出基本解法,再讨论优化,不要一开始就追求最优空间复杂度
对于力扣刷题,建议按照题目分类集中练习动态规划问题,编辑距离是一个很好的起点。掌握这个问题后,很多其他字符串相关的动态规划问题都能触类旁通。
