拼写错误检测(动态规划法)

最近忙于完成分布式数据库大项目的代码,算法作业也因此被搁置了好几天。分布式代码终于敲完交上去了,现在有时间继续完成算法作业了。其实算法作业并不是很难,大家仔细分析下问题,多搜搜博客,学习下前辈们如何解决这类问题,问题其实很容易解决的!只有站在前人的肩膀上,我们才能取得更大的进步,同时塑造更好的自己(不管了,我就是想写小学生作文~~~)!我还是本着原来的想法,我把我的思路和设计的关键部分贴出来,大家分享学习,全部的代码我不会放出来,原因很简单,我不是很想让大家都交雷同作业,也希望大家能理解。下面开始我们的第三个算法的设计与实现吧。


任务要求:以第2题中的哈希表英语词典为基础,应用动态规划法设计和实现拼写错误检测程序。 对输入单词进行检测,如果不正确,给出最接近的3个可选词。


算法思想:

首先我们要讲一个名词:编辑距离(EditDistance)【又称Levenshtein距离】:对于两个字符串,由一个转换为另一个所需的最少编辑操作次数。其中许可的编辑操作包括将一个字符替换成为另一个字符,插入一个字符,删除一个字符。

想了一个简单直观的例子,帮助大家理解以上两句话:

输入待查对象:kittain,我们字典中的一个对象sitting,我们求他俩的最短编辑距离的时候的步骤为:

sittain(k->s替换);

sittin(删掉a);

sitting(插入g)。

但是电脑毕竟不是人脑,它没有思想,它一眼看不出来我们上述的操作,但是它擅长搜索啊,计算啊。所以我们一个字母一个字母的开始做比较。

首先想我们算最终的编辑距离是不是得依赖前面算得的编辑距离,我们是不是得层层递归,才能算出从字符串a到字符串b的编辑距离,所以我们使用了分治的思想,将一个复杂的问题分解为类似的子问题。

所以我们先按照递归的思想:

字符串a,共m位,从a[1]到a[m]


字符串b,共n位,从b[1]到b[n]


d[i][j]表示字符串a到b的编辑距离。

按照递归的思想做解决这个问题(a[i]和b[j]为a,b的最后一位):

<1>当a[i]=b[j],d[i][j] = d[i-1][j-1]


<2>当d[i]!=b[j],d[i][j]等于如下三项的最小值:


    ·d[i-1][j]+1(删除a[i])


    ·d[i][j-1]+1(插入b[j])


    ·d[i-1][j-1]+1(将a[i]替换成b[j])

递归的边界:

·a[i][0] = i,b字符串为空,表示将a[1]到a[i]全部删除,所以编辑距离为i;


·a[0][i] = j,a字符串为空,表示将b[1]到b[j]全部插入,所以编辑距离为j。

优化:(动态规划解决冗余)
以上的设计思路中存在着严重的问题,由于采用了递归,导致代码的效率很低下,时间复杂度呈指数增长,也存在这大量的重复字问题的计算,解决这种冗余的办法就是用动态规划。

递归的解决思路是从后往前算的,也就是我们想的到d[i][j],就必须先得到 d[i][j-1]、d[i-1][j]、d[i-1][j-1]三者中的最小。

而动态规划的思想恰恰相反,它的思想就是从前往后算,先算出各个子问题,然后根据子问题,计算得出原问题,因此可以解决大量出现的冗余问题。

设计思路:

首先,在第二题的代码中,我给出了一个find()函数,这个函数的作用是根据我所输入的待查找字符串,计算出hashCode,然后去我的哈希表里面查找这个字符串,如果存在的话,可以直接返回该字符串对应的对象。因为hash表查找比我的编辑距离(Edit Distance)计算相似距离的方法快(编辑距离是循环hash表,表中每个单词都做比对),所以我们首先使用hash表的find方法查找输入的单词是不是正确的单词,如果不是,我们再使用动态规划优化后的编辑距离计算相似距离的方法找出三个最为相似的单词输出。

程序代码:

动态规划求编辑距离的核心代码:

package tst;

    /**
    *
    *@ClassName EditDistance.java 
    *@author Leno E-mail:cliugeek@us-forever.com
    *@date 2017年12月7日下午1:28:27
    *@Description 动态规划思想求编辑距离,从前往后算
    */

public class EditDistance {
    public int min(int a,int b,int c){
        int tmp = a<b?a:b;
        return tmp<c?tmp:c;
    }
    public int edit_distance(String a,String b){
        int lena = a.length();
        int lenb = b.length();
        int d[][] = new int[lena+1][lenb+1];
        int i = 0 , j = 0;
        for(i=0;i<=lena;i++){
            d[i][0] = i;
        }
        for(j=0;j<=lenb;j++){
            d[0][j] = j;
        }
        for(i=1;i<=lena;i++){
            for(j=1;j<=lenb;j++){
                if(a.charAt(i-1) == b.charAt(j-1)){
                    d[i][j] = d[i-1][j-1];
                }else{
                    d[i][j] = min(d[i-1][j]+1,d[i][j-1]+1,d[i-1][j-1]+1);
                }
            }
        }
        return d[lena][lenb]; 
    }
}

测试例和运行结果:

这次的测试例给的比较多,我随便输入了几个单词进行了相应的测试,结果如下:

result1

result2

result3

以上是本次作业的核心实现部分,如果有不正确的地方,还希望大家不吝指正。

<think>好的,我现在要解决用户提到的“最长公共子字符串问题”。首先,我需要明确这个问题到底是什么意思。最长公共子字符串,听起来像是找两个或多个字符串中连续相同的部分,对吧?比如“abcde”和“abcf”中的最长公共子字符串应该是“abc”,对吧?但用户可能需要更详细的解释,包括定义、应用场景、解决方等。 首先,我应该回忆下,子字符串和子序列的区别。子字符串必须是连续的,而子序列可以不连续。比如对于“abcde”和“aec”,最长公共子序列是“ace”,而子字符串可能只有“a”或“c”等。所以,最长公共子字符串问题确实是寻找两个字符串中连续相同的最长子串。 接下来,用户可能需要知道如何解决这个问题。常见的解有哪些呢?我记得暴力动态规划、后缀树等方。暴力的思路是检查所有可能的子字符串组合,然后找出最长的公共部分。但这种方的时间复杂度很高,是O(n^4)或者O(n^3)吗?比如对于两个长度为n的字符串,每个字符串有O()个子字符串,然后比较这些子字符串是否相等,最坏情况下时间复杂度很高,显然不适用于长字符串。 然后是动态规划动态规划通常用于优化重复子问题的情况。比如,最长公共子序列问题用动态规划可以做到O()的时间复杂度。那最长公共子字符串是否也可以用类似的方?是的,应该可以。建立个二维数组dp,其中dp[i][j]表示以第个字符串的第i个字符和第二个字符串的第j个字符结尾的最长公共子字符串的长度。如果当前字符相等,那么dp[i][j] = dp[i-1][j-1] + 1,否则dp[i][j] = 0。这样,遍历整个二维数组,记录最大值和位置,就能得到最长公共子字符串。这种方的时间复杂度是O(n*m),空间复杂度也是O(n*m),其中n和m是两个字符串的长度。如果空间不够优化的话,可能需要用滚动数组来减少空间复杂度,因为每次只需要前行的信息,所以可以优化到O(min(n,m))的空间。 还有种方是使用后缀树,构建广义后缀树,然后找到最深的共有节点,这样的方时间复杂度是线性的,O(n+m),但实现起来比较复杂,可能在实际应用中不太常见,尤其是对于编程面试等情况,动态规划更为常见。 然后,我需要考虑如何具体实现动态规划。举个例子,比如字符串“abcde”和“abcf”,它们的dp表应该是怎样的呢?比如,当i=0(假设从0开始索引)时,字符是a,和第二个字符串的各个位置比较,当j=0时,a和a相等,所以dp[0][0]=1;接着,当i=1,字符是b,和第二个字符串的j=1的b相等,此时dp[1][1] = dp[0][0]+1=2,依此类推,直到i=2,字符是c,j=2的c,此时dp[2][2]=3,这时候可能最长长度是3,对应的子字符串是abc。之后遇到j=3时,字符是f,与第三个字符d不同,所以dp[3][3]=0。这样,整个过程中最大的值就是3,对应的子字符串是abc。 用户可能还关心这个问题的应用场景,比如在生物信息学中比较DNA序列的相似性,或者版本控制系统中检测文件差异,或者文本编辑器的拼写检查等。这些都是实际应用的例子。 此外,是否需要处理多个字符串的情况?最长公共子字符串问题通常讨论的是两个字符串的情况,但如果有多个字符串,问题复杂度会大大增加,可能需要不同的方,比如将动态规划扩展到多维,但这会导致时间和空间复杂度急剧上升,因此实际中可能采用其他优化策略。 然后,可能还要提到优化空间的方。比如,如果使用动态规划,二维数组的空间可能比较大,可以只保留上行的数据,或者使用维数组,因为每次计算只需要左上的值,这样可以将空间复杂度降低到O(n)或O(m),取决于哪个更小。例如,如果字符串长度分别为m和n,那么用长度为n的数组,每次更新时从右往左或者从下往上,避免覆盖还未使用的数据。这可能需要更仔细的实现步骤。 另外,有没有其他变种问题?比如允许有k个不匹配的最长子字符串,或者寻找所有最长公共子字符串。这些可能需要不同的处理方式,但用户的问题主要集中在基本的最长公共子字符串上,所以可能暂时不需要深入讨论这些变种。 接下来,应该总结解决这个问题的步骤。首先明确问题定义,然后讲解动态规划的思路,给出状态转移方程,说明如何构建dp表,如何记录最大值及其位置,最后根据位置提取子字符串。同时,要分析时间复杂度和空间复杂度,以及可能的优化方。 在代码实现方面,可能给出伪代码或者具体语言的例子,但用户可能只需要了解思路,所以用自然语言描述即可。例如,初始化个二维数组,遍历两个字符串的每个字符,根据是否相等来更新dp值,并不断更新最大值和结束位置,最后根据结束位置和最大值长度截取子字符串。 最后,需要指出动态规划的优缺点。优点是思路清晰,实现相对简单,适用于中等长度的字符串;缺点是当字符串非常长时,时间和空间开销较大,此时可能需要更高效的算法如后缀树,但实现复杂。 总结起来,回答的结构应该是:问题定义、应用场景、解决方动态规划为主,可能提及其他方)、实现步骤、复杂度分析以及优化空间的方。这样用户能够逐步理解问题及其解决方案,并了解不同方的优缺点。</think>### 最长公共子字符串问题介绍 #### 1. **问题定义** **最长公共子字符串(Longest Common Substring)** 指在两个或多个字符串中,**连续相同** 的最长子串。例如: - 字符串 A:`"abcde"` - 字符串 B:`"abcf"` - 最长公共子字符串为 `"abc"`(长度 3)。 > 📌 注意:子字符串要求字符连续,与子序列(允许不连续)不同。 --- #### 2. **应用场景** - **生物信息学**:DNA序列比对。 - **文本差异检测**:版本控制系统(如Git)中的文件差异分析。 - **拼写检查**:查找输入错误与正确词汇的相似部分。 --- #### 3. **解决方** ##### **动态规划(最优常用方)** - **核心思想**:通过二维数组记录以每个字符结尾的公共子串长度。 - **状态转移方程**: $$ dp[i][j] = \begin{cases} dp[i-1][j-1] + 1 & \text{若 } A[i] = B[j] \\ 0 & \text{否则} \end{cases} $$ - `dp[i][j]`:以字符串 A 的第 `i` 个字符和字符串 B 的第 `j` 个字符结尾的最长公共子串长度。 - **步骤**: 1. 初始化二维数组 `dp`,大小为 `(len(A)+1) x (len(B)+1)`。 2. 遍历所有字符对 `(i, j)`,根据字符是否相等更新 `dp[i][j]`。 3. 记录最大长度及其结束位置。 4. 根据结束位置和长度截取子字符串。 - **示例**: - A = `"abcde"`, B = `"abcf"` - `dp` 表如下(简化): | | a | b | c | f | |---|---|---|---|---| | a | 1 | 0 | 0 | 0 | | b | 0 | 2 | 0 | 0 | | c | 0 | 0 | 3 | 0 | | d | 0 | 0 | 0 | 0 | | e | 0 | 0 | 0 | 0 | - 最长子串为 `"abc"`(长度 3)。 - **复杂度**: - 时间复杂度:$O(n \cdot m)$(n、m 为两字符串长度)。 - 空间复杂度:$O(n \cdot m)$,可优化至 $O(\min(n, m))$(使用滚动数组)。 ##### **其他方** - **暴力**:枚举所有子串组合并比较,时间复杂度 $O(n^3)$,不实用。 - **后缀树**:构建广义后缀树后查找最深共有节点,时间复杂度 $O(n + m)$,但实现复杂。 --- #### 4. **代码实现(Python示例)** ```python def longest_common_substring(s1, s2): m, n = len(s1), len(s2) dp = [[0]*(n+1) for _ in range(m+1)] max_len = 0 end_pos = 0 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] + 1 if dp[i][j] > max_len: max_len = dp[i][j] end_pos = i # 记录结束位置 else: dp[i][j] = 0 return s1[end_pos - max_len : end_pos] if max_len > 0 else "" ``` --- #### 5. **优化空间复杂度** 使用滚动数组(维数组)优化空间: ```python def longest_common_substring_optimized(s1, s2): if len(s1) > len(s2): s1, s2 = s2, s1 # 确保 s1 是较短的字符串 m, n = len(s1), len(s2) dp = [0] * (m + 1) max_len = 0 end_pos = 0 for j in range(1, n+1): prev = 0 # 保存左上角的值 for i in range(1, m+1): temp = dp[i] if s1[i-1] == s2[j-1]: dp[i] = prev + 1 if dp[i] > max_len: max_len = dp[i] end_pos = i else: dp[i] = 0 prev = temp return s1[end_pos - max_len : end_pos] if max_len > 0 else "" ``` --- #### 6. **总结** - **动态规划**是解决最长公共子字符串问题的标准方,平衡了效率和实现难度。 - 若字符串极长,可研究后缀树等高级算法,但需权衡实现复杂度。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CliuGeek

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值