墨风如雪博客

  • 源码小店
  • 导航站
  • 登录
  • java
  • 资源分享
让AI使用变得如此简单
  1. 首页
  2. 算法
  3. 正文

每日一道算法题:编辑距离算法详解

2023年 6月 21日 107点热度 0人点赞 0条评论

引言

编辑距离(Edit Distance),又称Levenshtein距离,是指通过对字符串进行添加、删除、修改操作,将一个字符串转化为另一个字符串所需的最少操作次数。编辑距离算法被广泛应用于字符串匹配、语音识别、基因序列比对等领域。

本文将介绍编辑距离算法的概念、基本算法及其改进,以及应用扩展。并给出相应JAVA代码。

编辑距离的概念和应用

编辑距离计算的是两个字符串之间的相似度。在字符串匹配、文本处理、数据挖掘等领域都有广泛的应用。

举例说明,两个字符串$s_1,s_2$的编辑距离可以通过以下方式计算:

  • 通过将一个字符替换成另一个字符,来使$s_1$和$s_2$更相似;
  • 通过将某个字符删除,来让$s_1$和$s_2$更相似;
  • 通过增加一个字符,来让$s_1$和$s_2$更相似。

一个字符串$s$与$s'$的编辑距离,我们记为$D(s,s')$。则两个字符串$s_1,s_2$之间的编辑距离$D(s_1,s_2)$可以通过以下三种操作实现:

  1. 插入操作将一个字符插入到字符串中某个合适的位置;
  2. 删除操作将一个字符从字符串中删除;
  3. 替换操作将一个字符替换成另一个字符。

其中的每种操作都有一个惩罚因子,分别记为$c{\text{ins}}$、$c{\text{del}}$、$c_{\text{rep}}$。它们代表相应操作的代价。

字符串之间的编辑距离可以通过动态规划求解。

基本算法

1. 暴力算法

暴力算法通过穷举可行编辑操作的方式,找到两个字符串之间最短的编辑距离。

暴力算法存在一个递归式,通过最优子结构性质,自底向上的计算编辑距离。

代码实现:

public static int editDistance(String word1, String word2) {
    int n = word1.length();
    int m = word2.length();

    if (n * m == 0) {
        return n + m;
    }

    int[][] distance = new int[n + 1][m + 1];

    // 初始化边界条件
    for (int i = 0; i < n + 1; i++) {
        distance[i][0] = i;
    }
    for (int j = 0; j < m + 1; j++) {
        distance[0][j] = j;
    }

    // 状态转移
    for (int i = 1; i < n + 1; i++) {
        for (int j = 1; j < m + 1; j++) {
            int left = distance[i - 1][j] + 1;
            int down = distance[i][j - 1] + 1;
            int left_down = distance[i - 1][j - 1];
            if (word1.charAt(i - 1) != word2.charAt(j - 1)) {
                left_down ++;
            }
            distance[i][j] = Math.min(left, Math.min(down, left_down));

        }
    }

    return distance[n][m];
}

时间复杂度:$O(nm)$。

空间复杂度:$O(nm)$。

2. DP算法

DP算法也是自底向上的动态规划算法。

代码实现:

public static int editDistance(String word1, String word2) {
    if (word1 == null || word2 == null) {
        return -1;
    }

    int n = word1.length();
    int m = word2.length();

    if (n * m == 0) {
        return n + m;
    }

    int[] pre = new int[m + 1];
    int[] cur = new int[m + 1];

    // 第一层,pre先赋值
    for (int j = 0; j < m + 1; j++) {
        pre[j] = j;
    }

    // 状态转移
    for (int i = 1; i < n + 1; i++) {
        cur[0] = i;
        for (int j = 1; j < m + 1; j++) {
            int left = pre[j] + 1;
            int down = cur[j - 1] + 1;
            int left_down = pre[j - 1];
            if (word1.charAt(i - 1) != word2.charAt(j - 1)) {
                left_down++;
            }
            cur[j] = Math.min(left, Math.min(down, left_down));
        }
        pre = cur.clone();
    }

    return pre[m];
}

时间复杂度:$O(nm)$。

空间复杂度可以进一步优化,见后续改进算法。

改进算法

1. 基于滚动数组的DP算法

基于DP算法的空间复杂度,我们可以发现只与上一层的状态有关,因此可以使用滚动数组,使得空间复杂度变为$O(n)$。

代码实现:

public static int editDistance(String word1, String word2) {
    if (word1 == null || word2 == null) {
        return -1;
    }

    int n = word1.length();
    int m = word2.length();

    if (n * m == 0) {
        return n + m;
    }

    int[] dp = new int[m + 1];

    // 第一层:初始化
    for (int j = 0; j < m + 1; j++) {
        dp[j] = j;
    }

    // 状态转移
    for (int i = 1; i < n + 1; i++) {
        int left_top = dp[0];
        dp[0] = i;
        for (int j = 1; j < m + 1; j++) {
            int left = dp[j - 1] + 1;
            int down = dp[j] + 1;
            int left_down = left_top;
            if (word1.charAt(i - 1) != word2.charAt(j - 1)) {
                left_down++;
            }
            left_top = dp[j];
            dp[j] = Math.min(left, Math.min(down, left_down));
        }
    }

    return dp[m];
}

时间复杂度:$O(nm)$。

空间复杂度:$O(n)$。

2. 基于分块的DP算法

当字符串的长度非常长时,基于滚动数组的优化效果会有所下降。

