【问题标题】:Counting Sheep (Beatrix Trotter) Javascript数羊(Beatrix Trotter)Javascript
【发布时间】:2017-06-05 22:29:25
【问题描述】:

我正在尝试解决有关 CodeWars 的问题,该问题是 Google CodeJam 2016 资格赛的一部分。 https://www.codewars.com/kata/bleatrix-trotter-the-counting-sheep/train/javascript

我相信我的代码占了所有测试用例,我唯一的问题是它无法在 codewars 上提交,因为它的运行时间大于 12000 毫秒。

如何让我的代码更有效率?还有测试循环是否无限的最佳实践。

function trotter(n) {
  var tracker = [];
  var sum = [];
  var snacker = n;
  for(var i = 0; tracker.length < 10; i++){
    sum = snacker.toString().split('');
    sum.forEach(function(num) {
        if (tracker.indexOf(num) == -1) {
             tracker.push(num);
        }
    });
    snacker += n;
  }
  return tracker.length === 10 ? snacker - n : "INSOMNIA";
}

【问题讨论】:

  • for 循环可以比 forEach 快,而 if..else 通常比复合运算符 @ 快987654323@,存储tracker.length 的值比每次获取都快。但当然编译器优化意味着这并不总是正确的。
  • 我会做你刚才提到的调整,看看是否有效,谢谢
  • 我尝试使用 map 和 reduce 尽可能提高效率,但我无法获得相同的返回值;
  • 如果 n == 2,您不会在 10 次或更少的迭代中得到“3”。
  • 我发布到 codwars 的链接中的说明指定 n 只能通过乘以 1 的增量来增加。

标签: javascript performance runtime infinite-loop


【解决方案1】:

