Подсчет Овец (Беатрикс Троттер) Javascript

1

Я пытаюсь решить вопрос о CodeWars, который был частью квалификационных раундов Google CodeJam 2016. https://www.codewars.com/kata/bleatrix-trotter-the-counting-sheep/train/javascript

Я считаю, что мой код учитывает все тестовые примеры, моя единственная проблема заключается в том, что он не может быть отправлен на кодовые слова, потому что он имеет время работы более 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";
}
Теги:
performance
infinite-loop
runtime

3 ответа

1
Лучший ответ

Когда производительность и снижение читаемости не являются проблемой, вы должны:

  • Предпочитайте for и в while циклы, а не встроенные, такие как map, forEach...

  • В последних версиях JavaScript (2016) это похоже на то, что for циклов, особенно обратных for циклов, является наиболее эффективным вариантом.

    Кроме того, имейте в виду, что вам не нужно использовать свои 3 выражения. Например, для меня...:

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

    а также

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

    последовательно возвращаются лучшие результаты, чем то же самое с инициализацией в слоте инициализации и чем в while циклы, хотя в этом последнем случае разница не столь велика.

    Более того, см. Javascript Performance: While vs For Loops

  • При использовании foo.bar через некоторое while или for выражения, например, for (let я = 0; я < array.length; ++i), предпочитайте объявлять предельное условие выше, чтобы оно не оценивалось каждый раз, так как это включает поиск объектов:

    const totalElements = array.length;
    
    for (let i = 0; i < totalElements; ++i) { ... }
    
  • Предпочитайте pre-increment вместо post-increment. Последний создаст временную переменную, хранящую значение pre-incrementation, которое вернется к вам, в то время как первое сначала выполнит приращение, а затем вернет увеличенное значение. Никакой временной переменной не требуется.

    Подробнее об этом см. В разделе post increment vs pre increment - Оптимизация Javascript

    Фактически, если результат будет таким же, независимо от того, какой из них вы используете, я бы посоветовал вам использовать предварительный приращение, когда это возможно.

  • Избегайте встроенных методов, имеющих эквивалентное выражение. Например, (n + '') более совершенен, чем n.toString().

  • Избегайте преобразований типов и предпочитайте использовать целые числа, а не плавающие или строки, как современные типы переменных тегов JS-движков. Это означает, что, с одной стороны, изменение типов будет иметь штраф за производительность, а с другой стороны, что использование их последовательно позволит движку выполнять некоторую оптимизацию по типу.

    Подробнее об этом см. Https://www.html5rocks.com/en/tutorials/speed/v8/.

  • По возможности используйте побитовые операторы. Например, целочисленное деление может быть выполнено с помощью Math.floor(a/b) или с a/b | 0 a/b | 0, что почти в два раза быстрее.

    Подробнее о целочисленном делении в JavaScript см. В этом интересном разделе: Как выполнить целочисленное деление и получить остаток в JavaScript?

  • Предпочитайте поиск объекта (object.prop или object['prop']) вместо использования Array.prototype.indexOf(...).

    См. Javascript: какой поиск выполняется быстрее: array.indexOf против хеша объекта?

  • Избегайте использования arrays и objects и связанных с ними методов, когда есть альтернативы с более простыми структурами или неуязвимыми структурами данных.

    Например, решение @RobG использует splice. Хотя я не знаю о внутренней реализации, возможно, это перемещение элементов, которые появляются после удаленных, чтобы снова объединить массив и обновить его length.

    Однако с моим решением length массива всегда одна и та же, и вы просто меняете свои значения от false до true, что требует гораздо меньших накладных расходов, так как нет необходимости перераспределять пространство.

  • Если возможно, используйте типизированные массивы, хотя я попытался Uint8Array здесь Uint8Array, назначив ему true и 1, и ни один из них не улучшил время; первый фактически почти удвоил время, а первый сохранил их более или менее одинаковыми. Возможно, это был BooleanArray который мог бы работать.

Имейте в виду, что это всего лишь список некоторых методов или функций, которые, я думаю, могут помочь ускорить ваш пример. Я настоятельно рекомендую вам прочитать внешние ссылки, которые я добавил, чтобы лучше понять, как и почему они работают, и где они могут быть применены.

Более того, в целом, на более низком уровне вы сохраняете свой код, т.е. Используете основные типы данных и операции, наиболее эффективными будут.

Чтобы доказать это, ниже я покажу вам высоко оптимизированную версию этого кода, которая использует целочисленное деление (n/10) | 0 (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 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
  • 0
    @ GeorgeYammine Я обновил свое решение. Текущий должен быть почти в 5 раз быстрее исходного.
0

Другой подход:

    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));
  • 0
    Использование RegExp и Array.prototype.replace производительность. Я попытался измерить ваше время выполнения нескольких итераций, используя настройки теста в моем ответе, и мне пришлось убить вкладку ...: \
0

Следующий код не имеет специальных оптимизаций и использует все методы 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 могут быть медленными, вот строковая версия, которая примерно в два раза быстрее:

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 раз, поэтому не очень важно перемещаться за пределы условия. Альтернативой является включение счетчика, который увеличивается каждый раз, когда цифра добавляется к найденному, но это не значит оптимизировать производительность так же, как найти что-то разумное, которое работает и передает (нераскрытые) тесты на кодовых таблицах.

  • 0
    Кстати, время прохождения тестов на codewars не зависит от вашего кода. Каждый раз они показывают другой непредсказуемый результат.
  • 0
    огромная помощь и обратная связь большое спасибо
Показать ещё 4 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню