【问题标题】:Faster way match characters between strings than Regex?比正则表达式更快地匹配字符串之间的字符?
【发布时间】:2015-02-04 22:14:20
【问题描述】:

用例是我想将查询字符串与单词数组进行比较,并返回匹配项。匹配是当一个单词包含查询字符串中的所有字符时,顺序无关紧要,重复字符都可以。正则表达式似乎太强大了(只需要锤子的大锤)。我编写了一个解决方案,通过循环遍历字符并使用 indexOf 来比较字符,但它似乎始终较慢。 (http://jsperf.com/indexof-vs-regex-inside-a-loop/10) 正则表达式是此类操作的最快选项吗?有没有办法让我的替代解决方案更快?

 var query = "word",
    words = ['word', 'wwoorrddss', 'words', 'argument', 'sass', 'sword', 'carp', 'drowns'],
    reStoredMatches = [],
    indexOfMatches = [];

  function match(word, query) {
    var len = word.length,
      charMatches = [],
      charMatch,
      char;

    while (len--) {
      char = word[len];
      charMatch = query.indexOf(char);

      if (charMatch !== -1) {
        charMatches.push(char);
      }
    }
    return charMatches.length === query.length;
  }

  function linearIndexOf(words, query) {
    var wordsLen = words.length,
      wordMatch,
      word;

    while (wordsLen--) {
      word = words[wordsLen];

      wordMatch = match(word, query);

      if (wordMatch) {

        indexOfMatches.push(word);
      }
    }
  }

  function linearRegexStored(words, query) {
    var wordsLen = words.length,
      re = new RegExp('[' + query + ']', 'g'),
      match,
      word;

    while (wordsLen--) {
      word = words[wordsLen];
      match = word.match(re);

      if (match !== null) {

        if (match.length >= query.length) {

          reStoredMatches.push(word);
        }
      }
    }
  }

【问题讨论】:

  • 可以向 indexOf() 提供多个字符,这应该比简单的 RegExp 更快:只需使用“return query.indexOf(word)!==-1”
  • 正则表达式在浏览器本机编译代码中运行。也许你的代码更适合你的情况,但 JS 更慢。
  • 请注意,您的代码似乎比 Firefox 上的正则表达式快得多。
  • 我可能有一个想法,但我需要更精确。单词只能由字母组成吗?如果不是,是否可以将可能的字符减少到小于“所有字符”的组(例如“字母+数字”或“ascii 字符”或“非空格”......)?另一个问题,是预先固定的单词列表,从某种意义上说,它从脚本的开头到结尾永远不会改变?最后,你知道你的单词列表的大小(在现实世界中)吗?
  • 我的回答有什么遗漏吗?您尚未评论、投票或接受它,但它似乎回答了您的问题。

标签: javascript regex performance


【解决方案1】:

请注意,您的正则表达式是错误的,这肯定是它运行速度如此之快的原因

现在,如果您的查询是“单词”(如您的示例中所示),则正则表达式将是:

/[word]/g

这意味着查找以下字符之一:“w”、“o”、“r”或“d”。如果匹配,则 match() 返回 true。完毕。绝对比最正确的 indexOf() 快得多。 (即,在简单的 match() 调用的情况下,“g”标志将被忽略,因为如果任何一件事匹配,该函数将返回 true。)

另外,你提到了任意数量的字符的想法/概念,我想如下所示:

'word', 'wwoorrddss'

如果您真的是指每个字符的“任意数字”,那么 indexOf() 肯定无法正确捕捉到这一点。因为您应该匹配无数个案例。像这样的正则表达式:

/w+o+r+d+s+/g

您肯定很难用纯 JavaScript 编写正确的代码,而不是使用正则表达式。但是,无论哪种方式,这都会有点慢。


从下面的评论中,单词的所有字母都是必需的,为了做到这一点,你必须有 3 个!对 3 个字母的单词进行测试(3 个阶乘):

/(a.*b.*c)|(a.*c.*b)|(b.*a.*c)|(b.*c.*a)|(c.*a.*b)|(c.*b.*a)/

显然,阶乘会很快增加您的可能性数量并在超长的正则表达式中消除您的记忆(尽管如果一个单词多次使用相同的字母,您可以简化,但您不必测试该字母不止一次)。

1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
...

这可能就是为什么用纯 JavaScript 正确编写的测试要慢得多

此外,在您的情况下,您应该几乎像在 Scrabble 词典中那样编写单词:所有字母按字母顺序一次(Scrabble 保持重复)。所以“word”这个词就是“dorw”。正如您在示例中所示,“wwoorrddss”一词将是“dorsw”。你可以有一些后端工具来生成你的单词表(所以你仍然把它们写成“word”和“words”,你的工具会对它们进行按摩并将它们转换为“dorw”和“dorsw”。)然后你可以对您按字母顺序测试的单词的字母,结果是您不需要正则表达式的愚蠢阶乘,您可以简单地这样做:

/d.*o.*r.*w/

这将匹配任何包含单词“word”的单词,例如“password”。

对字母进行排序的一种简单方法是将单词拆分为一个字母数组,然后对该数组进行排序。您可能仍然会得到重复,这将取决于排序功能。 (我不认为默认的 JavaScript 排序会自动删除重复项。)


更详细一点,如果您的测试应该不区分大小写,那么您希望在运行测试之前将您的字符串转换为小写。所以像:

query = query.toLowerCase();

在你的顶级职能的早期。

【讨论】:

  • 幸运的是,我不需要为每个字符匹配“任何数字”。 indexOf() 只需要检查至少查询字符串的所有字符都在被检查的单词中。因此,以“单词”作为查询,swordds 是匹配项,而 rrows 不是。事实上,我可能应该忽略重复项并在满足最小值后立即中断循环,例如:jsbin.com/benifi/1/edit?js,console。我的正则表达式可能是错误的,但它似乎确实返回了相同的结果 (jsbin.com/huquza/1/edit?js,console)。我不完全理解为什么。可能是不使用它的原因。
  • 所有字母的顺序?那是一个完全不同的正则表达式...... 8-) 我会做一些更新给你看。
