Отслеживайте, сколько раз была вызвана рекурсивная функция

62

 function singleDigit(num) {
      let counter = 0
      let number = [...num + ''].map(Number).reduce((x, y) => {return x * y})

      if(number <= 9){
          console.log(number)
      }else{
          console.log(number)
          return singleDigit(number), counter += 1
      }
   }
singleDigit(39)

Приведенный выше код принимает целое число и сводит его к одной цифре, умножая его на свои собственные цифры.

Пример 39

3 x 9 = 27.
2 x 7 = 14.
1 x 4 = 4.

Консоль будет регистрировать:

27 
14 
4

Как отследить, чтобы рекурсивная функция была вызвана 3 раза?

Я попытался добавить счетчик, но он не обновляется. Буду признателен за любую помощь

chs242
источник
4
.map(Number)является избыточным, поскольку *оператор в любом случае приводит значения к числу. ;-)
RobG
4
Пара вопросов: 1) Как вы собираетесь работать с отрицательными числами? Например, число -57действительно a -50и a -7.. если смотреть таким образом, то будет сделано уменьшение -5x, -7дающее положительное число 35. Или вы хотите, чтобы он видел только отрицательный знак с, 5а не с 7, хотя на 7самом деле это тоже отрицательный знак . 2) Как вы собираетесь работать с числами, которые включают ноль? поскольку это автоматически обнулит сокращение. Следовательно, чем больше число, которое вы передаете, тем больше вероятность того, что оно обнулится. Другим вариантом будет пропустить нули
Pimp Trizkit
3
Я понимаю, что мои вышеупомянутые вопросы касаются не подсчета рекурсии, а скорее аспекта решения загадок содержания, используемого в этом вопросе. Пожалуйста, прости меня.
Тачка Тризкит
3
Я польщен, что вам нравится мой ответ, но для практических целей я думаю, что stackoverflow.com/a/59570894/1346276 - самый чистый общий вариант.
phipsgabler
2
@phipsgabler любой, кто находит время, чтобы написать умный и последовательный ответ, заслуживает подобного. Спасибо
chs242

Ответы:

25

Это почти чисто академический вариант, но для этой цели вы можете использовать модифицированный комбинатор с фиксированной точкой .

Позволяет немного сократить и улучшить исходную функцию:

function singleDigit(n) {
    let digitProduct = [...(n + '')].reduce((x, y) => x * y, 1);
    return digitProduct <= 9 ? digitProduct : singleDigit(digitProduct);
}

// singleDigit(123234234) == 0

Из этого варианта мы можем выделить и вывести рекурсивный вызов:

function singleDigitF(recur) {
    return function (n) {
        let digitProduct = [...(n + '')].reduce((x, y) => x * y, 1);
        return digitProduct <= 9 ? digitProduct : recur()(digitProduct);
    };
}

Эта функция теперь может использоваться с комбинатором с фиксированной точкой; в частности, я реализовал Y-комбинатор, адаптированный для (строгого) JavaScript, следующим образом:

function Ynormal(f, ...args) {
    let Y = (g) => g(() => Y(g));
    return Y(f)(...args);
}

где у нас есть Ynormal(singleDigitF, 123234234) == 0.

Теперь приходит хитрость. Поскольку мы вычеркнули рекурсию к Y-комбинатору, мы можем посчитать количество рекурсий в нем:

function Ycount(f, ...args) {
    let count = 1;
    let Y = (g) => g(() => {count += 1; return Y(g);});
    return [Y(f)(...args), count];
}

Быстрая проверка в узле REPL дает:

> Ycount(singleDigitF, 123234234)
[ 0, 3 ]
> let digitProduct = (n) => [...(n + '')].reduce((x, y) => x * y, 1)
undefined
> digitProduct(123234234)
3456
> digitProduct(3456)
360
> digitProduct(360)
0
> Ycount(singleDigitF, 39)
[ 4, 3 ]

Этот комбинатор теперь будет работать для подсчета количества вызовов в любой рекурсивной функции, написанной в стиле singleDigitF.

(Обратите внимание, что есть два источника получения нуля в качестве очень частого ответа: числовое переполнение ( 123345456999999999становление 123345457000000000и т. Д.) И тот факт, что вы почти наверняка получите ноль в качестве промежуточного значения где-то, когда размер входных данных увеличивается.)