当性能很重要且可读性降低不是问题时,您应该:

  • 更喜欢forwhile 循环而不是像mapforEach 这样的内置函数...

  • 在最近 (2016) 版本的 JavaScript 中,看起来 for 循环,特别是反向 for 循环,是性能最高的选项。

    另外,请记住,您不需要使用它的 3 个表达式。例如,对我来说……:

    let found = 0;
    
    for (;found < 10;) {
    

    let j = chars.length;
    
    for (;j;) {
    

    始终返回比初始化槽中的初始化和 while 循环更好的结果,尽管在后一种情况下差异不是那么大。

    更多详情请见Javascript Performance: While vs For Loops

  • 当在whilefor 的表达式中使用foo.bar 时,例如for (let i = 0; i &lt; array.length; ++i),最好声明上面的限制条件,这样就不会每次都对其进行评估,因为这涉及到@ 987654322@:

    const totalElements = array.length;
    
    for (let i = 0; i < totalElements; ++i) { ... }
    
  • 更喜欢前增量而不是后增量。后者会创建一个临时变量来存储预增量值,它会返回给你,而前者会先做增量,然后返回增量值。不需要临时变量。

    有关更多信息,请参阅post increment vs pre increment - Javascript Optimization

    实际上,如果无论您使用哪个结果都相同,那么我建议您尽可能使用预增量。

  • 避免使用具有等效表达式的内置方法。例如,(n + '')n.toString() 性能更高。

  • 避免类型转换,更喜欢使用整数而不是浮点数或字符串作为现代 JS 引擎标记变量类型。这意味着,一方面,更改类型会降低性能,另一方面,始终如一地使用它们将允许引擎进行一些特定于类型的优化。

    有关更多信息,请参阅https://www.html5rocks.com/en/tutorials/speed/v8/

  • 尽可能使用bitwise operators。例如,可以使用Math.floor(a /b)a / b | 0 进行整数除法,速度几乎是原来的两倍。

    有关JavaScript 中整数除法的更多信息,请参阅这个有趣的帖子:How to perform integer division and get the remainder in JavaScript?

  • 首选对象查找(object.propobject['prop'])而不是使用Array.prototype.indexOf(...)

    Javascript: what lookup is faster: array.indexOf vs object hash?

  • 当有更简单的结构或不可变数据结构的替代方案时,避免使用arraysobjects 及相关方法。

    例如,@RobG 的解决方案使用splice。虽然我不知道内部实现,但可能它正在移动已删除元素之后的元素以再次压缩数组并更新其length

    但是,在我的解决方案中,数组的length 始终相同,您只需将其值从false 更改为true,这涉及的开销要少得多,因为无需重新分配空间。

  • 尽可能使用类型化数组,虽然我在这里尝试了Uint8Array,都分配了true1,但它们都没有改善时间;前者实际上几乎将时间翻了一番,而第一个则使它们或多或少保持不变。也许有一个BooleanArray 可以工作。

请记住,这只是我认为可能有助于加快您的示例速度的一些技术或功能的列表。我强烈建议您阅读我添加的外部链接,以便更好地了解它们的工作原理和原因,以及它们还可以应用于哪些地方。

此外,一般来说,您保留代码的级别越低,即您使用基本数据类型和操作,性能越好。

为了证明这一点,下面我将向您展示这段代码的高度优化版本,它使用整数除法 (n / 10) | 0 和余数 (%)。

function trotter(N) {
  if (N === 0) return 'INSOMNIA';
  
  const digits = [false, false, false, false, false, false, false, false, false, false];
    
  let n;
  let last = 0;
  let found = 0;
 
  for (;found < 10;) {
    n = last += N;
    
    for (;n;) {
      const digit = n % 10;
            
      n = (n / 10) | 0;
            
      if (!digits[digit]) {
        digits[digit] = true;
        
        ++found;
      }
    }
  }
  
  return last;
}

const numbers = [0, 2, 7, 125, 1625, 1692];
const outputs = ['INSOMNIA', 90, 70, 9000, 9750, 5076];

// CHECK IT WORKS FIRST:

numbers.map((number, index) => {
  if (trotter(number) !== outputs[index]) {
    console.log('EXPECTED = ' + outputs[index]);
    console.log('     GOT = ' + trotter(number));

    throw new Error('Incorrect value.');
  }
});


// PERF. TEST:

const ITERATIONS = 1000000;

const t0 = performance.now();

for (let i = 0; i < ITERATIONS; ++i) {
  numbers.map((number, index) => trotter(number));
}

const t1 = performance.now();

console.log(`AVG. TIME: ${ (t1 - t0) / ITERATIONS } ms. with ${ ITERATIONS } ITERATIONS`);
   AVG. TIME: 0.0033206450000000005 ms. with 1000000 ITERATIONS

     BROWSER: Google Chrome Version 59.0.3071.86 (Official Build) (64-bit)
          OS: macOS Sierra

BRAND, MODEL: MacBook Pro (Retina, 15-inch, Mid 2015)
   PROCESSOR: 2,8 GHz Intel Core i7
      MEMORY: 16 GB 1600 MHz DDR3

您可以在下面看到我的初始答案,它使用了此处列出的一些其他优化,但仍将 number 变量 n 转换为 string 并使用 String.prototype.split() 获取其数字。

它比上面的慢了将近 5 倍!

function trotter(N) {
  if (N === 0) return 'INSOMNIA';
  
  const digits = [false, false, false, false, false, false, false, false, false, false];
  
  let n = N;
  let i = 0;
  let found = 0;
  
  for (;found < 10;) {
    // There's no need for this multiplication:

    n = N * ++i;
    
    // Type conversion + Built-in String.prototype.split(), both can
    // be avoided:

    const chars = (n + '').split('');
    
    let j = chars.length;
    
    for (;j;) {
      const digit = chars[--j];
            
      if (!digits[digit]) {
        digits[digit] = true;
        
        ++found;
      }
    }
  }
  
  return n;
}

const numbers = [0, 2, 7, 125, 1625, 1692];
const outputs = ['INSOMNIA', 90, 70, 9000, 9750, 5076];

// CHECK IT WORKS FIRST:

numbers.map((number, index) => {
  if (trotter(number) !== outputs[index]) {
    console.log('EXPECTED = ' + outputs[index]);
    console.log('     GOT = ' + trotter(number));

    throw new Error('Incorrect value.');
  }
});


// PERF. TEST:

const ITERATIONS = 1000000;

const t0 = performance.now();

for (let i = 0; i < ITERATIONS; ++i) {
  numbers.map((number, index) => trotter(number));
}

const t1 = performance.now();

console.log(`AVG. TIME: ${ (t1 - t0) / ITERATIONS } ms. with ${ ITERATIONS } ITERATIONS`);
   AVG. TIME: 0.016428575000000004 ms. with 1000000 ITERATIONS

     BROWSER: Google Chrome Version 59.0.3071.86 (Official Build) (64-bit)
          OS: macOS Sierra

BRAND, MODEL: MacBook Pro (Retina, 15-inch, Mid 2015)
   PROCESSOR: 2,8 GHz Intel Core i7
      MEMORY: 16 GB 1600 MHz DDR3

【讨论】:

  • @GeorgeYammine 我更新了我的解决方案。当前的应该比最初的快 5 倍。
【解决方案2】:

另一种方法:

    function trotter(n){
      var unseen = '0123456789', last = 0;
      while (unseen != '' && last < 72*n) {
        last += n;
        var reg = new RegExp('[' + last + ']+', 'g');
        unseen = unseen.replace(reg, '');
      }
      return unseen != '' ? 'INSOMNIA' : last;
    };

    console.log('0   : ' + trotter(0));
    console.log('125 : ' + trotter(125));
    console.log('1625: ' + trotter(1625));

【讨论】:

  • 使用RegExpArray.prototype.replace 是性能杀手。我试图使用我的答案中的测试设置来测量您运行多次迭代的时间,我不得不杀死标签...:\
【解决方案3】:

以下代码没有特殊优化,使用所有 ECMA-262 ed 3 方法。它在 339 毫秒内运行了全套测试。

function trotter(n){
  var nums = ['0','1','2','3','4','5','6','7','8','9'];
  var i = 1;

  // Zero creates an infinite loop, so skip it
  if (n !== 0) {

    // While there are numbers to remove, keep going
    // Limit loops to 100 just in case
    while (nums.length && i < 100) {
      // Get test number as an array of digits
      var d = ('' + (n * i)).split('');

      // For each digit, if in nums remove it
      for (var j=0, jLen=d.length; j<jLen; j++) {
        var idx = nums.indexOf(d[j]);

        if (idx > -1) nums.splice(idx, 1);
      }

      i++;
    }
  }
  // If there are numbers left, didn't get to sleep
  // Otherwise, return last number seen (put d back together and make a numer)
  return nums.length? 'INSOMNIA' : Number(d.join(''));
}

console.log('0   : ' + trotter(0));
console.log('125 : ' + trotter(125));
console.log('1625: ' + trotter(1625));

大多数情况在大约 10 次迭代中得到解决,但是 125、1250、12500 等需要 73 次迭代,我认为这是任何数字中最多的一次。

由于 Array 方法可能很慢,这里有一个字符串版本,大约是twice as fast

function trotter(n){
  var found = '';
  if (n !== 0) {
    var i = 1;
    while (found.length < 10) {
      var d = i * n + '';
      var j = d.length;
      while (j) {
        var c = d[--j];
        var idx = found.indexOf(c);
        if (idx == -1) found += c;
      }
      i++;
    }
  }
  return found.length == 10? +d : 'INSOMNIA';
}

[0,    // Infinte loop case
 125,  // Max loop case
 1625] // just a number
 .forEach(function (n) {
  console.log(n + ' : ' + trotter(n));
});

虽然执行while (found.length) 不是最优的,但它通常只计算大约 10 次,因此移出该条件并不是很重要。另一种方法是包含一个计数器,每次将一个数字添加到 found 时都会增加一个计数器,但这并不是真正意义上的优化性能,而是找到一些合理的、可行的并通过(未公开的)测试的东西在代码大战中。

【讨论】:

  • 顺便说一句,代码大战中的测试通过时间并不取决于您的代码。每次他们显示不同的不可预知的结果。
  • 请问您为什么认为我的运行时需要这么长时间?即使我在代码中将迭代次数限制在 100 次,它仍然会超时
  • 没有什么突出的。您可以尝试将 forEach 替换为 for 循环(最少的代码更改),看看有什么区别。使用有效输入永远不应该达到 100 次迭代的限制,73 是我用 125、1250 等得到的最大迭代次数。我测试了多达 10,000,001 次,所以并不详尽,但我认为它给出了相当高的信心。我从逻辑上想不出除了 0 之外应该失败的数字。
  • @RobG 虽然您指出的问题是正确的,但您的代码也存在多个可以改进的性能问题。特别是,我认为你在滥用Arrays,特别是使用indexOfsplice
  • 使用我回答中的测试设置,您的代码平均需要 0.03289369 ms. 才能运行。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-10
  • 2012-12-30
  • 1970-01-01
  • 2014-04-17
  • 2011-02-07
相关资源
最近更新 更多