获得最接近的字符串匹配

时间:2011-05-02 16:20:28

标签: algorithm language-agnostic string-comparison levenshtein-distance

我需要一种方法来将多个字符串与测试字符串进行比较,并返回与其非常相似的字符串:

TEST STRING: THE BROWN FOX JUMPED OVER THE RED COW

CHOICE A   : THE RED COW JUMPED OVER THE GREEN CHICKEN
CHOICE B   : THE RED COW JUMPED OVER THE RED COW
CHOICE C   : THE RED FOX JUMPED OVER THE BROWN COW

(如果我这样做的话)最接近“TEST STRING”的字符串应为“CHOICE C”。最简单的方法是什么?

我计划将其实现为多种语言,包括VB.net,Lua和JavaScript。此时,伪代码是可以接受的。如果您能提供特定语言的示例,我们也不胜感激!

12 个答案:

答案 0 :(得分:919)

答案 1 :(得分:83)

这个问题一直在生物信息学中出现。上面接受的答案(顺便提一下)在生物信息学中被称为Needleman-Wunsch(比较两个字符串)和Smith-Waterman(在较长字符串中找到近似子字符串)算法。他们工作得很好,几十年来一直在努力。

但是,如果你有一百万个字符串需要比较呢?这是一个万亿的成对比较,每个都是O(n * m)!现代DNA测序仪很容易产生十亿个短DNA序列,每个序列长约200个DNA“字母”。通常,我们希望为每个这样的字符串找到与人类基因组最佳匹配(30亿个字母)。显然,Needleman-Wunsch算法及其亲属不会这样做。

这种所谓的“对齐问题”是一个积极研究的领域。最流行的算法目前能够在合理的硬件(例如,8个内核和32 GB RAM)上在几个小时内找到10亿个短字符串和人类基因组之间的不精确匹配。

这些算法中的大多数通过快速找到短精确匹配(种子)然后使用较慢的算法(例如,Smith-Waterman)将它们扩展到完整字符串来工作。这样做的原因是我们真的只对一些接近的比赛感兴趣,所以摆脱99.9%没有共同点的配对是值得的。

如何找到完全匹配有助于找到不精确的匹配?好吧,假设我们只允许查询和目标之间的单一差异。很容易看出,这种差异必须出现在查询的右半部分或左半部分,因此另一半必须完全匹配。这个想法可以扩展到多个不匹配,并且是Illumina DNA测序仪常用的ELAND算法的基础。

有许多非常好的算法可以进行精确的字符串匹配。给定长度为200的查询字符串和长度为30亿的目标字符串(人类基因组),我们希望找到目标中存在长度为k的子字符串的任何位置,该子字符串与查询的子字符串完全匹配。一个简单的方法是从索引目标开始:获取所有k-long子串,将它们放入一个数组中并对它们进行排序。然后获取查询的每个k-long子字符串并搜索已排序的索引。 排序和搜索可以在O(log n)时间内完成。

但存储可能是个问题。一个30亿字母目标的指数需要拥有30亿个指针和30亿个k字长。看起来很难将其安装在不到几十GB的RAM中。但令人惊讶的是,我们可以使用Burrows-Wheeler transform大大压缩索引,并且它仍然可以高效查询。人类基因组的索引可以容纳小于4 GB的RAM。这个想法是流行序列对齐器的基础,例如BowtieBWA

或者,我们可以使用suffix array,它只存储指针,但代表目标字符串中所有后缀的同时索引(实质上是k的所有可能值的同时索引;同样如此) Burrows-Wheeler变换)。如果我们使用32位指针,人类基因组的后缀数组索引将需要12 GB的RAM。

上述链接包含大量信息和主要研究论文的链接。 ELAND链接转到PDF,其中包含有用的数字,说明了所涉及的概念,并展示了如何处理插入和删除。