phipsgabler
источник
6
Для тех, кто высказался вниз: я действительно согласен с вами, что это не лучшее практическое решение, поэтому я назвал его «чисто академическим».
phipsgabler
Честно говоря, это потрясающее решение и полностью подходит для регрессионного / математического типа исходного вопроса.
Шераф
73

Вы должны добавить встречный аргумент в определение вашей функции:

function singleDigit(num, counter = 0) {
    console.log(`called ${counter} times`)
    //...
    return singleDigit(number, counter+1)
}
singleDigit(39)
Sheraff
источник
6
классно. Похоже, мой счетчик не работал, потому что я объявил его в функции
chs242
7
Правила области видимости @ chs242 будут диктовать, что объявление ее в функции будет создавать новое при каждом вызове. stackoverflow.com/questions/500431/…
Taplar
10
@ chs242 это не значит, что вы объявили это внутри функции. Технически это все параметры по умолчанию, а в вашем случае просто значение никогда не переносится в следующий раз, когда функция вызывается рекурсивно. ae каждый раз, когда запускается функция counter, ее отбрасывают и устанавливают на 0, если вы явно не переносите ее в своем рекурсивном вызове, как это делает Sheraff. AesingleDigit(number, ++counter)
zfrisch
2
Правильно @zfrisch Теперь я понимаю. Спасибо, что
нашли
35
Пожалуйста, измените ++counterна counter+1. Они функционально эквивалентны, но последний лучше определяет намерения, не (неоправданно) изменяет и не изменяет параметры и не имеет возможности случайного постинкрементного увеличения. Или еще лучше, так как это хвостовой вызов, используйте вместо этого цикл.
BlueRaja - Дэнни Пфлюгофт
37

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

Тем не менее, есть другое решение в JS. Несколько других ответов предложили просто объявить count вне рекурсивной функции:

let counter = 0
function singleDigit(num) {
  counter++;
  // ..
}

Это конечно работает. Однако это делает функцию не входящей (не может быть вызвана дважды правильно). В некоторых случаях вы можете игнорировать эту проблему и просто убедиться, что вы не вызываете singleDigitдважды (javascript является однопоточным, так что это не так уж сложно сделать), но это ошибка, ожидающая того, что произойдет, если вы обновитесь singleDigitпозже, чтобы стать асинхронной, и она также чувствует некрасиво.

Решение состоит в том, чтобы объявить counterпеременную снаружи, но не глобально. Это возможно, потому что у javascript есть замыкания:

function singleDigit(num) {
  let counter = 0; // outside but in a closure

  // use an inner function as the real recursive function:
  function recursion (num) {
    counter ++
    let number = [...num + ''].map(Number).reduce((x, y) => {return x * y})

    if(number <= 9){
      return counter            // return final count (terminate)
    }else{
      return recursion(number)  // recurse!
    }
  }

  return recursion(num); // start recursion
}

Это похоже на глобальное решение, но каждый раз, когда вы вызываете singleDigit(которая теперь не является рекурсивной функцией), оно будет создавать новый экземпляр counterпеременной.

slebetman
источник
1
Переменная counter доступна только внутри singleDigitфункции и предоставляет альтернативный чистый способ сделать это без передачи аргумента imo. +1
AndrewL64
1
Так recursionкак теперь он полностью изолирован, следует передать счетчик как последний параметр. Я не думаю, что создание внутренней функции необходимо. Если вам не нравится идея иметь параметры для единственной выгоды рекурсии (я понимаю, что пользователь может связываться с ними), тогда заблокируйте их Function#bindв частично примененной функции.
Таможенный командир
@customcommander Да, я упомянул об этом в первой части моего ответа - the traditional solution is to pass the count as a parameter. Это альтернативное решение в языке с замыканиями. В некотором смысле проще следовать, потому что это только одна переменная, а не бесконечное число экземпляров переменных. В других отношениях знание этого решения помогает, когда отслеживаемая вещь является общим объектом (представьте, что вы создаете уникальную карту) или очень большим объектом (например, строкой HTML)
slebetman
counter--было бы традиционным способом решить вашу претензию "нельзя дважды назвать правильно"
MonkeyZeus
1
@MonkeyZeus Какая разница? Кроме того, как вы узнали бы, какое число инициализировать счетчик, чтобы увидеть, что это счет, который мы хотим найти?
Slebetman
22

Другой подход, поскольку вы производите все числа, заключается в использовании генератора.

Последний элемент - это ваше число, nуменьшенное до однозначного числа, и чтобы подсчитать, сколько раз вы повторяли, просто прочитайте длину массива.

