什么是编辑距离?如何用动态规划计算编辑距离

来源:站长查询作者:马来西亚程序员头衔:程序员
导读:本期聚焦于小伙伴创作的《什么是编辑距离?如何用动态规划计算编辑距离》,敬请观看详情。编辑距离衡量两个字符串相互转换所需的最少单字符操作次数,常出现在拼写检查与生物序列比对中。其底层逻辑是把字符串对齐问题拆成子问题:每步只能插入、删除或替换一个字符。若直接递归枚举所有操作,时间复杂度会指数级膨胀。动态规划通过维护二维状态表,将重复子结构结果缓存,把复杂度压到行列乘积级别。设 dp[i][j] 表示前缀长 i 与 j 的最小代价,边界为空串转换,转移时取左上、左、上三方向最小值加操作成本。掌握状态定义与初始化,就能写出稳定可复用的实现。

编辑距离(Levenshtein Distance)是度量两个字符串差异程度的基础指标,定义为将一个字符串变成另一个字符串所需的最少单字符编辑操作次数。允许的操作通常包括插入一个字符、删除一个字符以及替换一个字符。它在自然语言处理、拼写纠错、DNA序列比对以及版本差异对比中都有广泛应用。理解编辑距离不仅有助于解决实际工程中的相似度判断问题,也是学习动态规划思想的经典入口。

什么是编辑距离?如何用动态规划计算编辑距离

一、编辑距离的问题定义

给定两个字符串 s1 和 s2,长度分别为 m 和 n。我们希望通过一系列基本操作把 s1 转换成 s2,操作次数越少越好。每一次操作可以是:在 s1 中插入一个字符,使长度加一;从 s1 中删除一个字符,使长度减一;或者把 s1 当前某个位置的字符替换成另一个字符。这三种操作通常计为代价 1,但在某些业务场景下也可以赋予不同权重。

从直观上看,如果两个字符串开头字符相同,那么它们对编辑距离的贡献应当从剩余子串继续计算;如果不同,就需要考虑替换、删除或插入哪条路径更优。这种“局部最优可推导全局最优”的性质,正是动态规划能够生效的前提。若采用暴力递归枚举所有操作组合,时间复杂度会达到指数级,无法处理稍长的文本。

二、动态规划的状态与转移

我们定义二维数组 dp,其中 dp[i][j] 表示 s1 的前 i 个字符(即 s1[0..i-1])转换成 s2 的前 j 个字符(即 s2[0..j-1])所需的最小编辑次数。这里使用前缀长度而非下标本身,是为了让空串情况落在索引 0 上,简化边界处理。

状态转移时,若 s1[i-1] 等于 s2[j-1],则这两个字符无需额外操作,直接继承 dp[i-1][j-1];若不相等,则可以从三个方向取最小再加一:从左上角 dp[i-1][j-1] 做替换,从左方 dp[i][j-1] 做插入,从上方 dp[i-1][j] 做删除。边界条件是 dp[0][j] 等于 j(空串插入 j 次),dp[i][0] 等于 i(删除 i 次)。

def edit_distance(s1, s2):
    m, n = len(s1), len(s2)
    # 创建 (m+1) x (n+1) 的二维列表
    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
    # 填充状态表
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i - 1] == s2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]
            else:
                dp[i][j] = min(
                    dp[i - 1][j - 1] + 1,  # 替换
                    dp[i][j - 1] + 1,      # 插入
                    dp[i - 1][j] + 1       # 删除
                )
    return dp[m][n]

# 示例
print(edit_distance("kitten", "sitting"))  # 输出 3

三、复杂度与空间优化

上述实现的时间复杂度为 O(m*n),空间复杂度同样为 O(m*n),因为需要完整保存二维表。对于长度在几千以内的字符串,这种开销完全可以接受。而且保留完整 dp 表还能反向追踪具体编辑路径,用于展示“如何从一个串变到另一个串”。

如果只关心距离数值,可以将空间压缩到 O(min(m, n))。核心是观察到每一行只依赖上一行,因此用两个一维数组滚动更新即可。下方代码展示以较短串为列、仅用两行缓冲的写法,在内存受限环境更友好,但会丢失路径还原能力,需按业务权衡。

def edit_distance_space(s1, s2):
    # 保证 s2 是较短串,节省空间
    if len(s1) < len(s2):
        s1, s2 = s2, s1
    m, n = len(s1), len(s2)
    prev = list(range(n + 1))
    curr = [0] * (n + 1)
    for i in range(1, m + 1):
        curr[0] = i
        for j in range(1, n + 1):
            if s1[i - 1] == s2[j - 1]:
                curr[j] = prev[j - 1]
            else:
                curr[j] = min(prev[j - 1] + 1, prev[j] + 1, curr[j - 1] + 1)
        prev, curr = curr, prev
    return prev[n]

print(edit_distance_space("sunday", "saturday"))  # 输出 3

四、常见误区与工程注意点

一个常见误区是把编辑距离和最长公共子序列混为一谈。最长公共子序列只关心保留字符,不计入插入删除的对称代价;编辑距离则明确建模了单向或双向的改写成本。另外,在中文场景下,若以字为单位而非字节或词,需先做好分词或字符切分,否则 UTF-8 编码长度会干扰计数。

在工程落地时,如果批量计算大量字符串两两距离,应当考虑利用矩阵运算库或剪枝策略,例如当长度差已超过当前最优值就提前终止。对于模糊搜索,可结合 BK 树或 SimHash 降低全量比对开销,而不是每次都跑完整动态规划。

编辑距离的价值不只在于算出一个数,更在于它用严谨的状态定义把“相似”变成了可计算的问题。

五、小结

通过动态规划计算编辑距离,本质是把字符串转换过程拆成可复用的子问题,用一张表记录前缀之间的最优解。只要抓住状态定义、边界初始化与三类操作转移,就能写出正确且易维护的代码。进一步的空间压缩与路径回溯,则视具体业务对内存和解释性的要求而定。

编辑距离动态规划字符串匹配修改时间:2026-08-01 18:24:37

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。