最后,虽然这些算法基本上解决了(重新)测序单个人类基因组(十亿个短串)的问题,但DNA测序技术的改进速度甚至超过了摩尔定律,我们正在快速接近万亿字母数据集。例如,目前有一些项目正在对10,000 vertebrate species的基因组进行测序,每十亿个字母长一个左右。当然,我们希望在数据上进行成对不精确的字符串匹配...

答案 2 :(得分:28)

我认为选择B更靠近测试字符串,因为它只是原始字符串的4个字符(和2个删除)。而你看C更接近,因为它包括棕色和红色。但是,它会有更大的编辑距离。

有一种名为Levenshtein Distance的算法可以测量两个输入之间的编辑距离。

Here是该算法的工具。

  1. 费率选择A的距离为15.
  2. 选择B的距离为6。
  3. 将选择C作为距离9。
  4. 编辑:对不起,我在levenshtein工具中不断混合字符串。更新以更正答案。

答案 3 :(得分:18)

Lua实施,为子孙后代:

function levenshtein_distance(str1, str2)
    local len1, len2 = #str1, #str2
    local char1, char2, distance = {}, {}, {}
    str1:gsub('.', function (c) table.insert(char1, c) end)
    str2:gsub('.', function (c) table.insert(char2, c) end)
    for i = 0, len1 do distance[i] = {} end
    for i = 0, len1 do distance[i][0] = i end
    for i = 0, len2 do distance[0][i] = i end
    for i = 1, len1 do
        for j = 1, len2 do
            distance[i][j] = math.min(
                distance[i-1][j  ] + 1,
                distance[i  ][j-1] + 1,
                distance[i-1][j-1] + (char1[i] == char2[j] and 0 or 1)
                )
        end
    end
    return distance[len1][len2]
end

答案 4 :(得分:13)

您可能对此博文感兴趣。

http://seatgeek.com/blog/dev/fuzzywuzzy-fuzzy-string-matching-in-python

Fuzzywuzzy是一个Python库,提供简单的距离测量,例如Levenshtein距离,用于字符串匹配。它构建在标准库中的difflib之上,如果可用,将使用C实现Python-levenshtein。

http://pypi.python.org/pypi/python-Levenshtein/

答案 5 :(得分:10)

您可能会发现这个图书馆很有帮助! http://code.google.com/p/google-diff-match-patch/

目前提供Java,JavaScript,Dart,C ++,C#,Objective C,Lua和Python

它也很好用。我在几个Lua项目中使用它。

我认为把它移植到其他语言并不太难!

答案 6 :(得分:2)

如果您是在搜索引擎或数据库前端的上下文中执行此操作,则可以考虑使用Apache Solr这样的工具和ComplexPhraseQueryParser插件。此组合允许您搜索字符串索引,其结果按相关性排序,由Levenshtein距离确定。

当传入的查询可能有一个或多个拼写错误时,我们一直在对大量的艺术家和歌曲集合使用它,并且它工作得很好(并且考虑到这些集合在数百万字符串中,速度非常快)。 / p>

此外,使用Solr,您可以通过JSON按需搜索索引,因此您不必在您正在查看的不同语言之间重新构建解决方案。

答案 7 :(得分:1)

这些算法的非常非常好的资源是Simmetrics:http://sourceforge.net/projects/simmetrics/

