「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战」。
题目
链接:leetcode-cn.com/problems/ed…
给你两个单词 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')
提示:
0 <= word1.length, word2.length <= 500word1和word2由小写英文字母组成
解题思路
思路1
采用自顶向下的递归方式,从右向左来对比两个字符串。
如果字符相同,则不需要操作,直接跳过;另外在其他三个操作方法(删除,替换,插入)中选择操作步数最少的方法。(备注:不要陷入递归的细节中去,我们的脑子可不像电脑那样一直可以进行压栈出栈操作)
递归解法存在许多的重叠子问题,采用备忘录的方式来记录对应长度的操作步数。
我们从后往前遍历字符串word1,word2,当两个字符串相等时,我们不做修改,继续往前遍历,当不相等时,可以做如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
如果字符串word1遍历结束,则将word2剩下的字符添加到word1首部,操作次数为word2剩下的字符数。
如果字符串word2遍历结束,则将word1剩下的字符删除,操作次数为word1剩下的字符数。
/**
* @param {string} word1
* @param {string} word2
* @return {number}
*/
var minDistance = function(word1, word2) {
const len1 = word1.length, len2 = word2.length
// 初始化备忘录二维数组
let memo = Array(len1+1)
for(let i=0; i<len1+1; i++) {
memo[i] = Array(len2+1).fill(-1)
}
/**
*@param {number} i word1.length-1
*@param {number} j word2.length-2
*@return {number} 编辑步数
*/
function dp(i, j) {
// base case
// 如果word1串读完了,则直接插入剩余的word2串
if(i == -1) return j+1
// 如果word2串读完了,则直接删除剩余的word1串
if(j == -1) return i+1
// 对应长度的步数已在备忘录中,直接返回步数
if(memo[i][j] != -1) {
return memo[i][j]
}
// 两个字符相同,不需要任何操作,直接跳过
if (word1.charAt(i) == word2.charAt(j)) {
memo[i][j] = dp(i-1, j-1)
}
// 不同则选择步数最少的操作
else {
memo[i][j] = min(
dp(i-1, j-1)+1, //替换操作,两个串的长度均减一
dp(i-1, j)+1, //word1串的删除操作,word1串长度减一
dp(i, j-1)+1 //word1串的插入操作,word1不变,word2减一
)
}
// 返回需要操作的步数
return memo[i][j]
}
// 在三个值中选最小值
function min(a, b, c) {
return Math.min(a, Math.min(b, c))
}
// 调用dp函数,返回结果
return dp(len1-1, len2-1)
};
思路2
暴力穷举
我们根据上面的思路写出如下代码
var minDistance = function(word1, word2) {
const dp = (i, j) => {
// 因为递归传入的是上一次的(i -1, j - 1),所以这里都需要 + 1
if (i === -1) return j + 1 // s1走完了,将s2剩下的插入s1,需要j + 1步
if (j === -1) return i + 1 // s2走完了,删除s1剩下的,需要i + 1步
if (word1[i] === word2[j]) {
// 什么都不做,i,j向前移动一位
return dp(i - 1, j - 1)
} else {
// 找出最小的
return Math.min(
dp(i, j - 1) + 1, // 插入,在word1[i]中插入和word2[j]一样的字符,相当于把word2向前移动1位,word1不动
dp(i - 1, j) + 1, // 删除,把word1[i]删除,相当于word1向前移动1位,word2不动
dp(i - 1, j - 1) + 1 // 替换操作,都向前移动1位
)
}
}
// 从后往前遍历,i, j 初始化指向最后一个索引
return dp(word1.length - 1, word2.length - 1)
};
上面我们用暴力递归穷举出了所有方法,找出其中步骤最小的。我们想办法来对上面的代码做些优化。
思路3
记忆化搜索
我们添加一个字典来存储已经计算过的项。
var minDistance = function(word1, word2) {
// 建一个字典
const memo = new Map()
const dp = (i, j) => {
if (memo.has(i + '' + j)) return memo.get((i + '' + j))
if (i === -1) return j + 1 // s1走完了,将s2剩下的插入s1,需要j + 1步
if (j === -1) return i + 1 // s2走完了,删除s1剩下的,需要i + 1步
if (word1[i] === word2[j]) {
// 什么都不做,i,j向前移动一位
memo.set(i + '' + j, dp(i - 1, j - 1))
} else {
memo.set(i + '' + j, Math.min(
dp(i, j - 1) + 1, // 插入,在word1[i]中插入和word2[j]一样的字符,相当于把word2向前移动1位,word1不动
dp(i - 1, j) + 1, // 删除,把word1[i]删除,相当于word1向前移动1位,word2不动
dp(i - 1, j - 1) + 1 // 替换操作,都向前移动1位
))
}
return memo.get(i + '' + j)
}
// 从后往前遍历,i, j 初始化指向最后一个索引
return dp(word1.length - 1, word2.length - 1)
};
思路4
我们使用DP来解这道题。详细看代码
function minDistance(word1: string, word2: string): number {
const m = word1.length, n = word2.length;
// 我们要多添加一行一列,用来做base case
const dp = Array.from(Array(word1.length + 1), () => Array(word2.length+1).fill(0));
// 添加一列,base case
for (let i = 1; i <= m; i++) {
dp[i][0] = i;
}
// 添加一行,base case
for (let i = 1; i <= n; i++) {
dp[0][i] = i;
}
// 因为我们补了一行/列base case,这里都从1开始
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (word1[i - 1] === word2[j - 1]) { // 相等,什么都不做
dp[i][j] = dp[i - 1][j - 1]
} else {
dp[i][j] = Math.min(
dp[i - 1][j] + 1, // s1删除操作
dp[i][j - 1] + 1, // s1插入操作
dp[i -1][j - 1] + 1 // 替换
)
}
}
}
return dp[m][n]
};
复杂度分析
暴力穷举时间复杂度为指数级,加字典后为O(mn), DP时间复杂度O(mn),空间复杂度O(mn)