【问题标题】:Optimizing String Matching Algorithm优化字符串匹配算法
【发布时间】:2012-07-09 15:35:00
【问题描述】:
function levenshtein(a, b) {
  var i,j,cost,d=[];

  if (a.length == 0) {return b.length;}
  if (b.length == 0) {return a.length;}

  for ( i = 0; i <= a.length; i++) {
    d[i] = new Array();
    d[ i ][0] = i;
  }

  for ( j = 0; j <= b.length; j++) {
    d[ 0 ][j] = j;
  }

  for ( i = 1; i <= a.length; i++) {
    for ( j = 1; j <= b.length; j++) {
      if (a.charAt(i - 1) == b.charAt(j - 1)) {
        cost = 0;
      } else {
        cost = 1;
      }

      d[ i ][j] = Math.min(d[ i - 1 ][j] + 1, d[ i ][j - 1] + 1, d[ i - 1 ][j - 1] + cost);

      if (i > 1 && j > 1 && a.charAt(i - 1) == b.charAt(j - 2) && a.charAt(i - 2) == b.charAt(j - 1)) {
        d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost)
      }
    }
  }

  return d[ a.length ][b.length];
}

function suggests(suggWord) {
  var sArray = [];
  for(var z = words.length;--z;) {
    if(levenshtein(words[z],suggWord) < 2) { 
      sArray.push(words[z]);
    }   
  }
}

你好。

我正在使用 Damerau-Levenshtein 算法的上述实现。它在普通 PC 浏览器上足够快,但在平板电脑上需要大约 2/3 秒。

基本上,我会将发送到建议函数的单词与字典中的每个单词进行比较,如果距离小于 2,则将其添加到我的数组中。

dic 是一个大小约为 600,000 (699KB) 的单词数组 这样做的目的是为我的 Javascript 拼写检查器提供建议词功能。

关于如何加快速度的任何建议?还是另一种方式?

