优化字符串匹配算法

时间:2012-07-09 15:35:00

标签: javascript string algorithm optimization

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拼写检查器制作一个建议单词功能。

有关如何提高速度的任何建议吗?或者采用不同的方式吗?

4 个答案:

答案 0 :(得分:4)

如果您只寻找小于某个阈值的距离,您可以做的一件事是首先比较长度。例如,如果您只希望距离小于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​​(长度 - 长度)测试,因为您知道您只查看可以匹配的长度的单词。这样可以节省您使用大量字典单词所做的任何事情。

Levenshtein相对较贵,而言语较长则更为昂贵。如果简单地说Levenshtein过于昂贵而不能做很多次,特别是用较长的单词,你可以利用你的阈值的另一个副作用,只考虑完全匹配或距离为1的单词(一次插入) ,删除,替换或转置)。根据这个要求,你可以通过检查他们的第一个字符匹配或者他们的最后一个字符是否匹配来进一步筛选Levenshtein计算的候选者(除非任何一个字的长度为1或2,在这种情况下Levensthein应该很便宜)。实际上,您可以检查前n个字符或后n个字符的匹配,其中n =(suggWord.length-1)/ 2。如果他们没有通过该测试,你可以假设他们不会通过Levenshtein匹配。为此,您需要按字母顺序排序字典单词的主要数组,此外,还需要一个索引到该数组中的数组,但按其反转字符按字母顺序排序。然后你可以对这两个数组进行二进制搜索,只需对其开头或结尾的n个字符与suggWord开头或结尾匹配的单词的小子集进行Levenshtein计算,并且其长度相差于最多的一个角色。

答案 1 :(得分:3)

我必须优化相同的算法。对我来说最有效的是缓存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]);}    
}
}

答案 2 :(得分:2)

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

修改

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

阵列

对于上面的单词,如果你想找到Levenshtein距离为1,那么这些例子将是

  • parray,aprray,arpray,arrpay,arrayp(插入一个角色)

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

对于这些词,Levenshtein距离是1

rray,aray,arry(删除一个角色)

最后是这些话:

prray,apray,arpay,arrpy和arrap(替换角色)

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

因此,如果您只查找这些特定组合而不是所有单词,那么您将获得解决方案。如果你知道Levenshtein算法是如何工作的,我们就已经对它进行了逆向工程。

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

如果 pary 是您输入的单词,应该从字典中更正为 part 。因此,对于 pary ,您不需要查看以 ab 开头的单词,例如因为对于以 ab 开头的任何单词,Levenshtein距离将大于1.

答案 3 :(得分:2)

您可以在代码中执行一些简单的操作,以提高执行速度。我完全重写了代码的性能,静态类型符合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/来修饰您的代码,以便我可以在一半的时间内阅读它。我还删除了数组中的一些冗余查找。如果这对你来说执行得更快,请告诉我。