文章目录
HJ52 计算字符串的编辑距离
描述
Levenshtein 距离,又称编辑距离,指的是两个字符串之间,由一个转变成另一个所需的最少单字符编辑操作次数。被允许的转变包括:
- 对于任意一个字符串,在任意位置插入一个字符;
- 对于任意一个字符串,删除任意一个字符;
- 对于任意一个字符串,替换任意一个字符。
现在,对于给定的字符串 s 和 t,请计算出它们的编辑距离。
输入描述
第一行输入一个长度为 1<=len(s)<=10^3,仅由小写字母组成的字符串 s。
第二行输入一个长度为 1<=len(t)<=10^3,仅由小写字母组成的字符串 t。
输出描述
输出一个整数,表示 s 和 t 的编辑距离。
示例1
输入:
abcdefg
abcdef
输出:
1
说明:
在这个样例中,可以选择将 s 末尾的 ‘g’ 删除。当然,也可以选择在 t 末尾插入 ‘g’。
HJ52 计算字符串的编辑距离
描述
Levenshtein 距离,又称编辑距离,指的是两个字符串之间,由一个转变成另一个所需的最少单字符编辑操作次数。被允许的转变包括:
- 对于任意一个字符串,在任意位置插入一个字符;
- 对于任意一个字符串,删除任意一个字符;
- 对于任意一个字符串,替换任意一个字符。
现在,对于给定的字符串 s 和 t,请计算出它们的编辑距离。
输入描述
第一行输入一个长度为 1<=len(s)<=10^3,仅由小写字母组成的字符串 s。
第二行输入一个长度为 1<=len(t)<=10^3,仅由小写字母组成的字符串 t。
输出描述
输出一个整数,表示 s 和 t 的编辑距离。
示例1
输入:
abcdefg
abcdef
输出:
1
说明:
在这个样例中,可以选择将 s 末尾的 ‘g’ 删除。当然,也可以选择在 t 末尾插入 ‘g’。
解题思路
算法分析
这道题是经典的动态规划问题——编辑距离(Levenshtein距离)。主要涉及:
- 状态定义:dp[i][j]表示s[0:i]和t[0:j]的编辑距离
- 状态转移:根据字符是否相同选择最优操作
- 边界处理:空字符串的初始化
- 空间优化:滚动数组优化空间复杂度
动态规划状态转移
状态转移方程
graph LR
A[状态转移方程] --> B[状态 dp_i_j]
B --> C{当前字符是否相同}
C -->|相同| D[继承左上状态 dp_i_减1_j_减1]
C -->|不同| E[三种操作取最小值]
E --> F[删除:删除 s_第_i_个字符]
E --> G[插入:插入 t_第_j_个字符]
E --> H[替换:将 s_第_i_个字符替换为 t_第_j_个]
算法流程图
flowchart TD
A[读取字符串 s 和 t] --> B[获取长度:m 是 s 的长度,n 是 t 的长度]
B --> C[创建 dp 数组,大小为 m 加 1 乘以 n 加 1]
C --> D[初始化边界条件]
D --> E[第 0 行:dp_0_j 设为 j,遍历所有 j]
E --> F[第 0 列:dp_i_0 设为 i,遍历所有 i]
F --> G[进入双重循环,填充 dp 表]
G --> H{当前 s 的第 i 个字符是否等于 t 的第 j 个字符}
H -->|是| I[dp_i_j 设为 dp_i_减1_j_减1]
H -->|否| J[dp_i_j 设为三种操作中的最小值]
I --> K{是否循环结束}
J --> K
K -->|否| G
K -->|是| L[返回最终结果 dp_m_n]
DP表格示例
以s=“abc”, t="ab"为例:
graph TD
A[DP 表格构建过程] --> B[构造完整 DP 数组并填充]
B --> C[每个 dp_i_j 代表编辑距离]
C --> D[完成后,查看 dp_3_2 的值]
D --> E[最终答案:dp_3_2 等于 1]
三种操作详解
graph TD
A[编辑操作] --> B[插入操作]
A --> C[删除操作]
A --> D[替换操作]
B --> E[在字符串 s 中插入字符]
B --> F[代价:dp_i_j减1 加一]
C --> G[从字符串 s 中删除字符]
C --> H[代价:dp_i减1_j 加一]
D --> I[将 s 中字符替换为 t 中字符]
D --> J[代价:dp_i减1_j减1 加一]
代码实现思路
基础DP版本:
- 二维数组存储所有状态
- 时间复杂度O(mn),空间复杂度O(mn)
- 易于理解和调试
空间优化版本:
- 滚动数组优化空间
- 时间复杂度O(mn),空间复杂度O(min(m,n))
- 适用于大规模数据
记忆化递归版本:
- 自顶向下的思考方式
- 代码更直观,容易理解递归关系
- 适合面试讲解
时间复杂度分析
算法版本 | 时间复杂度 | 空间复杂度 | 特点 |
---|---|---|---|
基础DP | O(mn) | O(mn) | 经典实现 |
空间优化 | O(mn) | O(min(m,n)) | 节省空间 |
记忆化递归 | O(mn) | O(mn) | 易于理解 |
关键优化技巧
空间优化:
// 只需要两行数组 prev := make([]int, m+1) curr := make([]int, m+1)
字符串长度优化:
// 确保s是较短的字符串 if m > n { s, t = t, s m, n = n, m }
边界条件处理:
// 空字符串的初始化 dp[0][j] = j // 需要j次插入 dp[i][0] = i // 需要i次删除
实际应用场景
- 文本相似度计算:搜索引擎的模糊匹配
- 拼写检查:单词纠错功能
- DNA序列比对:生物信息学中的序列分析
- 版本控制:Git中的文件差异比较
- 自然语言处理:文本相似度度量
算法扩展
面试考点
- 状态定义能力:正确定义DP状态
- 状态转移推导:理解三种操作的含义
- 边界条件处理:空字符串的初始化
- 空间优化思维:滚动数组的使用
- 实际应用理解:算法的实际价值
这个问题是动态规划的经典应用,展示了如何将复杂问题分解为子问题,并通过状态转移方程求解最优解。编辑距离算法在实际工程中有广泛应用,是面试中的高频考点。
完整题解代码
package main
import (
"fmt"
)
func main() {
var s, t string
fmt.Scanln(&s)
fmt.Scanln(&t)
result := editDistance(s, t)
fmt.Println(result)
}
// editDistance 计算两个字符串的编辑距离
// 使用动态规划算法,时间复杂度O(mn),空间复杂度O(mn)
func editDistance(s, t string) int {
m, n := len(s), len(t)
// dp[i][j] 表示 s[0:i] 和 t[0:j] 的编辑距离
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
// 初始化边界条件
// s为空字符串时,需要插入t的所有字符
for j := 0; j <= n; j++ {
dp[0][j] = j
}
// t为空字符串时,需要删除s的所有字符
for i := 0; i <= m; i++ {
dp[i][0] = i
}
// 动态规划填表
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if s[i-1] == t[j-1] {
// 字符相同,不需要操作
dp[i][j] = dp[i-1][j-1]
} else {
// 字符不同,取三种操作的最小值
dp[i][j] = min(
dp[i-1][j]+1, // 删除s[i-1]
dp[i][j-1]+1, // 插入t[j-1]
dp[i-1][j-1]+1, // 替换s[i-1]为t[j-1]
)
}
}
}
return dp[m][n]
}
// 空间优化版本:只使用O(min(m,n))空间
func editDistanceOptimized(s, t string) int {
m, n := len(s), len(t)
// 确保s是较短的字符串,优化空间使用
if m > n {
s, t = t, s
m, n = n, m
}
// 只需要两行:当前行和上一行
prev := make([]int, m+1)
curr := make([]int, m+1)
// 初始化第一行
for i := 0; i <= m; i++ {
prev[i] = i
}
// 逐行计算
for j := 1; j <= n; j++ {
curr[0] = j // 边界条件
for i := 1; i <= m; i++ {
if s[i-1] == t[j-1] {
curr[i] = prev[i-1]
} else {
curr[i] = min(
prev[i]+1, // 删除
curr[i-1]+1, // 插入
prev[i-1]+1, // 替换
)
}
}
// 交换prev和curr
prev, curr = curr, prev
}
return prev[m]
}
// 递归+记忆化版本(展示不同实现思路)
func editDistanceMemo(s, t string) int {
m, n := len(s), len(t)
memo := make([][]int, m+1)
for i := range memo {
memo[i] = make([]int, n+1)
for j := range memo[i] {
memo[i][j] = -1
}
}
var dfs func(i, j int) int
dfs = func(i, j int) int {
// 边界条件
if i == 0 {
return j
}
if j == 0 {
return i
}
// 记忆化
if memo[i][j] != -1 {
return memo[i][j]
}
if s[i-1] == t[j-1] {
memo[i][j] = dfs(i-1, j-1)
} else {
memo[i][j] = min(
dfs(i-1, j)+1, // 删除
dfs(i, j-1)+1, // 插入
dfs(i-1, j-1)+1, // 替换
)
}
return memo[i][j]
}
return dfs(m, n)
}
// min 返回三个数中的最小值
func min(a, b, c int) int {
if a <= b && a <= c {
return a
}
if b <= c {
return b
}
return c
}
// 用于两个数的min函数
func min2(a, b int) int {
if a < b {
return a
}
return b
}