const digits = [...to_single_digit(39)];
console.log(digits);
//=> [27, 14, 4]
<script>
function* to_single_digit(n) {
  do {
    n = [...String(n)].reduce((x, y) => x * y);
    yield n;
  } while (n > 9);
}
</script>


Последние мысли

Возможно, вы захотите рассмотреть возможность возврата в свое состояние. Все номера с нулем в нем будет возвращать ноль.

singleDigit(1024);       //=> 0
singleDigit(9876543210); //=> 0

// possible solution: String(n).includes('0')

То же самое можно сказать и о любых числах, состоящих 1только из.

singleDigit(11);    //=> 1
singleDigit(111);   //=> 1
singleDigit(11111); //=> 1

// possible solution: [...String(n)].every(n => n === '1')

Наконец, вы не уточнили, принимаете ли вы только положительные целые числа. Если вы принимаете отрицательные целые числа, приведение их к строкам может быть рискованным:

[...String(39)].reduce((x, y) => x * y)
//=> 27

[...String(-39)].reduce((x, y) => x * y)
//=> NaN

Возможное решение:

const mult = n =>
  [...String(Math.abs(n))].reduce((x, y) => x * y, n < 0 ? -1 : 1)

mult(39)
//=> 27

mult(-39)
//=> -27
customcommander
источник
Великий. @customcommander спасибо, что объяснили это очень ясно
chs242
6

Здесь было много интересных ответов. Я думаю, что моя версия предлагает дополнительную интересную альтернативу.

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

  {
    digit: 4,
    steps: [39, 27, 14, 4],
    calls: 3
  }

Затем вы можете записать шаги, если хотите, или сохранить их для дальнейшей обработки.

Вот версия, которая делает это:

const singleDigit = (n, steps = []) =>
  n <= 9
    ? {digit: n, steps: [... steps, n], calls: steps .length}
    : singleDigit ([... (n + '')] .reduce ((a, b) => a * b), [... steps, n])

console .log (singleDigit (39))

Обратите внимание, что мы отслеживаем, stepsно выводим calls. Хотя мы могли бы отслеживать количество вызовов с помощью дополнительного параметра, это, похоже, ничего не дает. Мы также пропускаем map(Number)шаг - они будут приведены к числам в любом случае умножением.

Если у вас есть опасения по поводу того, что этот stepsпараметр по умолчанию выставлен как часть вашего API, его достаточно легко скрыть с помощью внутренней функции, подобной этой:

const singleDigit = (n) => {
  const recur = (n, steps) => 
    n <= 9
      ? {digit: n, steps: [... steps, n], calls: steps .length}
      : recur ([... (n + '')] .reduce ((a, b) => a * b), [... steps, n])
  return recur (n, [])
}

И в любом случае, было бы немного чище извлечь умножение цифр в вспомогательную функцию:

const digitProduct = (n) => [... (n + '')] .reduce ((a, b) => a * b)

const singleDigit = (n, steps = []) =>
  n <= 9
    ? {digit: n, steps: [... steps, n], calls: steps .length}
    : singleDigit (digitProduct(n), [... steps, n])
Скотт Саует
источник
2
Еще один отличный ответ;) Обратите внимание, что когда n отрицательно, digitProductвернется NaN( -39 ~> ('-' * '3') * '9'). Таким образом, вы можете использовать абсолютное значение n и использовать -1или 1в качестве начального значения вашего снижения.
таможенный командир
@customcommander: на самом деле, он будет возвращаться {"digit":-39,"steps":[-39],"calls":0}, так как -39 < 9. Хотя я согласен, что это может быть связано с некоторой проверкой ошибок: параметр является числом? - это положительное целое число? - и т. д. Я не думаю, что обновлю, чтобы включить это. Это фиксирует алгоритм, и обработка ошибок часто зависит от кода.
Скотт Саует
6

Если вы просто пытаетесь подсчитать, сколько раз оно уменьшается, и не заботитесь о рекурсии конкретно ... вы можете просто удалить рекурсию. Приведенный ниже код остается верным Исходному сообщению, поскольку он не считается num <= 9нуждающимся в сокращении. Следовательно, singleDigit(8)будет иметь count = 0и singleDigit(39)будет count = 3, так же, как OP и принятый ответ демонстрируют:

