Концептуально простой и понятный способ вычисления коэффициентов заданного числа n
может выглядеть следующим образом:
# ruby def factors(n) (1..n).select { |int| n % int == 0 } end // javascript function factors(n) { return Array(n).fill().map((_, idx) => idx + 1) .filter(int => n % int === 0); }
Эти примеры кода выполняют свою работу. Они перебирают полный список чисел от 1 до n
, выбирая только те числа, на которые n
можно разделить без остатка. Как и ожидалось, возвращенный результат представляет собой массив факторов n
. Хотя этот подход работает, это то, что мы называем подходом «грубой силы», и он очень медленный. Скорость кода может не иметь большого значения, когда мы вычисляем коэффициенты относительно небольшого числа. Однако, если на входе очень большое число, неэффективность этого алгоритма становится очевидной. Мы можем быть более умными, чем это.
Более быстрый подход
Вместо того, чтобы перебирать каждое число от 1 до n
, мы можем остановиться после достижения квадратного корня из n
. Или, точнее, пол квадратного корня из n
(в случае квадратного корня, который не является целым числом). Например, квадратный корень из 48 равен 6,92820323028. При вычислении коэффициентов 48 мы должны выполнять итерацию от 1 до 6.
При использовании этого подхода мы можем добавлять до двух чисел в наш список факторов на каждой итерации. Концептуально это помогает думать о наших факторах как о парах. Представьте себе двумерный массив, содержащий все множители числа 24: [[1, 24], [2, 12], [3, 8], [4, 6]]
, почему бы не добавить оба элемента пары одновременно? (примечание: приведенный ниже код создает плоский массив факторов). По мере того, как мы итерируем от 1 до квадратного корня из n
, когда мы найдем целый делитель n
(пусть это будет i
), мы добавим i
в список множителей вместе с числом, на которое мы должны умножить i
, чтобы вернуть продукт n
.
Есть одно исключение. При нахождении множителей квадратного числа вы, вероятно, захотите включить квадратный корень только один раз. Мы учитываем это в строке 8 приведенного ниже кода.
# ruby def factors(n) factors = [] limit = Math.sqrt(n).floor (1..limit).each do |i| if n % i == 0 factors.push(i) factors.push(n / i) if (n / i) != i end end factors end
Вот тот же подход, написанный на JavaScript:
// javascript function factors(n) { const factors = []; const limit = Math.floor(Math.sqrt(n)); for (let i = 1; i <= limit; i++) { if (n % i == 0) { factors.push(i); if (n / i !== i) { factors.push(n / i) } } } return factors; }
Если вы протестируете этот код с небольшими числовыми аргументами, вы вряд ли заметите разницу в скорости кода. Вы можете посмотреть на увеличенное количество строк в более эффективном коде и подумать: а оно действительно необходимо?. Попробуйте вместо этого запустить эти примеры кода с гораздо большим вводом — factors(108800324)
— и вы заметите большую разницу. Фактически, исходная реализация JavaScript factors
может вызвать ошибку — JavaScript heap out of memory
.
Бенчмаркинг
Вы можете изучить разницу в скорости двух наших ruby-методов, factors
, с помощью модуля Benchmark
. Это довольно просто! Ниже я переименовал наши два метода ruby, чтобы различать их, и использовал модуль Benchmark
для анализа их скорости.
require 'benchmark' def slow_factors(n) (1..n).select { |int| n % int == 0 } end def faster_factors(n) factors = [] limit = Math.sqrt(n).floor (1..limit).each do |i| if n % i == 0 factors.push(i) factors.push(n / i) if (n / i) != i end end factors end Benchmark.bm do |bm| bm.report { slow_factors(108_800_324) } bm.report { faster_factors(108_800_324) } end
Вот сообщаемые данные:
user system total real 8.250000 0.010000 8.260000 ( 8.265131) 0.000000 0.000000 0.000000 ( 0.000724)
Какая разница! В этом примере используется очень большой ввод, но вы все равно увидите разницу при использовании небольшого ввода:
Benchmark.bm do |bm| bm.report { slow_factors(6024) } bm.report { faster_factors(6024) } end # data: user system total real 0.000000 0.000000 0.000000 ( 0.000467) 0.000000 0.000000 0.000000 ( 0.000018)
В JavaScript методы console.time
и console.timeEnd
забавны. Использование аргумента 108800324
с нашей более медленной функцией factor
привело к ошибке, поэтому нам придется сравнить наш код с чем-то меньшим.
Я переименовал две функции JavaScript factor
, чтобы различать их, и запустил их с помощью приведенного ниже кода:
console.time("slow factors"); slowFactors(1800324); console.timeEnd("slow factors"); console.time("faster factors"); fasterFactors(1800324); console.timeEnd("faster factors");
Результат:
slow factors: 1455.518ms faster factors: 0.143ms
Вы можете увидеть существенную разницу в скорости здесь, даже с гораздо меньшим числовым аргументом.
Как начинающий программист или когда вы впервые задумываетесь о том, как подойти к проблеме, с которой вы раньше не сталкивались, я рекомендую сначала обрисовать в общих чертах самый простой возможный подход — даже если это «грубая сила». Не пытайтесь быть слишком умным сразу. Обратите особое внимание на требования задачи и используйте методичный пошаговый план для решения некоторых более простых тестовых случаев. После того, как вы подтвердите свое понимание проблемы, более подробно изучите, как вы можете оптимизировать свой код, чтобы выполнять работу более эффективно.
Пример создания списка множителей для числа n
довольно прост, но я обнаружил, что этот шаблон полезен при работе с задачами всех уровней сложности. Я надеюсь, что это простое пошаговое руководство было полезным для тех, кто пытается установить подобный шаблон в своей работе!
Хотите стать участником Medium?
Если вы хотите узнать больше здесь, на Medium, вы можете зарегистрироваться по моей реферальной ссылке. Обратите внимание, что я получу часть членского взноса.