【问题讨论】:

    标签: javascript string algorithm optimization


    【解决方案1】:

    如果您只寻找小于某个阈值的距离,您可以做的一件事是首先比较长度。例如,如果您只希望距离小于 2,那么两个字符串长度之差的绝对值也必须小于 2。这样做通常可以让您避免进行更昂贵的 Levenshtein 计算。

    这背后的原因是,长度相差 2 的两个字符串将需要至少两次插入(因此得到的最小距离为 2)。

    您可以如下修改您的代码:

    function suggests(suggWord) {
      var sArray = [];
      for(var z = words.length;--z;) {
        if(Math.abs(suggWord.length - words[z].length) < 2) {
          if (levenshtein(words[z],suggWord) < 2) { 
            sArray.push(words[z]);
          }
        }   
      }
    }
    

    我不会做很多 javascript,但我认为这是你可以做到的。

    部分问题在于您有大量的字典单词,并且至少对其中的每一个单词都进行了一些处理。一个想法是为每个不同的单词长度设置一个单独的数组,并将您的字典单词组织到其中而不是一个大数组中(或者,如果您必须有一个大数组,用于 alpha 查找或其他什么,那么使用索引数组进入那个大阵列)。然后,如果您有一个 5 个字符长的 suggWord,您只需查看 4、5 和 6 个字母单词的数组。然后,您可以在上面的代码中删除 Match.Abs​​(length-length) 测试,因为您知道您只查看可以匹配的长度的单词。这样您就不必对字典中的大量单词进行任何处理。

    Levenshtein 相对昂贵,对于较长的单词更是如此。如果仅仅是 Levenshtein 太昂贵而无法多次执行的情况,尤其是对于较长的单词,您可以利用阈值的另一个副作用,即仅考虑完全匹配或距离为 1 的单词(一次插入、删除、替换或转座)。鉴于该要求,您可以通过检查它们的第一个字符匹配或最后一个字符匹配(除非任一单词的长度为 1 或 2,在这种情况下,Levenshtein 应该很便宜)来进一步过滤 Levenshtein 计算的候选者。事实上,您可以检查前 n 个字符或后 n 个字符的匹配,其中 n = (suggWord.length-1)/2。如果他们没有通过该测试,您可以假设他们不会通过 Levenshtein 匹配。为此,您需要按字母顺序排列的字典单词的主要数组,以及该数组的索引数组,但按字母顺序按字母顺序排列。然后你可以对这两个数组进行二进制搜索,并且只需要对它们的开始或结束的 n 个字符与 suggWord 开始或结束匹配的单词的小子集进行 Levenshtein 计算,并且长度不同最多一个字符。

    【讨论】:

    • 好主意!有没有比较长度和距离的javascript函数?
    • 距离算法有什么替代方法,因为它仍然会杀死剧本?
    • @user983969 有替代方案,但我已经编辑以包含一些其他可以进一步提高性能的想法,但仍然使用 Levensthein。对于替代方案,您可能希望将其作为一个单独的问题发布(即,针对大型单词列表进行快速模糊匹配的想法?使用 javascript 标签)
    【解决方案2】:

    我必须优化相同的算法。对我来说最有效的是缓存 d 数组..您在 levenshtein 函数之外以大尺寸(您期望的字符串的最大长度)创建它,因此每次调用该函数时都不必重新初始化它.

    在我的例子中,在 Ruby 中,它在性能上产生了巨大的差异。但当然这取决于您的 words 数组的大小...

    function levenshtein(a, b, d) {
    
    var i,j,cost;
    
    if (a.length == 0) {return b.length;}
    if (b.length == 0) {return a.length;}
    
    for ( i = 1; i <= a.length; i++) {
    
        for ( j = 1; j <= b.length; j++) {
    
            if (a.charAt(i - 1) == b.charAt(j - 1)) {
    
                cost = 0;
    
            } else {
    
                cost = 1;
    
            }
    
            d[ i ][j] = Math.min(d[ i - 1 ][j] + 1, d[ i ][j - 1] + 1, d[ i - 1 ][j - 1] + cost);
    
            if (i > 1 && j > 1 && a.charAt(i - 1) == b.charAt(j - 2) && a.charAt(i - 2) == b.charAt(j - 1)) {
    
                d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost)
    
            }
    
        }
    
    }
    
    return d[ a.length ][b.length];
    
    }
    
    function suggests(suggWord)
    {
    d = [];
    for ( i = 0; i <= 999; i++) {
    
        d[i] = new Array();
    
        d[ i ][0] = i;
    
    }
    for ( j = 0; j <= 999; j++) {
    
        d[ 0 ][j] = j;
    
    }
    
    
    var sArray = [];
    for(var z = words.length;--z;)
    {
            if(levenshtein(words[z],suggWord, d) < 2)
            {sArray.push(words[z]);}    
    }
    }
    

    【讨论】:

    • 您能否提供一个 JS 示例,因为我现在正在研究您所说的内容,但不确定它的正确性如何
    【解决方案3】:

    您可以在代码中做一些简单的事情来从根本上提高执行速度。我完全重写了您的代码以提高性能、符合 JIT 解释的静态类型以及符合 JSLint:

    var levenshtein = function (a, b) {
            "use strict";
            var i = 0,
                j = 0,
                cost = 1,
                d = [],
                x = a.length,
                y = b.length,
                ai = "",
                bj = "",
                xx = x + 1,
                yy = y + 1;
            if (x === 0) {
                return y;
            }
            if (y === 0) {
                return x;
            }
            for (i = 0; i < xx; i += 1) {
                d[i] = [];
                d[i][0] = i;
            }
            for (j = 0; j < yy; j += 1) {
                d[0][j] = j;
            }
            for (i = 1; i < xx; i += 1) {
                for (j = 1; j < yy; j += 1) {
                    ai = a.charAt(i - 1);
                    bj = b.charAt(j - 1);
                    if (ai === bj) {
                        cost = 0;
                    } else {
                        cost = 1;
                    }
                    d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
                    if (i > 1 && j > 1 && ai === b.charAt(j - 2) && a.charAt(i - 2) === bj) {
                        d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost);
                    }
                }
            }
            return d[x][y];
        };
    

    在多维查找的每个间隔查找数组的长度是非常昂贵的。我还使用http://prettydiff.com/ 美化了您的代码,这样我就可以用一半的时间阅读它。我还删除了数组中的一些冗余查找。如果这对您来说执行得更快,请告诉我。

    【讨论】:

      【解决方案4】:

      您应该将所有单词存储在trie 中。与存储单词的字典相比,这是节省空间的。匹配单词的算法是遍历trie(标记单词的结尾)并找到单词。

      编辑

      就像我在评论中提到的那样。对于 0 或 1 的 Levenshtein 距离,您不需要遍历所有单词。如果两个词相等,则它们的 Levenshtein 距离为 0。现在问题归结为预测给定单词的 Levenshtein 距离为 1 的所有单词。举个例子:

      数组

      对于上面的单词,如果你想找到Levenshtein距离为1,示例将是

      • parray, aprray, arpray, arrpay, arrayp(插入字符)

      这里的p可以用任何其他字母代替。

      同样对于这些词,Levenshtein 距离是 1

      rray, aray, arry(删除一个字符)

      最后是这些话:

      pray、apray、arpay、arrpy 和 arrap(字符替换)

      同样,p 可以替换为任何其他字母。

      因此,如果您只查找这些特定组合而不是所有单词,您将找到您的解决方案。如果您知道 Levenshtein 算法的工作原理,我们已经对其进行了逆向工程。

      最后一个例子是你的用例:

      如果 pary 是您作为输入获得的单词,并且应该从字典中将其更正为 part。所以对于 pary 你不需要查看以 ab 开头的单词,例如因为对于任何以 ab 开头的单词,Levenshtein 距离都会大于 1。

      【讨论】:

      • 我不确定你可以结合 Trie 和 Levenshtein
      • 如何将我的数组转换为 trie?我可以把它和 Levenshtein 结合起来吗?
      • 不是将单词存储在数组中,而是将其存储在 trie 中。您可以查看如何实现 trie。这是1优化。但我也认为,对于小于 2 的 Levenshtein 距离,您不一定要遍历字典中的所有单词。
      • Levenshtein-Damerau 还处理转置,这增加了可能的匹配。
      猜你喜欢
      • 1970-01-01
      • 2013-12-24
      • 2015-02-13
      • 2016-07-19
      • 2021-11-06
      • 2012-07-24
      • 2010-09-08
      • 2013-07-02
      • 1970-01-01
      相关资源
      最近更新 更多