我们可以基于分块的思想,将一位向量$v$块分为$S=\sqrt{n}$个块,每个块中包含$\sqrt{n}$个元素。并且我们可以事先预处理出所有块之间的编辑距离。

在算法的每次编辑中,只有两个块之间存在编辑操作。因此,我们只需要计算需要编辑的两个块之间的编辑距离即可。

时间复杂度:$O(n\sqrt{n})$。

由于实现较为复杂,代码略。

应用扩展

1. 电话号码转换

电话号码的转换问题可以看作是编辑距离问题的一个特殊情况,其中可以通过增加、删除或更改一个数字得到一个新的电话号码。为了满足编辑距离问题的约束,我们为每个运营商提供一个所有有效的电话号码列表,然后对所有号码进行比较。

代码实现:

public static int phoneNumTransfer(String num1, String num2, Map<Character, String> mapping) {
    String str1 = transfer(num1, mapping);
    String str2 = transfer(num2, mapping);

    return editDistance(str1, str2);
}

public static String transfer(String num, Map<Character, String> mapping) {
    StringBuffer buffer = new StringBuffer();
    for (int i = 0; i < num.length(); i++) {
        char c = num.charAt(i);
        if (mapping.containsKey(c)) {
            buffer.append(mapping.get(c));
        }
    }
    return buffer.toString();
}

public static void main(String[] args) {
    Map<Character, String> mapping = new HashMap<>();
    mapping.put('0', "0");
    mapping.put('1', "1");
    mapping.put('2', "abc");
    mapping.put('3', "def");
    mapping.put('4', "ghi");
    mapping.put('5', "jkl");
    mapping.put('6', "mno");
    mapping.put('7', "pqrs");
    mapping.put('8', "tuv");
    mapping.put('9', "wxyz");

    int result = phoneNumTransfer("1234", "9234", mapping);
    System.out.println(result);
}

2. 最长公共子序列问题

最长公共子序列问题是指找到两个序列中的最长公共子序列。该问题可以通过基于DP的编辑距离算法来求解。

代码实现:

public static int longestCommonSubsequences(String word1, String word2) {
    int n = word1.length();
    int m = word2.length();

    if (n * m == 0) {
        return 0;
    }

    int[][] dp = new int[n + 1][m + 1];

    // 初始化边界条件
    for (int i = 0; i < n + 1; i++) {
        dp[i][0] = 0;
    }
    for (int j = 0; j < m + 1; j++) {
        dp[0][j] = 0;
    }

    // 状态转移
    for (int i = 1; i < n + 1; i++) {
        for (int j = 1; j < m + 1; j++) {
            if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }

    return dp[n][m];
}

时间复杂度:$O(nm)$。

空间复杂度:$O(nm)$。

3. 基因序列比对

基因序列比对是通过比较两个基因序列是否相同来识别性别和疾病的遗传性质。

当两个基因序列非常长时,我们可以使用基于分块的DP算法来加速计算。而且,由于很多基因序列比较相似,因此我们可以使用频率域比对方法来快速判断它们之间的相似度。

我们可以使用质量值指标来预测每个字符在字面上出现的可能性,这样比对和计算编辑距离时就可以考虑字母出现的频率。

由于实现较为复杂,代码略。

总结

本文阐述了字符串编辑距离算法的概念及其应用、基本算法和改进算法,以及一些应用场景。对不同算法的时间复杂度和空间复杂度进行了分析,并给出了相应的JAVA代码实现。

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: java 动态规划 图 字符串 广度遍历 深度遍历 算法 距离
最后更新:2023年 5月 27日

墨风如雪

一个热爱生活,热爱分享的程序员

打赏 点赞
< 上一篇
下一篇 >

文章评论

您需要 登录 之后才可以评论

墨风如雪

一个热爱生活,热爱分享的程序员

最新 热点 随机
最新 热点 随机
阿里WebAgent开源:引领自主搜索新纪元 重磅炸弹!字节跳动开源BAGEL:70亿参数,统一多模态理解与生成,AI“全能王”诞生记! 小米MiMo-VL:7B参数,怎么就成了多模态界的“越级打怪王”? 炸裂!DeepSeek 8B 量化版降临:告别显存焦虑,你的 3080 Ti 也能玩转顶级大模型了! 美团炸场AI圈:点外卖点出个软件?用「对话式编程」重塑生产力! 当你的证件照学会了眨眼微笑:腾讯混元 HunyuanPortrait 开源,让数字肖像「活过来」!
重塑AI推理格局?微软Phi-4模型震撼发布:轻量化性能炸裂炸裂!微软这门免费AI Agent新手课,GitHub近2万星,简直是宝藏!ComfyUI“打通任督二脉”:直接调用Veo2、GPT-4o等65大模型!一键串联你的AI工作流AI圈炸锅了!Mistral Medium 3:性能 SOTA,成本打骨折,企业玩家的新宠?字节终于开源“扣子”同款引擎了!FlowGram:AI 时代的可视化工作流利器告别“微信黑箱”!Chatlog:让你的聊天记录也能拥有“AI大脑”!
IBM WebSphere 企业级应用服务器 java 微服务框架技术Apache ServiceComb 遇到Github打不开的情况怎么处理? java 持久层框架Hibernate的(超详细总结) 门罗币 (XMR)简介:了解这种匿名数字货币的特点和优势? 设计模式:责任链设计模式
标签聚合
java 设计模式 动态规划 deepseek 算法 spring 教程 AI

COPYRIGHT © 2023 墨风如雪博客. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

免责声明 - 隐私政策