Ниже приводится подробное описание того, как создавать такие рекурсивные функции. Я думаю, что описанные шаги помогают решить огромное количество проблем. Они не панацея, но могут быть весьма полезными. Но сначала, вот над чем мы будем работать:
const getFlips = (n) =>
n <= 0
? ['']
: getFlips (n - 1) .flatMap (r => [r + 'H', r + 'T'])
Определение нашего алгоритма
Чтобы рекурсивно решить подобную задачу, нам нужно ответить на несколько вопросов:
На каком значении мы повторяемся?
Для простых рекурсий это часто один числовой параметр. Во всех случаях должен быть способ продемонстрировать, что мы продвигаемся к некоторому конечному состоянию.
Это простой случай, и должно быть совершенно очевидно, что мы хотим повторять количество бросков; назовем это n
.
Когда заканчивается наша рекурсия?
Нам нужно прекратить повторяться в конце концов. Здесь мы могли бы остановиться, когда n
равно 0 или, возможно, когда n
равно 1. Оба варианта могут работать. Давайте на мгновение отложим это решение, чтобы посмотреть, что может быть проще.
Как нам преобразовать ответ с одного шага в ответ на следующий?
Чтобы рекурсия могла сделать что-то полезное, важной частью является вычисление результата нашего следующего шага на основе текущего.
(Опять же, здесь возможны сложности для более сложных рекурсий. Например, нам может понадобиться использовать все нижние результаты для вычисления следующего значения. Например, найдите Каталонские номера. Здесь мы можем игнорировать это; наша рекурсия проста.)
Итак, как нам преобразовать, скажем, ['HH', 'HT', 'TH', 'TT']
в следующий шаг, ['HHH', 'HHT', 'HTH', 'HTT', 'THH', 'THT', 'TTH', 'TTT']
? Хорошо, если мы внимательно посмотрим на следующий результат, мы увидим, что в первой половине все элементы начинаются с «H», а во второй — с «T». Если мы проигнорируем первые буквы, каждая половина будет копией нашего ввода, ['HH', 'HT', 'TH', 'TT']
. Это выглядит очень многообещающе! Таким образом, наш рекурсивный шаг может состоять в том, чтобы сделать две копии предыдущего результата, первая с каждым значением, предшествующим 'H'
, вторая — с 'T'
.
Каково значение для нашего базового случая?
Это связано с вопросом, который мы пропустили. Мы не можем сказать, на чем оно заканчивается, не зная также, когда оно заканчивается. Но хороший способ сделать вывод для обоих — это работать в обратном направлении.
Чтобы вернуться от ['HHH', 'HHT', 'HTH', 'HTT', 'THH', 'THT', 'TTH', 'TTT']
к ['HH', 'HT', 'TH', 'TT']
, мы можем взять первую половину и удалить начальную букву «H» из каждого результата. Давай сделаем это снова. Из ['HH', 'HT', 'TH', 'TT']
мы берем первую половину и удаляем начальную букву «Н» из каждой, чтобы получить ['H', 'T']
. Хотя это может быть нашей остановкой, что произойдет, если мы сделаем еще один шаг вперед? Взяв первую половину и удалив начальное H
из одного оставшегося элемента, мы получим только ['']
. Имеет ли этот ответ смысл? Я бы сказал, что да: сколько существует способов подбросить монету ноль раз? Только один. Как бы мы записали это как строку из H
s и T
s? Как пустая строка. Таким образом, массив, содержащий только пустую строку, является отличным ответом для случая 0. Это также отвечает на наш второй вопрос о том, когда заканчивается рекурсия. Он заканчивается, когда n
равно нулю.
Написание кода для этого алгоритма
Конечно, теперь мы должны превратить этот алгоритм в код. Мы также можем сделать это в несколько шагов.
Объявление нашей функции
Мы пишем это, начиная с определения функции. Наш параметр называется n
. Я собираюсь вызвать функцию getFlips
. Итак, мы начинаем с
const getFlips = (n) =>
<something here>
Добавляем наш базовый случай.
Мы уже говорили, что закончим, когда n
будет равно нулю. Обычно я предпочитаю сделать это немного более устойчивым, проверяя любое n
, которое меньше или равно нулю. Это остановит бесконечную рекурсию, если кто-то передаст отрицательное число. Вместо этого мы могли бы выбрать исключение в этом случае, но наше объяснение ['']
для случая нуля, похоже, верно и для отрицательных значений. (Кроме того, я абсолютно ненавижу создание исключений!)
Это дает нам следующее:
const getFlips = (n) =>
n <= 0
? ['']
: <something here>
Здесь я решил использовать условное (троичное) выражение вместо операторов if-else
, потому что я предпочитаю работать с выражениями, а не с операторами, насколько это возможно. Эту же технику можно легко написать с помощью if-else
, если это кажется вам более естественным.
Обработка рекурсивного случая
Наше описание состояло в том, чтобы сделать две копии предыдущего результата, первая с каждым значением, предшествующим 'H'
, вторая — с 'T'
. Наш предыдущий результат, конечно же, getFlips (n - 1)
. Если мы хотим, чтобы перед каждым значением в этом массиве стояло 'H'
, лучше всего использовать .map
. Мы можем id вот так: getFlips (n - 1) .map (r => 'H' + r)
. И, конечно же, вторая половина просто getFlips (n - 1) .map (r => 'T' + r)
. Если мы хотим объединить два массива в один, есть много методов, включая .push
и .concat
. Но современным решением, вероятно, будет использование параметров спреда и просто возврат [...first, ...second]
.
Собрав все это вместе, мы получаем этот фрагмент:
const getFlips = (n) =>
n <= 0
? ['']
: [...getFlips (n - 1) .map (r => 'H' + r), ...getFlips (n - 1) .map (r => 'T' + r)]
console .log (getFlips (3))
Изучение результатов
Мы можем проверить это на нескольких случаях. Но мы должны быть достаточно убеждены кодом. Кажется, это работает, это относительно просто, нет очевидных пограничных случаев. Но я все еще вижу проблему. Мы вычисляем getFlips (n - 1)
дважды без веской причины. В рекурсивной ситуации это обычно довольно проблематично.
Для этого есть несколько очевидных исправлений. Во-первых, нужно отказаться от моего увлечения программированием на основе выражений и просто использовать логику if-else
с локальной переменной:
Замените условный оператор операторами if-else
const getFlips = (n) => {
if (n <= 0) {
return ['']
} else {
const prev = getFlips (n - 1)
return [...prev .map (r => 'H' + r), ...prev .map (r => 'T' + r)]
}
}
(Технически else
не нужен, и некоторые линтеры жаловались бы на него. Я думаю, что код читается лучше, когда он включен.)
Вычислить параметр по умолчанию для использования в качестве локальной переменной
Другим было бы использование значения параметра по умолчанию в более раннем определении.
const getFlips = (n, prev = n > 0 && getFlips (n - 1)) =>
n <= 0
? ['']
: [...prev .map (r => 'H' + r), ...prev .map (r => 'T' + r)]
Это можно по праву рассматривать как слишком сложное, и это может вызвать проблемы, когда ваша функция используется в неожиданных обстоятельствах. Не передавайте это, например, вызову массива map
.
Переосмыслите рекурсивный шаг
Любой из вышеперечисленных будет работать. Но есть лучшее решение.
Мы также можем написать почти такой же код с другим подходом к рекурсивному шагу, если увидим другой способ превращения ['HH', 'HT', 'TH', 'TT']
в ['HHH', 'HHT', 'HTH', 'HTT', 'THH', 'THT', 'TTH', 'TTT']
. Наша техника заключалась в том, чтобы разбить массив посередине и удалить первые буквы. Но есть и другие копии этой базовой версии в версии массива без одной из своих букв. Если мы удалим последние буквы из каждого, мы получим ['HH', 'HH', 'HT', 'HT', 'TH', 'TH', 'TT', 'TT']
, что является нашей исходной версией, в которой каждая строка встречается дважды.
Первый код, который приходит на ум для реализации этого — просто getFlips (n - 1) .map (r => [r + 'H', r + 'T'])
. Но это было бы немного неправильно, так как оно преобразовывало бы ['HH', 'HT', 'TH', ' TT']
в [["HHH", "HHT"], ["HTH", "HTT"], ["THH", "THT"], [" TTH", " TTT"]]
с дополнительным уровнем вложенности, а рекурсивное применение просто давало бы бессмыслицу. Но есть альтернатива .map
, которая удаляет этот дополнительный уровень вложенности, .flatMap
.
И это приводит нас к решению, которым я очень доволен:
const getFlips = (n) =>
n <= 0
? ['']
: getFlips (n - 1) .flatMap (r => [r + 'H', r + 'T'])
console .log (getFlips (3))
person
Scott Sauyet
schedule
25.08.2020