const singleDigit = (num) => {
    let count = 0, ret, x;
    while (num > 9) {
        ret = 1;
        while (num > 9) {
            x = num % 10;
            num = (num - x) / 10;
            ret *= x;
        }
        num *= ret;
        count++;
        console.log(num);
    }
    console.log("Answer = " + num + ", count = " + count);
    return num;
}

Нет необходимости обрабатывать числа 9 или меньше (т.е. num <= 9). К сожалению, код OP будет обрабатываться, num <= 9даже если он его не считает. Код выше не будет ни обрабатывать, ни считать num <= 9. Это просто проходит через.

Я предпочитаю не использовать, .reduceпотому что выполнение математики выполнялось намного быстрее. И, для меня, легче понять.


Дальнейшее размышление о скорости

Я чувствую, хороший код тоже быстро. Если вы используете этот тип сокращения (который часто используется в нумерологии), вам может понадобиться использовать его для огромного количества данных. В этом случае скорость станет самым важным.

Использование обоих .map(Number)и console.log(на каждом этапе сокращения) очень и очень долго для выполнения и не нужно. Простое удаление .map(Number)из OP ускорило его примерно в 4,38 раза. Удаление console.logускорило его настолько, что было почти невозможно правильно протестировать (я не хотел ждать этого).

Так что , похоже на customcommander ответ «s, не используя .map(Number)ни console.logи толкающие результаты в массив и использовать .lengthдля countгораздо гораздо быстрее. К сожалению для ответа customcommander , использование функции генератора действительно очень медленно (этот ответ примерно в 2,68 раза медленнее, чем OP без .map(Number)и console.log)

Кроме того, вместо использования .reduceя просто использовал фактическую математику. Это единственное изменение ускорило мою версию функции в 3,59 раза.

Наконец, рекурсия медленнее, она занимает место в стеке, использует больше памяти и имеет ограничение на количество повторений. Или, в этом случае, сколько шагов сокращения он может использовать, чтобы завершить полное сокращение. Развертывание рекурсии в итеративных циклах сохраняет все это в одном и том же месте в стеке и не имеет теоретического ограничения на количество шагов сокращения, которое можно использовать для завершения. Таким образом, эти функции здесь могут «уменьшить» почти любое целое число, ограниченное только временем выполнения и продолжительностью массива.

Все это в виду ...

const singleDigit2 = (num) => {
    let red, x, arr = [];
    do {
        red = 1;
        while (num > 9) {
            x = num % 10;
            num = (num - x) / 10;
            red *= x;
        }
        num *= red;
        arr.push(num);
    } while (num > 9);
    return arr;
}

let ans = singleDigit2(39);
console.log("singleDigit2(39) = [" + ans + "],  count = " + ans.length );
 // Output: singleDigit2(39) = [27,14,4],  count = 3

Вышеуказанная функция работает очень быстро. Это примерно в 3,13 раза быстрее, чем OP (без .map(Number)и console.log), и примерно в 8,4 раза быстрее, чем ответ customcommander . Имейте в виду, что удаление console.logиз OP предотвращает создание числа на каждом шаге сокращения. Следовательно, здесь необходимо поместить эти результаты в массив.

PT

Тачка Тризкит
источник
1
В этом ответе много образовательного значения, так что спасибо за это. I feel good code is also fast.Я бы сказал , что качество кода имеет следует оценивать с определенным набором требований. Если производительность не является одним из них, вы ничего не получите, заменив код, который любой может понять, на «быстрый» код. Вы не поверите, что количество кода, которое я видел, было подвергнуто рефакторингу до такой степени, что никто больше не может его понять (по какой-то причине оптимальный код, как правило, также недокументирован;). Наконец, имейте в виду, что сгенерированные ленивые списки позволяют потреблять элементы по требованию.
таможенный командир
Спасибо, я думаю. ИМХО, читая реальную математику о том, как это сделать, мне было легче понять ... чем те [...num+''].map(Number).reduce((x,y)=> {return x*y})или даже [...String(num)].reduce((x,y)=>x*y)утверждения, которые я вижу в большинстве ответов здесь. Так что для меня это было дополнительным преимуществом лучшего понимания того, что происходит на каждой итерации, и намного быстрее. Да, минимизированный код (который имеет свое место) ужасно труден для чтения. Но в этих случаях, как правило, сознательно не заботятся о его читабельности, а просто о конечном результате, который можно вырезать, вставить и продолжить.
Сутенер Тризкит
Разве в JavaScript нет целочисленного деления, чтобы вы могли сделать эквивалент C digit = num%10; num /= 10;? Необходимость выполнить num - xсначала удаление последней цифры перед делением, вероятно, заставит JIT-компилятор сделать отдельное деление от того, которое он сделал, чтобы получить остаток.
Питер Кордес
Я так не думаю. Это vars (у JS нет ints). Следовательно, n /= 10;при необходимости преобразуется nв число с плавающей точкой. num = num/10 - x/10может преобразовать его в число с плавающей точкой, которое является длинной формой уравнения. Следовательно, я должен использовать реорганизованную версию, num = (num-x)/10;чтобы сохранить ее целым числом. В JavaScript я не могу найти способ, который может дать вам как частное, так и остаток от одной операции деления. Кроме того, digit = num%10; num /= 10;это два отдельных оператора и, следовательно, две отдельные операции деления. Прошло много времени с тех пор, как я использовал C, но я тоже думал, что это правда.
Тачка Тризкит
6