不幸的是,包含大量文档的精彩网站已经不见了:( 如果它再次恢复,它以前的地址是这样的: http://www.dcs.shef.ac.uk/~sam/simmetrics.html

Voila(由“Wayback Machine”提供):http://web.archive.org/web/20081230184321/http://www.dcs.shef.ac.uk/~sam/simmetrics.html

你可以研究代码源,有几十种算法可以进行这些比较,每种算法都有不同的权衡。这些实现都是用Java实现的。

答案 8 :(得分:1)

要以有效的方式查询大量文本,您可以使用编辑距离/前缀编辑距离的概念。

  

编辑距离ED(x,y):从术语x到术语y的最小转换数

但是,在每个术语和查询文本之间计算ED是资源和时间密集型的。因此,我们不是首先计算每个项的ED,而是使用称为Qgram Index的技术提取可能的匹配项。然后对这些选定的术语应用ED计算。

Qgram索引技术的一个优点是它支持模糊搜索。

调整QGram索引的一种可能方法是使用Qgrams构建一个倒置索引。在那里,我们存储在Qgram下包含特定Qgram的所有单词。(而不是存储完整字符串,您可以为每个字符串使用唯一ID)。您可以在Java中使用Tree Map数据结构。 以下是存储术语的一个小例子

  

col: col mbia, col ombo,gan col a,ta col ama

然后在查询时,我们计算查询文本和可用术语之间的常见Qgrams数。

Example: x = HILLARY, y = HILARI(query term)
Qgrams
$$HILLARY$$ -> $$H, $HI, HIL, ILL, LLA, LAR, ARY, RY$, Y$$
$$HILARI$$ -> $$H, $HI, HIL, ILA, LAR, ARI, RI$, I$$
number of q-grams in common = 4

共同的q-gram数= 4。

对于具有大量常见Qgrams的条款,我们根据查询字词计算ED / PED,然后向最终用户建议该术语。

你可以在下面的项目中找到这个理论的实现(参见" QGramIndex.java")。随意问任何问题。 https://github.com/Bhashitha-Gamage/City_Search

要了解有关编辑距离,前缀编辑距离Qgram索引的更多信息,请观看Hannah Bast教授https://www.youtube.com/embed/6pUg2wmGJRo教授的以下视频(课程从20:06开始)

答案 9 :(得分:0)

在这里您可以使用golang POC计算给定单词之间的距离。您可以为其他示波器调整minDistancedifference

游乐场:https://play.golang.org/p/NtrBzLdC3rE

package main

import (
    "errors"
    "fmt"
    "log"
    "math"
    "strings"
)

var data string = `THE RED COW JUMPED OVER THE GREEN CHICKEN-THE RED COW JUMPED OVER THE RED COW-THE RED FOX JUMPED OVER THE BROWN COW`

const minDistance float64 = 2
const difference float64 = 1

type word struct {
    data    string
    letters map[rune]int
}

type words struct {
    words []word
}

// Print prettify the data present in word
func (w word) Print() {
    var (
        lenght int
        c      int
        i      int
        key    rune
    )
    fmt.Printf("Data: %s\n", w.data)
    lenght = len(w.letters) - 1
    c = 0
    for key, i = range w.letters {
        fmt.Printf("%s:%d", string(key), i)
        if c != lenght {
            fmt.Printf(" | ")
        }
        c++
    }
    fmt.Printf("\n")
}

func (ws words) fuzzySearch(data string) ([]word, error) {
    var (
        w      word
        err    error
        founds []word
    )
    w, err = initWord(data)
    if err != nil {
        log.Printf("Errors: %s\n", err.Error())
        return nil, err
    }
    // Iterating all the words
    for i := range ws.words {
        letters := ws.words[i].letters
        //
        var similar float64 = 0
        // Iterating the letters of the input data
        for key := range w.letters {
            if val, ok := letters[key]; ok {
                if math.Abs(float64(val-w.letters[key])) <= minDistance {
                    similar += float64(val)
                }
            }
        }

        lenSimilarity := math.Abs(similar - float64(len(data)-strings.Count(data, " ")))
        log.Printf("Comparing %s with %s i've found %f similar letter, with weight %f", data, ws.words[i].data, similar, lenSimilarity)
        if lenSimilarity <= difference {
            founds = append(founds, ws.words[i])
        }
    }

    if len(founds) == 0 {
        return nil, errors.New("no similar found for data: " + data)
    }

    return founds, nil
}

func initWords(data []string) []word {
    var (
        err   error
        words []word
        word  word
    )
    for i := range data {
        word, err = initWord(data[i])
        if err != nil {
            log.Printf("Error in index [%d] for data: %s", i, data[i])
        } else {
            words = append(words, word)
        }
    }
    return words

}

func initWord(data string) (word, error) {
    var word word

    word.data = data
    word.letters = make(map[rune]int)
    for _, r := range data {
        if r != 32 { // avoid to save the whitespace
            word.letters[r]++
        }

    }
    return word, nil
}
func main() {
    var ws words
    words := initWords(strings.Split(data, "-"))
    for i := range words {
        words[i].Print()
    }
    ws.words = words

    solution, _ := ws.fuzzySearch("THE BROWN FOX JUMPED OVER THE RED COW")
    fmt.Println("Possible solutions: ", solution)

}

答案 10 :(得分:0)

使用C# is here的示例。

public static void Main()
{
    Console.WriteLine("Hello World " + LevenshteinDistance("Hello","World"));
    Console.WriteLine("Choice A " + LevenshteinDistance("THE BROWN FOX JUMPED OVER THE RED COW","THE RED COW JUMPED OVER THE GREEN CHICKEN"));
    Console.WriteLine("Choice B " + LevenshteinDistance("THE BROWN FOX JUMPED OVER THE RED COW","THE RED COW JUMPED OVER THE RED COW"));
    Console.WriteLine("Choice C " + LevenshteinDistance("THE BROWN FOX JUMPED OVER THE RED COW","THE RED FOX JUMPED OVER THE BROWN COW"));
}

public static float LevenshteinDistance(string a, string b)
{
    var rowLen = a.Length;
    var colLen = b.Length;
    var maxLen = Math.Max(rowLen, colLen);

    // Step 1
    if (rowLen == 0 || colLen == 0)
    {
        return maxLen;
    }

    /// Create the two vectors
    var v0 = new int[rowLen + 1];
    var v1 = new int[rowLen + 1];

    /// Step 2
    /// Initialize the first vector
    for (var i = 1; i <= rowLen; i++)
    {
        v0[i] = i;
    }

    // Step 3
    /// For each column
    for (var j = 1; j <= colLen; j++)
    {
        /// Set the 0'th element to the column number
        v1[0] = j;

        // Step 4
        /// For each row
        for (var i = 1; i <= rowLen; i++)
        {
            // Step 5
            var cost = (a[i - 1] == b[j - 1]) ? 0 : 1;

            // Step 6
            /// Find minimum
            v1[i] = Math.Min(v0[i] + 1, Math.Min(v1[i - 1] + 1, v0[i - 1] + cost));
        }

        /// Swap the vectors
        var vTmp = v0;
        v0 = v1;
        v1 = vTmp;
    }

    // Step 7
    /// The vectors were swapped one last time at the end of the last loop,
    /// that is why the result is now in v0 rather than in v1
    return v0[rowLen];
}

输出为:

Hello World 4
Choice A 15
Choice B 6
Choice C 8

答案 11 :(得分:0)

我曾经在我们的系统中实施过另一种相似性度量,并且给出了令人满意的结果:-

用例

有一个用户查询需要与一组文档匹配。

算法

  1. 从用户查询中提取关键字(相关的POS标记-名词,专有名词)。
  2. 现在根据以下公式计算分数,以测量用户查询与给定文档之间的相似度。

对于从用户查询中提取的每个关键字:-

  • 开始在文档中搜索给定的单词,然后该单词在文档中的每个后续出现都会减少奖励积分。

本质上,如果第一个关键字在文档中出现4次,则得分将计算为:-

  • 第一次出现将获取“ 1”点。
  • 第二次出现将使计算出的分数增加1/2
  • 第三次出现将使总数增加1/3
  • 第四次出现1/4

总相似度得分= 1 + 1/2 + 1/3 + 1/4 = 2.083

类似地,我们为用户查询中的其他关键字计算它。

最后,总分将代表用户查询与给定文档之间的相似程度。