【问题标题】:Fastest way to test for a minimum number of lines or tokens测试最少行数或标记的最快方法
【发布时间】:2016-09-18 04:36:14
【问题描述】:

我现在有两次发现自己想知道 Javascript 字符串是否有最少的行数,但又不想浪费地拆分字符串来找出答案。两次都对正则表达式进行了过多的实验,以意识到解决方案很简单。这篇自我回答的帖子是为了防止我(希望其他人)不得不重新解决这个问题。

更一般地说,我想有效地确定任何给定的字符串是否至少具有指定数量的标记。我不需要确切知道字符串有多少个标记。标记可以是字符、子字符串、匹配正则表达式的子字符串或分隔单元,例如单词或行。

Another SO question 探讨了拆分字符串或进行全局正则表达式匹配是否更快,以便计算字符串中的行数。据报道,拆分速度更快,至少在内存充足的情况下。我们这里的问题是,如果我们只需要知道令牌的数量是否等于或超过最小值,在一般情况下,我们是否可以比字符串拆分更快地对正则表达式进行测试?

以下是我在尝试匹配最少行数时所犯的一些错误——在这种情况下至少为 42 行:

/(^[\n]*){42}/m.test(stringToTest)
/(\n[^\n]*|[^\n]*){42}/.test(stringToTest)
/(\n[^\n]*|[^\n]*(?!\n)){42}/.test(stringToTest)

这些表达式显然很乐意匹配 42 次。他们为 stringToTest = '' 返回 true。

