问题简介
题目描述
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例说明
✅ 示例 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 -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
解题思路
📌 核心思想:动态规划(Dynamic Programming)
编辑距离是经典的 DP 问题。我们定义状态、转移方程,并逐步构建答案。
✅ 方法一:二维动态规划(标准解法)
步骤详解:
-
定义状态
dp[i][j]表示将word1的前i个字符转换为word2的前j个字符所需的最小操作数。 -
初始化边界条件
dp[0][j] = j:空字符串变word2[:j]需要j次插入。dp[i][0] = i:word1[:i]变空字符串需要i次删除。
-
状态转移方程
对于
i > 0且j > 0:-
如果
word1[i-1] == word2[j-1],则无需操作:
dp[i][j] = dp[i-1][j-1] -
否则,取三种操作的最小值 + 1:
dp[i][j] = min( dp[i-1][j] + 1, // 删除 word1[i-1] dp[i][j-1] + 1, // 插入 word2[j-1] dp[i-1][j-1] + 1 // 替换 word1[i-1] 为 word2[j-1] )
-
-
返回结果
dp[m][n]即为答案(m = len(word1),n = len(word2))
✅ 方法二:空间优化的一维 DP
💡 观察 :dp[i][j] 仅依赖于上一行和当前行的左侧值,因此可用滚动数组优化空间。
- 使用一维数组
dp[j]表示当前行。 - 用变量
prev保存dp[i-1][j-1](左上角)。 - 遍历更新时注意顺序。
本题以清晰性优先,以下代码实现以方法一为主,但会在 Go 中展示空间优化版本作为对比。
代码实现
java:Java
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// dp[i][j]: word1前i个字符 -> word2前j个字符的最小操作数
int[][] dp = new int[m + 1][n + 1];
// 初始化边界
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
// 填充DP表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(
Math.min(dp[i - 1][j], dp[i][j - 1]),
dp[i - 1][j - 1]
) + 1;
}
}
}
return dp[m][n];
}
}
go:Go
// 方法一:二维DP(清晰版)
func minDistance(word1 string, word2 string) int {
m, n := len(word1), len(word2)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
dp[i][0] = i
}
for j := 0; j <= n; j++ {
dp[0][j] = j
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if word1[i-1] == word2[j-1] {
dp[i][j] = dp[i-1][j-1]
} else {
dp[i][j] = min(min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1
}
}
}
return dp[m][n]
}
// 方法二:空间优化(一维DP)
func minDistanceOptimized(word1 string, word2 string) int {
m, n := len(word1), len(word2)
if m < n {
return minDistanceOptimized(word2, word1) // 确保 word1 更长,节省空间
}
dp := make([]int, n+1)
for j := 0; j <= n; j++ {
dp[j] = j
}
for i := 1; i <= m; i++ {
prev := dp[0] // 保存 dp[i-1][j-1]
dp[0] = i
for j := 1; j <= n; j++ {
temp := dp[j]
if word1[i-1] == word2[j-1] {
dp[j] = prev
} else {
dp[j] = min(min(dp[j], dp[j-1]), prev) + 1
}
prev = temp
}
}
return dp[n]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
示例演示
以 word1 = "horse", word2 = "ros" 为例:
构建 DP 表如下(✅ 表示匹配,无需操作):
| "" | r | o | s | |
|---|---|---|---|---|
| "" | 0 | 1 | 2 | 3 |
| h | 1 | 1 | 2 | 3 |
| o | 2 | 2 | 1 | 2 |
| r | 3 | 2 | 2 | 2 |
| s | 4 | 3 | 3 | 2 |
| e | 5 | 4 | 4 | 3 |
最终 dp[5][3] = 3,符合预期。
答案有效性证明
✅ 数学归纳法验证 DP 正确性:
- 基础情况:空字符串到任意字符串的操作数显然为长度(全插入/删除),初始化正确。
- 归纳假设 :假设所有
dp[i'][j'](其中i' < i或j' < j)已正确计算。 - 归纳步骤 :
- 若末尾字符相同,则最优解必不操作该字符,故
dp[i][j] = dp[i-1][j-1]。 - 若不同,则最后一次操作必为插入、删除或替换之一,取三者最小值加 1,覆盖所有可能。
- 若末尾字符相同,则最优解必不操作该字符,故
- 因此,DP 状态转移覆盖所有情况,结果最优。
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 二维 DP | O ( m t i m e s n ) O(m \\times n) O(mtimesn) | O ( m t i m e s n ) O(m \\times n) O(mtimesn) |
| 一维 DP(优化) | O ( m t i m e s n ) O(m \\times n) O(mtimesn) | O ( m i n ( m , n ) ) O(\\min(m, n)) O(min(m,n)) |
其中 m = t e x t l e n ( w o r d 1 ) m = \\text{len}(word1) m=textlen(word1), n = t e x t l e n ( w o r d 2 ) n = \\text{len}(word2) n=textlen(word2)
问题总结
📌 关键点回顾:
- 编辑距离是字符串 DP 的经典模型,掌握其状态定义和转移逻辑至关重要。
- 三种操作(插入、删除、替换)在 DP 中对称体现,可通过"对齐"思想理解。
- 空间优化技巧适用于所有"只依赖前一行"的二维 DP 问题。
💡 扩展思考:
- 若允许"交换相邻字符"操作(如 Damerau-Levenshtein 距离),如何修改?
- 在实际应用中(如拼写检查、DNA 序列比对),编辑距离是基础算法。
✅ LeetCode Hot 100 中此题地位 :
作为字符串 + 动态规划的代表题,是必须掌握的核心题型之一。
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions