.NET中的算法检测简单的拼写错误

时间:2011-08-19 18:57:40

标签: .net algorithm spell-checking

是否有现成的.NET算法可以从预定义词列表中检测拼写错误?

例如,假设单词“Stuff”在我的列表中,有人输入“Stuf”,“sutff”或“stff”或“stiff”。我希望能够告诉那个人“Stuff”这个词可能是正确的词。

我不是在谈论任何语法或任何超过一个字母缺失,替代或混合的东西。

我的目标是防止在不同类型的列表中输入相同的单词。不是大写和小写不会对我造成问题,因为一切都是小写的。

4 个答案:

答案 0 :(得分:7)

这是一个经过充分研究的问题,有很多很好的算法可以做到这一点。他们中的大多数都是通过构建某种数据结构来处理所有法律词汇,以便能够有效地找到具有相似 edit distance 的词。非正式地,两个字符串之间的编辑距离是将一个字符串转换为另一个字符串所需的更改次数。例如,给定单词“拼写错误”和“拼错”,编辑距离为1(只是在单词中插入另一个's),而“cat”和“dog”之间的编辑距离为3(替换每个字母)

拼写错误的单词可能距离预期的单词只有很小的编辑距离,因此如果您可以以任何方式存储单词,对于任何字符串,查询距离目标较小的编辑距离的单词。 string,您可以提供用户可能意味着的可能单词的候选列表。

用于保存此数据的一种常见数据结构是trie,一种26路树结构,其中每个节点存储一个字的前缀,每个边对应于向当前前缀附加一些字母。如果您有这样的结构,您可以使用简单的递归树搜索找到与特定单词(可能是某个编辑距离之外)“接近”的单词。在每个点上,跟踪您希望远离目标单词的编辑距离的距离以及到目前为止已处理的拼写错误单词的数量。在每个点上,您可以跟随对应于单词中字母的trie中的边缘,或者您可以使用一个编辑距离通过跟随trie中的不同边缘插入新字母。

另一个经常用于此的数据结构是BK-tree,它以一种方式存储字符串,您可以有效地查询距离某些源字符串一定编辑距离的所有单词。这可以更直接地解决您的问题,尽管与尝试相比,如何构建BK树的在线资源更少。

一旦找到了某个编辑距离内的单词,您可能希望在将它们呈现给用户之前以某种方式对它们进行排名。这需要您了解人们在实践中倾向于做出什么样的拼写错误。常见错别字包括

  • 转置,其中交换了两个字母:“thme”而不是“他们”
  • 替换,其中使用了错误的字母:“算术”代替“算术”
  • 删除,遗漏了一封信:“helo”而不是“hello”
  • 重复,信件重复:“tommorrow”而不是“明天”

要构建一个好的拼写检查程序,理想情况下你会遇到与每种类型的错误相关的某种概率。这样,当你有可能的更正列表时,你可以将它们从最可能的到最不可能的排名。

希望这有帮助!

答案 1 :(得分:5)

这是一个很好的一步一步做你想要在python中实现的东西,但也有链接到C#和其他语言的实现。

http://norvig.com/spell-correct.html

答案 2 :(得分:1)

也许你想寻找一个三元组搜索?三字母搜索需要创建输入的3个字母的每个可能性,并在匹配中查找类似的字符串。

答案 3 :(得分:0)

用C#发布我的实现,允许长度大于等于2的字符串。 它检查2个单词是否匹配,而忽略换位替换删除(对于删除后长度> = 2的单词有效)和重复(允许多个)。

public bool CheckWordsSameWithTypo(string word1, string word2)
    {
        if (word1.Length < 2 || word2.Length < 2) return false;
        
        //transposition "thme" instead of "them"        
        bool matchWithTrans = false;
        #region transLogic
        var transOptions1 = new List<string>();
        var transOptions2 = new List<string>();
        for (int i = 1; i < word1.Length;  i++)
        {
            var wordArr = word1.ToArray();
            char letter1 = wordArr[i -1];
            char letter2 = wordArr[i];
            wordArr[i -1] = letter2;
            wordArr[i] = letter1;
            transOptions1.Add(new string(wordArr));
        }
        for (int i = 1; i < word2.Length;  i++)
        {
            var wordArr = word2.ToArray();
            char letter1 = wordArr[i -1];
            char letter2 = wordArr[i];
            wordArr[i -1] = letter2;
            wordArr[i] = letter1;
            transOptions2.Add(new string(wordArr));
        }
        if (transOptions1.Any(p => p.Equals(word2)) || transOptions2.Any(p => p.Equals(word1))) matchWithTrans = true;
        #endregion
        
        //substitution "arithmatic" instead of "arithmetic"
        bool matchWithSubst = false;
        #region substLogic
        var substOptionPatterns1 = new List<string>();
        var substOptionPatterns2 = new List<string>();
        for (int i = 0; i < word1.Length;  i++)
        {
            var wordArr = word1.ToArray();
            wordArr[i] = '.';
            substOptionPatterns1.Add(new string(wordArr));
        }
        for (int i = 0; i < word2.Length;  i++)
        {
            var wordArr = word2.ToArray();
            wordArr[i] = '.';
            substOptionPatterns2.Add(new string(wordArr));
        }
        foreach(var patt in substOptionPatterns1)
        {
            Regex regex = new Regex(patt);
            if (regex.IsMatch(word2)) matchWithSubst = true;
        }
        foreach(var patt in substOptionPatterns2)
        {
            Regex regex = new Regex(patt);
            if (regex.IsMatch(word1)) matchWithSubst = true;
        }
        #endregion
        
        //deletion "helo" instead of "hello"
        bool matchWithDeletion = false;
        #region deletionLogic
        var delOptions1 = new List<string>();
        var delOptions2 = new List<string>();
        for (int i = 0; i < word1.Length;  i++)
        {
            delOptions1.Add(word1.Remove(i, 1));
        }
        for (int i = 0; i < word2.Length;  i++)
        {
            delOptions2.Add(word2.Remove(i, 1));
        }
        if (delOptions1.Any(p => p.Length>1 && p.Equals(word2)) || delOptions2.Any(p => p.Length>1 && p.Equals(word1))) matchWithDeletion = true;
        #endregion
        
        //repetition "tommorrow" instead of "tomorow"
        bool matchWithRepetition = false;
        #region repsLogic
        StringBuilder word1_distinctBuilder = new StringBuilder(word1.Substring(0, 1));
        for (int i = 1; i < word1.Length;  i++)
        {
            string currentLetter = word1.Substring(i, 1);
            if(!word1_distinctBuilder.ToString().Substring(word1_distinctBuilder.ToString().Length-1, 1).Equals(currentLetter))
            {
                word1_distinctBuilder.Append(currentLetter);
            }
        }
        StringBuilder word2_distinctBuilder = new StringBuilder(word2.Substring(0, 1));
        for (int i = 1; i < word2.Length;  i++)
        {
            string currentLetter = word2.Substring(i, 1);
            if(!word2_distinctBuilder.ToString().Substring(word2_distinctBuilder.ToString().Length-1, 1).Equals(currentLetter))
            {
                word2_distinctBuilder.Append(currentLetter);
            }
        }
        matchWithRepetition = word1_distinctBuilder.ToString().Equals(word2_distinctBuilder.ToString());
        #endregion
        return matchWithTrans || matchWithSubst || matchWithDeletion || matchWithRepetition;
    }