【解决方案2】:

您正在尝试加快算法“word 中的字符是查询字符的子集”。您可以将此检查短路并避免一些分配(更具可读性但并非严格需要)。尝试以下版本的匹配

  function match(word, query) {
    var len = word.length;
    while (len--) {
      if (query.indexOf(word[len]) === -1) { // found a missing char
        return false;
      }
    }
    return true; // couldn't find any missing chars
  }

This gives a 4-5X improvement

根据应用程序,您可以尝试对单词进行预排序并将单词中的每个单词预排序作为另一种优化。

【讨论】:

  • 抱歉,这个周末我需要更长的时间来互动。这确实更快,但不幸的是它不会给出所需的结果。 (jsbin.com/kewuqisino/1/edit?js,console) 此处的匹配还包括具有不在查询字符串中的字符的单词,只要该单词包含查询字符串中的所有字符。抱歉,如果我的解释不清楚。我会尽快更新问题,使其更加明确。
  • 第 2 部分:如果 word 是查询,则匹配将是“word”和“swords”。您的解决方案更快,因为它会在找到不匹配的字符后立即退出。再次抱歉,如果我的问题混淆了。
【解决方案3】:

正则表达式匹配算法构造一个有限状态自动机,并根据当前状态和从左到右读取的字符做出决定。这涉及读取每个字符一次并做出决定。

对于静态字符串(在几个文本上查看固定字符串),你有更好的算法,比如 Knuth-Morris,它可以让你一次比一个字符更快,但是你必须理解这个算法 不是用于匹配正则表达式,只是普通字符串。

如果您对 Knuth-Morris(还有其他几种算法)感兴趣,请在 wikipedia 上了解一下。 http://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm

您可以做的一件好事是调查您是否使用 DFA 或 NDFA 进行正则表达式匹配例程,因为 NDFA 占用的内存更少且更易于计算,但 DFA 执行速度更快,但会产生一些编译损失等等内存占用。

Knuth-Morris 算法在工作之前还需要将字符串编译成自动机,所以如果你只是在某个字符串中查找一个单词,它可能不适用于你的问题。

【讨论】:

    猜你喜欢
    • 2020-10-19
    • 2010-11-19
    • 2011-03-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-26
    • 1970-01-01
    相关资源
    最近更新 更多