ChatGPT 算法训练营 — 编辑距离

1,865 阅读5分钟

ChatGPT 写代码还是可以的,不过在一步一步推导过程中就会错误频出了。

编辑距离是一种用来度量两个字符串之间相似程度的算法。它通常用于搜索错别字的纠正,或者模糊搜索。例如,如果我们搜索 gogle 时,它可以正确搜索到 google 的结果。

算法原理

编辑距离通过将一个字符串转换成另一个字符串所需的最少操作数来衡量两个字符串的相似度。这些操作可以是插入一个字符、删除一个字符或替换一个字符。

编辑距离的实现是通过动态规划来做的,那就让我们来看看如何用动态规划来拆分子问题。

具体来说,我们可以把两个字符串看成是由它们的子串组成的,那么问题就变成了如何计算这些子串之间的编辑距离。我们可以先计算最小的子串之间的编辑距离,然后再逐渐扩大子串的范围,直到计算出整个字符串的编辑距离。这样,我们就把大问题拆分成了很多个小问题。

我们假设两个字符串分别为 AB, 当我们需要计算A 的前 i 个字符到 B 的前 j 个字符的编辑距离dp[i][j]时,我们需要考虑以下三种情况:

  1. 如果 A[i] == B[i], 则其编辑距离就等于 dp[i-1][j-1]
  2. 如果 A[i] != B[i],则要分三种情况,然后取三种情况的最小值就是编辑距离:
    • 如果将 A 的第 i 个字符替换为 B 的第 j 个字符,那么编辑距离就是 dp[i-1][j-1] + 1

    • 如果将 A 的第 i 个字符删除,那么编辑距离就是 dp[i-1][j] + 1

    • 如果将 B 的第 j 个字符插入到 A 的第 i 个字符之后,那么编辑距离就是 dp[i][j-1] + 1

举个例子

假设 AabcdBabedf。我们来一步一步看它的编辑距离是怎么计算的。

首先,我们创建一个表格:

abedf
012345
a1x
b2
c3
d4

表格中的值 dp[i][j]A 的前 i 的字符到 B 的前 j 个字符的编辑距离。我们首先填充第一行和第一列,因为从空字符串转换到一个字符串的编辑距离就是进行该字符串长度的插入。这个就简单了。

例如这个时候我们要计算 x 位置的编辑距离, 我们就可以带入上面的那几种情况:

因为 ABx 位置的都为 a,所以其编辑距离就等于 d[i-1][j-1], 也就是 0, 我们将其更新到表格中

abedf
012345
a10x
b2
c3
d4

接下来我们就可以计算新的 x 位置的编辑距离了,由于此时 A[i] != B[j] 了, 我们就要按前文说的分三种情况分别计算了:

  1. 替换: dp[i-1][j-1] + 1 = 1 + 1 = 2
  2. 删除: dp[i-1][j] + 1 = 2 + 1 = 3
  3. 插入: dp[i][j-1] + 1 = 0 + 1 = 1

所以我们可以取得最小值为 1,然后写入表格

abedf
012345
a101x
b2
c3
d4

重复上面的步骤,我们就可以得到最终的结果

abedf
012345
a101234
b210123
c321123
d432212

因此我们就可以得到最终的编辑距离为 2 了。

如果理解了上述过程,写代码就是个翻译过程了。

代码实现

koltin

fun editDistance(s: String, t: String): Int {
    val m = s.length
    val n = t.length

    val dp = Array(m+1) { IntArray(n+1) }

    for (i in 0..m) {
        dp[i][0] = i
    }
    for (j in 0..n) {
        dp[0][j] = j
    }

    for (i in 1..m) {
        for (j in 1..n) {
            if (s[i-1] == t[j-1]) {
                dp[i][j] = dp[i-1][j-1]
            } else {
                dp[i][j] = minOf(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]) + 1
            }
        }
    }

    return dp[m][n]
}

Rust 实现

fn edit_distance(s: &str, t: &str) -> usize {
    let m = s.chars().count();
    let n = t.chars().count();

    let mut dp = vec![vec![0; n+1]; m+1];

    for i in 0..=m {
        dp[i][0] = i;
    }
    for j in 0..=n {
        dp[0][j] = j;
    }

    for i in 1..=m {
        for j in 1..=n {
            if s.chars().nth(i-1) == t.chars().nth(j-1) {
                dp[i][j] = dp[i-1][j-1];
            } else {
                dp[i][j] = dp[i-1][j-1].min(dp[i][j-1]).min(dp[i-1][j]) + 1;
            }
        }
    }

    dp[m][n]
}

接下来让我们分析下复杂度:

在上面的实现中,我们使用了一个二维数组 dp 来存储每个子问题的编辑距离。因此,算法的空间复杂度为 O(mn),其中 mn 分别是两个字符串的长度。

在算法的时间复杂度方面,我们需要遍历每个子问题,并对它们进行一些操作(比较两个字符是否相等、取最小值等)。因此,时间复杂度为 O(mn)

算法优化

如果我们看上面流程,我们是一行一行的去计算,而每一行的计算只依赖于几个特定点的值,因而我们可以使用滚动数组技巧将空间复杂度降至 O(n)。

Kotlin 实现

fun editDistance(s: String, t: String): Int {
    val m = s.length
    val n = t.length
    val dp = IntArray(n + 1) { it }
    var pre: Int
    var temp: Int
    for (i in 1..m) {
        pre = dp[0]
        dp[0] = i
        for (j in 1..n) {
            temp = dp[j]
            dp[j] = (pre + (s[i - 1] != t[j - 1]).toInt())
                .coerceAtMost(dp[j - 1] + 1)
                .coerceAtMost(dp[j] + 1)
            pre = temp
        }
    }
    return dp[n]
}

Rust 实现

fn edit_distance(s: &str, t: &str) -> usize {
    let m = s.len();
    let n = t.len();
    let mut dp = vec![0; n + 1];
    for j in 0..=n {
        dp[j] = j;
    }
    for i in 1..=m {
        let mut pre = dp[0];
        dp[0] = i;
        for j in 1..=n {
            let temp = dp[j];
            dp[j] = (pre + (s.as_bytes()[i - 1] != t.as_bytes()[j - 1]) as usize)
                .min((dp[j - 1] + 1).min(dp[j] + 1));
            pre = temp;
        }
    }
    dp[n]
}

我是古哥E下,前微信读书客户端程序猿 / 自学 5 年中医,维护过上万 Star 开源项目 QMUI Android,现独立维护好用简洁的 Android 组件库 emo。

关注我可得: ChatGPT 开发玩法 | 程序员学习经验 | 组件库新变动 | 中医健康调理 。

emo官网:emo.qhplus.cn