Короткое время назад Я написал POS T коснуться комбинаторики. Часть кода этой статьи использовала Комбинатор Объект, который породил комбинации выбора и хранит их в массиве.
Проблема с комбинаторными операциями заключается в том, что количество комбинаций может расти взрывно быстро С каждым дополнительным выбором добавлено – больше, чем экспоненциально быстро, в некоторых случаях.
Если у меня есть три предмета и разрешаете 0, 1, 2 или 3 из которых можно выбрать, я получаю 8 уникальных вариантов, если я Не обращайте внимания на порядок, не разрешайте повторов и включите набор NULL Отказ Удвоить, что к шести предметам, и вы зарабатываете 64 вариантами (8 * 8). Удвоить это снова (12 предметов), существует 4096 вариантов (64 * 64). В этом случае, с указанными выше ограничениями количество комбинаций составляет 2 к мощности N выбора, поэтому он вырастает просто (!) Экспоненциально.
Для большого количества предметов хранение каждой комбинации в массиве может привести к истощению памяти. Вместо того, чтобы комбинатор вернуть массив только после того, как все комбинации были сгенерированы, как насчет того, если он вернул каждый комбинацию один за другим, по мере необходимости? Так как комбинатор Генерация Комбинации, могут быть преобразованы в Генератор ?
Оригинальный комбинатор.js.
В исходном коде каждая комбинация, созданная звонком комбайн () хранится в Комбинации множество:
var Combinator = function (opts) {
var combinations = [];
function combine(current, remainder) {
if (remainder.length === 0) {
if (current.length >= (opts.min || 0) &&
current.length <= (opts.max || current.length))
combinations.push(current);
} else {
combine(current.concat(remainder[0]), remainder.slice(1, remainder.length));
combine(current, remainder.slice(1, remainder.length));
}
return this;
}
return {
combinations: combinations,
combine: combine
}
}
module.exports = Combinator;Алгоритм немного украшен с добавлением MIN/MAX опций – это ограничение количества комбинаций, которые содержат, по крайней мере, мин и в большинстве Макс элементы. Я могу быть использован так:
var menu = {
threeItems: {
min: 0,
max: 3,
values: [1, 2, 3]
}
}
var threeCombos = new Combinator({
min: menu.threeItems.min,
max: menu.threeItems.max
})
.combine([], menu.threeItems.values)
.combinations;menu.rueeitems.values Недвижимость имеет (сюрприз!) три значения. мин и Макс Свойства определяют набор комбинированных комбинаций. В этом случае мы просим наборы от 0 длиной (нулевой набор) до полной длины (все набор значений). Помните, что мы не заинтересованы в порядке, и мы не позволяем дубликатам. Давайте увидимся в действии:
console.log('threeCombos.length =', threeCombos.length, threeCombos);
-- output --
threeCombos.length = 8 [ [ 1, 2, 3 ], [ 1, 2 ], [ 1, 3 ], [ 1 ], [ 2, 3 ], [ 2 ], [ 3 ], [] ]Теперь, вместо того, чтобы использовать массив для хранения всех комбинаций, давайте преобразуем этот бит JavaScript, чтобы использовать новую функциональность генератора ES6. Генератор – это состоятельная функция, которая дает значения один за другим, в итеративной моде.
Наивная попытка
Функция генератора объявляется с использованием Функция * вместо функция. доходность Оператор вызывается в функции генератора, чтобы вернуть отдельные значения обратно к абонеру. Генератор запоминает состояние предыдущего вызова, поэтому последующие урожай s вернется следующее логическое значение. Звонящий использует Далее () Способ получения каждой последующей стоимости от функции генератора. Массивы не требуются!
Я могу быть довольно ленивым время от времени, поэтому я взял TL; DR подходит к JavaScript Документация на генераторах и просто крылатый его. Первая попытка была:
var CombinatorGenerator = function (opts) {
function* combine(current, remainder) {
if (remainder.length === 0) {
if (current.length >= (opts.min || 0) &&
current.length <= (opts.max || current.length)) {
yield(current);
}
} else {
combine(current.concat(remainder[0]), remainder.slice(1, remainder.length))
combine(current, remainder.slice(1, remainder.length))
}
}
return {
combine: combine
}
}Это имеет смысл, верно? Вместо того, чтобы выталкивать набор вариантов на массив, я просто даю значение. В клиентском коде я продолжаю звонить следующим (), пока генератор не скажет мне, что это сделано.
var menu = require('./menu');
var Combinator = require('./Combinator-generator-naive');
function run() {
var threeCombos = new Combinator({
min: menu.threeItems.min,
max: menu.threeItems.max
})
.combine([], menu.threeItems.values);
for (;;) {
var it = threeCombos.next();
if (it.done) {
console.log("done!")
break;
}
console.log("choice", it.value);
}
}
run();Увы, мои надежды были пунктирны. Выход:
PS C:\Users\Jeff\workspace\Generator> node .\test-generated.js done!
Хорошо, так очевидно, что новый комбинатор возвращается до того, как первая доходность делает, поэтому мы «сделаны!» прежде чем мы на самом деле сделали.
Интуитивно понятная попытка
Все еще ненавидит, чтобы прочитать документацию, я следую, попробую интуицию исправления ошибки. Так что происходит, если я просто уступил от внутреннего комбинировать Звонки – логично, нет? Вместо того:
} else {
combine(current.concat(remainder[0]), remainder.slice(1, remainder.length))
combine(current, remainder.slice(1, remainder.length))
}Я пытаюсь уступить от рекурсивных звонков:
} else {
yield combine(current.concat(remainder[0]), remainder.slice(1, remainder.length)).next()
yield combine(current, remainder.slice(1, remainder.length)).next()
}Истинно, это будет работать. Итак, давайте запустим это:
PS C:\Users\Jeff\workspace\Generator> node .\generated.js
choice { value: { value: { value: [Object], done: false }, done: false },
done: false }
choice { value: { value: { value: [Object], done: false }, done: false },
done: false }
done!Хммм … Это не хорошо – то, что возвращается, – это состояние рекурсивных генераторов, но не фактические значения из доходность Операции.
Задумчивая попытка
Хорошо, время прокрутить. Немного гугла на «рекурсивном генераторе» отказывается от ссылки на Python’s доходность от. Этот синтаксис делегирует выводы дохода к другому генератору. Есть ли эквивалент в JavaScript?
Да! – И это Урожай * синтаксис. Это на самом деле в документе ссылка о Генераторы ; Я прочитал это, я, возможно, подумал бы это раньше (лень, как преступность, не [всегда] платят). Правильный синтаксис:
} else {
yield* combine(current.concat(remainder[0]), remainder.slice(1, remainder.length))
yield* combine(current, remainder.slice(1, remainder.length))
}И теперь, когда я называю комбинировать Метод, я вижу:
node .\generated.js choice [ 1, 2, 3 ] choice [ 1, 2 ] choice [ 1, 3 ] choice [ 1 ] choice [ 2, 3 ] choice [ 2 ] choice [ 3 ] choice [] done!
Хороший! Я возвращаюсь все комбинации, один за другим. Успех!
Полный код, используемый в этом посте, можно найти здесь Отказ Счастливый генерируя!
Обновление 2/26/2017
После прочтения Эта статья Недостабимым Эриком Эллиоттом я начал думать, что я продал один тип истощения ресурсов (память) для другого (стека). Тем не менее, я запускаю комбинатор с помощью входного массива длиной 30, и он пробежал до завершения: это 2³⁰ комбинаций, создаваемых (более миллиарда). Обратите внимание, что алгоритм
- не использует рекурсию хвоста (или, может быть, это «разделение сплит-хвоста»?); а также
- Урожай * Согласно статье Эрика не следует оптимизировать как хвост рекурсивный звонок в любом случае
Тем не менее, это работает. Доказательство можно найти с помощью Generated30.js в репозитории Git для этого поста.
Оригинал: “https://www.freecodecamp.org/news/recursive-generator-f8bc30e5e412/”