Почему бы не сделать вызов console.countв вашей функции?

Изменить: фрагмент, чтобы попробовать в вашем браузере:

function singleDigit(num) {
    console.count("singleDigit");

    let counter = 0
    let number = [...num + ''].map(Number).reduce((x, y) => {return x * y})

    if(number <= 9){
        console.log(number)
    }else{
        console.log(number)
        return singleDigit(number), counter += 1
    }
}
singleDigit(39)

У меня это работает в Chrome 79 и Firefox 72

Mistermatt
источник
console.count не поможет, так как счетчик сбрасывается при каждом вызове функции (как было объяснено в ответах выше)
chs242
2
Я не понимаю вашу проблему, поскольку у меня она работает в Chrome и Firefox, я добавил фрагмент в свой ответ
Mistermatt
6

Вы можете использовать закрытие для этого.

Просто храните counterв закрытии функции.

Вот пример:

function singleDigitDecorator() {
	let counter = 0;

	return function singleDigitWork(num, isCalledRecursively) {

		// Reset if called with new params 
		if (!isCalledRecursively) {
			counter = 0;
		}

		counter++; // *

		console.log(`called ${counter} times`);

		let number = [...(num + "")].map(Number).reduce((x, y) => {
			return x * y;
		});

		if (number <= 9) {
			console.log(number);
		} else {
			console.log(number);

			return singleDigitWork(number, true);
		}
	};
}

const singleDigit = singleDigitDecorator();

singleDigit(39);

console.log('`===========`');

singleDigit(44);

Kholiavko
источник
1
Но таким образом счетчик продолжает рассчитывать на следующий вызов, его необходимо сбрасывать при каждом первоначальном вызове. Это приводит к сложному вопросу: как определить, когда рекурсивная функция вызывается из другого контекста, в данном случае это глобальная функция против функции.
РобГ
Это всего лишь пример, чтобы придумать мысль. Это может быть изменено, если спросить пользователя о его потребностях.
Холявко
@RobG Я не понимаю твой вопрос. Рекурсивная функция не может быть вызвана вне замыкания, потому что это внутренняя функция. Таким образом, нет никакой возможности или необходимости различать контекст, потому что есть только один возможный контекст
slebetman
@slebetman Счетчик никогда не сбрасывается. Функция, возвращаемая функцией singleDigitDecorator()будет увеличивать один и тот же счетчик при каждом вызове.
Таможенный командир
1
@ slebetman - проблема в том, что функция, возвращаемая singleDigitDecorator , не сбрасывает свой счетчик при повторном вызове. Это функция, которая должна знать, когда нужно сбросить счетчик, в противном случае для каждого использования требуется новый экземпляр функции. Возможный вариант использования Function.caller ? ;-)
RobG
1

Вот версия Python, которая использует функцию-обертку для упрощения счетчика, как было предложено в ответе slebetman - я пишу это только потому, что основная идея очень ясна в этой реализации:

from functools import reduce

def single_digit(n: int) -> tuple:
    """Take an integer >= 0 and return a tuple of the single-digit product reduction
    and the number of reductions performed."""

    def _single_digit(n, i):
        if n <= 9:
            return n, i
        else:
            digits = (int(d) for d in str(n))
            product = reduce(lambda x, y: x * y, digits)
            return _single_digit(product, i + 1)

    return _single_digit(n, 0)

>>> single_digit(39)
(4, 3)
Люк Сончак
источник
1
В Python, я предпочел бы что - то вроде этого .
phipsgabler