如何优化levenshtein距离以检查距离为1?

时间:2016-06-19 05:20:31

标签: javascript algorithm optimization time-complexity levenshtein-distance

我正在开发一款游戏,我只需要检查两个单词之间是否有0或1的距离,如果是这样,则返回true。我找到了一个通用的levenshtein距离算法:

function levenshtein(s, t) {
  if (s === t) { return 0; }
  var n = s.length, m = t.length;
  if (n === 0 || m === 0) { return n + m; }
  var x = 0, y, a, b, c, d, g, h, k;
  var p = new Array(n);
  for (y = 0; y < n;) { p[y] = ++y; }
  for (;
    (x + 3) < m; x += 4) {
    var e1 = t.charCodeAt(x);
    var e2 = t.charCodeAt(x + 1);
    var e3 = t.charCodeAt(x + 2);
    var e4 = t.charCodeAt(x + 3);
    c = x; b = x + 1; d = x + 2; g = x + 3; h = x + 4;

    for (y = 0; y < n; y++) {
      k = s.charCodeAt(y);
      a = p[y];

      if (a < c || b < c) { c = (a > b ? b + 1 : a + 1); }
      else { if (e1 !== k) { c++; } }

      if (c < b || d < b) { b = (c > d ? d + 1 : c + 1); } 
      else { if (e2 !== k) { b++; } }

      if (b < d || g < d) { d = (b > g ? g + 1 : b + 1); } 
      else { if (e3 !== k) { d++; } }

      if (d < g || h < g) { g = (d > h ? h + 1 : d + 1); }
      else { if (e4 !== k) { g++; } }

      p[y] = h = g; g = d; d = b; b = c; c = a;
    }
  }

  for (; x < m;) {
    var e = t.charCodeAt(x);
    c = x;
    d = ++x;
    for (y = 0; y < n; y++) {
      a = p[y];
      if (a < c || d < c) { d = (a > d ? d + 1 : a + 1); } 
      else {
        if (e !== s.charCodeAt(y)) { d = c + 1; }
        else { d = c; }
      }
      p[y] = d;
      c = a;
    }
    h = d;
  }

  return h;
}

哪个有效,但是这个地方将成为一个热点并且可能每秒运行数十万次,我想优化它,因为我不需要通用算法,只需检查是否存在距离为0或1。

我尝试写它并想出了这个:

function closeGuess(guess, word) {
  if (Math.abs(word.length - guess.length) > 1) { return false; }

  var errors = 0, guessIndex = 0, wordIndex = 0;

  while (guessIndex < guess.length || wordIndex < word.length) {
    if (errors > 1) { return false; }
    if (guess[guessIndex] !== word[wordIndex]) {
      if (guess.length < word.length) { wordIndex++; }
      else { guessIndex++; }
      errors++;
    } else {
      wordIndex++;
      guessIndex++;
    }
  }

  return true;
}

但是在分析之后我发现我的代码慢了两倍,这让我感到惊讶,因为我认为通用算法是O(n * m),我认为我的代码是O(n)。

我一直在测试这个小提琴的性能差异:https://jsfiddle.net/aubtze2L/3/

我可以使用更好的算法,还是可以更快地优化代码?

3 个答案:

答案 0 :(得分:2)

我没有看到一种更优雅的方式,它同时比旧的for-loop更快:

function lev01(a, b) {
  let la = a.length;
  let lb = b.length;
  let d = 0;
  switch (la - lb) {
    case 0: // mutation
      for (let i = 0; i < la; ++i) {
        if (a.charAt(i) != b.charAt(i) && ++d > 1) {
          return false;
        }
      }
      return true;
    case -1: // insertion
      for (let i = 0; i < la + d; ++i) {
        if (a.charAt(i - d) != b.charAt(i) && ++d > 1) {
          return false;
        }
      }
      return true;
    case +1: // deletion
      for (let i = 0; i < lb + d; ++i) {
        if (a.charAt(i) != b.charAt(i - d) && ++d > 1) {
          return false;
        }
      }
      return true;
  }
  return false;
}

console.log(lev01("abc", "abc"));
console.log(lev01("abc", "abd"));
console.log(lev01("abc", "ab"));
console.log(lev01("abc", "abcd"));
console.log(lev01("abc", "cba"));

效果比较(Chrome):

  • 80.33ms - lev01(这个答案)
  • 234.84ms - lev
  • 708.12ms - 关闭

答案 1 :(得分:1)

考虑以下情况:

  1. 如果术语长度的差异大于1,那么 他们之间的Levenshtein距离将大于1。
  2. 如果长度差异恰好为1,那么最短的字符串必须等于最长的字符串,只有一个删除(或插入)。
  3. 如果字符串长度相同,那么你应该这样做 考虑汉明距离的修改版本,返回false 如果找到两个不同的字符:
  4. 以下是一个示例实现:

    var areSimilar;
    
    areSimilar = function(guess, word) {
      var charIndex, foundDiff, guessLength, lengthDiff, substring, wordLength, shortest, longest, shortestLength, offset;
    
      guessLength = guess.length;
      wordLength = word.length;
    
      lengthDiff = guessLength - wordLength;
      if (lengthDiff < -1 || lengthDiff > 1) {
        return false;
      }
    
      if (lengthDiff !== 0) {
        if (guessLength < wordLength) {
          shortest = guess;
          longest = word;
          shortestLength = guessLength;
        } else {
            shortest = word;
          longest = guess;
          shortestLength = wordLength;
        }
    
        offset = 0;
        for (charIndex = 0; charIndex < shortestLength; charIndex += 1) {
            if (shortest[charIndex] !== longest[offset + charIndex]) {
            if (offset > 0) {
                return false; // second error
            }
            offset = 1;
            if (shortest[charIndex] !== longest[offset + charIndex]) {
                return false; // second error
            }
          }
        }
    
        return true; // only one error
      }
    
      foundDiff = false;
      for (charIndex = 0; charIndex < guessLength; charIndex += 1) {
        if (guess[charIndex] !== word[charIndex]) {
          if (foundDiff) {
            return false;
          }
          foundDiff = true;
        }
      }
      return true;
    };
    

    我已经更新了你的小提琴以包含这种方法。以下是我机器上的结果:

    close: 154.61
    lev: 176.72500000000002
    sim: 32.48000000000013
    

    小提琴:https://jsfiddle.net/dylon/aubtze2L/11/

答案 2 :(得分:0)

如果你知道你正在寻找距离0和1,那么通用DP算法就没有意义了(而且你所展示的算法看起来很复杂,请看一个更好的解释here )。

要检查距离是否为0,您只需要检查2个字符串是否相同。现在,如果距离为1,则意味着应该发生插入,删除或替换。因此,从原始字符串生成所有可能的删除,并检查它是否等于第二个字符串。所以你会得到这样的东西:

for (var i = 0; i < s_1.length; i++) {
  if s_2 == s_1.slice(0, i) + s_1.slice(i + 1) {
     return true
  }
}

对于插入和替换,您需要知道所有字符的字母。您可以将其定义为大字符串var alphabet = "abcde...."。现在你做了类似的事情,但是当你引入替换或插入时,你也会遍历字母表中的所有元素。我不打算在这里写完整个代码。

其他一些事情。你可以在这里进行大量的微观优化。例如,如果两个字符串的长度相差超过1,则它们显然不能有距离1.另一个字符串与字符串中基础字符的频率有关。