【问题讨论】:

    标签: javascript regex string count lines


    【解决方案1】:

    解决方案是测试一系列标记/非标记单元,而不是尝试测试分隔单元的正确计数或标记的正确计数。如果标记是分隔符并且您需要最小数量的分隔单元,则要求标记/非标记单元的计数等于所需的单元数少一。正如我们将看到的,这个解决方案具有惊人的性能。

    最少行数

    此函数检查最少行数,其中\n 分隔行而不是严格结束行,允许最后一行为空:

    function hasMinLineCount(text, minLineCount) {
        if (minLineCount <= 1)
            return true; // always 1+ lines, though perhaps empty
        var r = new RegExp('([^\n]*\n){' + (minLineCount-1) + '}');
        return r.test(text);
    }
    

    或者,\n 可以被假定为结束行,而不是纯粹界定它们,对非空的最后一行进行例外处理。例如,"apple\npear\n" 将是两行,而"apple\npear\ngrape" 将是三行。以下函数以这种方式计算行数:

    function hasMinLineCount(text, minLineCount) {
        var r = new RegExp('([^\n]*\n|[^\n]+$){' + minLineCount + '}');
        return r.test(text);
    }
    

    字符串分隔符和标记

    更一般地,对于由字符串分隔符分隔的任何单位:

    var _ = require('lodash');
    
    function hasMinUnitCount(text, minUnitCount, unitDelim) {
        if (minUnitCount <= 1)
            return true; // always 1+ units, though perhaps empty
        var escDelim = _.escapeRegExp(unitDelim);
        var r = new RegExp('(.*?'+ escDelim +'){' + (minUnitCount-1) + '}');
        return r.test(text);
    }
    

    我们还可以测试是否存在最少数量的字符串标记:

    var _ = require('lodash');
    
    function hasMinTokenCount(text, minTokenCount, token) {
        var escToken = _.escapeRegExp(token);
        var r = new RegExp('(.*?'+ escToken +'){' + minTokenCount + '}');
        return r.test(text);
    }
    

    正则表达式分隔符和标记

    我们可以通过允许单位分隔符和标记包含正则表达式字符来进一步概括。只需确保分隔符或标记可以明确地背靠背出现。示例正则表达式分隔符包括"&lt;br */&gt;""[|,]"。这些是字符串,而不是 RegExp 对象。

    function hasMinUnitCount(text, minUnitCount, unitDelimRegexStr) {
        if (minUnitCount <= 1)
            return true; // always 1+ units, though perhaps empty
        var r = new RegExp(
                  '(.*?'+ unitDelimRegexStr +'){' + (minUnitCount-1) + '}');
        return r.test(text);
    }
    
    function hasMinTokenCount(text, minTokenCount, tokenRegexStr) {
        var r = new RegExp('(.*?'+ tokenRegexStr +'){' + minTokenCount + '}');
        return r.test(text);
    }
    

    计算成本

    泛型函数之所以有效,是因为它们的正则表达式对字符进行非贪婪匹配(注意.*?),直到下一个分隔符或标记。这是一个计算成本高昂的前瞻和回溯过程,因此相对于更硬编码的表达式(例如上面的 hasMinLineCount() 中的表达式)而言,这些会降低性能。

    让我们重新审视最初的问题,即我们是否可以胜过使用正则表达式测试拆分字符串。回想一下,我们唯一的目标是测试最少的行数。我使用benchmark.js 进行测试,假设我们知道需要多行。代码如下:

    var Benchmark = require('benchmark');
    var suite = new Benchmark.Suite;
    
    var line = "Go faster faster faster!\n";
    var text = line.repeat(100);
    var MIN_LINE_COUNT = 50;
    var preBuiltBackingRegex = new RegExp('(.*?\n){'+ MIN_LINE_COUNT +'}');
    var preBuiltNoBackRegex = new RegExp('([^\n]*\n){'+ MIN_LINE_COUNT +'}');
    
    suite.add('split string', function() {
        if (text.split("\n").length >= MIN_LINE_COUNT)
            'has minimum lines';
    })
    .add('backtracking on-the-fly regex', function() {
        if (new RegExp('(.*?\n){'+ MIN_LINE_COUNT +'}').test(text))
            'has minimum lines';
    })
    .add('backtracking pre-built regex', function() {
        if (preBuiltBackingRegex.test(text))
            'has minimum lines';
    })
    .add('no-backtrack on-the-fly regex', function() {
        if (new RegExp('([^\n]*\n){'+ MIN_LINE_COUNT +'}').test(text))
            'has minimum lines';
    })
    .add('no-backtrack pre-built regex', function() {
        if (preBuiltNoBackRegex.test(text))
            'has minimum lines';
    })
    .on('cycle', function(event) {
        console.log(String(event.target));
    })
    .on('complete', function() {
        console.log('Fastest is ' + this.filter('fastest').map('name'));
    })
    .run({ 'async': true });
    

    以下是三轮运行的结果:

    split string x 263,260 ops/sec ±0.68% (85 runs sampled)
    backtracking on-the-fly regex x 492,671 ops/sec ±1.01% (82 runs sampled)
    backtracking pre-built regex x 607,033 ops/sec ±0.72% (87 runs sampled)
    no-backtrack on-the-fly regex x 581,681 ops/sec ±0.77% (84 runs sampled)
    no-backtrack pre-built regex x 723,075 ops/sec ±0.72% (89 runs sampled)
    Fastest is no-backtrack pre-built regex
    
    split string x 260,962 ops/sec ±0.82% (85 runs sampled)
    backtracking on-the-fly regex x 502,410 ops/sec ±0.79% (84 runs sampled)
    backtracking pre-built regex x 606,220 ops/sec ±0.67% (88 runs sampled)
    no-backtrack on-the-fly regex x 578,193 ops/sec ±0.83% (86 runs sampled)
    no-backtrack pre-built regex x 741,864 ops/sec ±0.68% (84 runs sampled)
    Fastest is no-backtrack pre-built regex
    
    split string x 262,266 ops/sec ±0.76% (87 runs sampled)
    backtracking on-the-fly regex x 495,697 ops/sec ±0.82% (87 runs sampled)
    backtracking pre-built regex x 608,178 ops/sec ±0.72% (88 runs sampled)
    no-backtrack on-the-fly regex x 574,640 ops/sec ±0.92% (87 runs sampled)
    no-backtrack pre-built regex x 739,629 ops/sec ±0.72% (86 runs sampled)
    Fastest is no-backtrack pre-built regex
    

    所有的正则表达式测试显然比拆分字符串检查行数更快,甚至是回溯测试。我想我会做正则表达式测试。

    【讨论】:

      猜你喜欢
      • 2013-04-07
      • 1970-01-01
      • 2011-01-03
      • 1970-01-01
      • 2011-05-28
      • 1970-01-01
      • 2010-10-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多