Слідкуйте за тим, скільки разів викликалася рекурсивна функція


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 рази?

Я спробував додати лічильник, але він не вдається оновити. Буду вдячний за будь-яку допомогу


4
.map(Number)є надмірним, оскільки *оператор примушує значення до нумерації все одно. ;-)
РобГ

4
Пара питань: 1) Як ви маєте намір мати справу з негативними числами? Наприклад, число -57дійсно є a -50і -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і т. Д.), І той факт, що ви майже напевно отримаєте нуль як проміжне значення десь, коли розмір вводу зростає.)


6
Коротким людям: я дійсно згоден з вами, що це не найкраще практичне рішення - саме тому я визначив це як "суто академічний".
phipsgabler

Чесно кажучи, це приголомшливе рішення і повністю підходить для регресії / математики типу оригінального питання.
Шераф

73

Ви повинні додати протилежний аргумент до визначення функції:

function singleDigit(num, counter = 0) {
    console.log(`called ${counter} times`)
    //...
    return singleDigit(number, counter+1)
}
singleDigit(39)

6
приголомшливий Схоже, мій лічильник не працював, тому що я оголосив його у функції
chs242

7
@ chs242 правила діапазону диктують, що оголошення його у функції створюватиме нове кожне виклик. stackoverflow.com/questions/500431 / ...
Taplar

10
@ chs242 це не те, що ви оголосили це в межах функції. Технічно це теж усі параметри за замовчуванням. У вашому випадку це просто значення, яке ніколи не переносилося на наступний раз, коли функція була рекурсивно викликана. е. кожного разу, коли функція запускається, counterбуде видалятися та встановлюватися 0, якщо ви явно не перенесете її у своєму рекурсивному виклику, як це робить Шераф. AesingleDigit(number, ++counter)
zfrisch

2
правильно @zfrisch Я розумію, що зараз. Дякуємо, що
знайшли

35
Змініть ++counterна counter+1. Вони функціонально еквівалентні, але останній вказує намір краще, не мутує і не параметризує, і не має можливості випадково після збільшення. Або ще краще, оскільки це хвостик, замість цього використовуйте петлю.
BlueRaja - Danny Pflughoeft

37

Традиційне рішення - передавати відлік як параметр функції, запропонованій іншою відповіддю.

Однак у js є ще одне рішення. Кілька інших відповідей пропонують просто оголосити кількість за межами рекурсивної функції:

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змінної.


1
Змінна лічильника доступна лише у межах 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 дякую, що ви зрозуміли це дуже чітко
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, як і ОП та прийнята відповідь демонструють:

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)з ОП скоротило його приблизно на 4.38 разів. Видаливши console.logйого так швидко, майже неможливо було правильно перевірити (я не хотів його чекати).

Таким чином, подібно до відповіді customcommander , не використовувати .map(Number)ні console.logі не виштовхувати результати в масив і використовувати .lengthдля countнабагато набагато швидше. На жаль для відповіді customcommander , використання функції генератора дійсно дуже повільне (ця відповідь приблизно на 2.68x повільніше, ніж OP без .map(Number)і console.log)

Крім того, замість використання .reduceя просто використав фактичну математику. Сама ця зміна прискорила мою версію функції в 3,5 раза.

Нарешті, рекурсія проходить повільніше, вона займає простір у стеці, використовує більше пам’яті та обмежує кількість разів, коли вона може «повторюватися». Або, у цьому випадку, скільки кроків зменшення він може використати для завершення повного зменшення. Розгортання вашої рекурсії до ітеративних циклів зберігає це все на тому самому місці стека і не має теоретичного обмеження, скільки кроків зменшення може використовувати для завершення. Таким чином, ці функції тут можуть «зменшити» майже будь-яке розмірне ціле число, лише обмежене часом виконання та тим, наскільки тривалим може бути масив.

Все це на увазі ...

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 рази швидше, ніж ОП (без .map(Number)і console.log) і приблизно в 8,4 рази швидше, ніж відповідь користувача. Майте на увазі, що видалення console.logз ОП не дозволяє йому створювати число на кожному кроці скорочення. Отже, необхідність просунути ці результати в масив.

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 зробити окремий поділ від того, що він зробив, щоб отримати решту.
Пітер Кордес

Я не думаю, що це. Ці var( ints) не мають. Таким чином, n /= 10;перетвориться nв float, якщо це потрібно. 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


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);


1
Але таким чином лічильник продовжує рахувати наступний дзвінок, його потрібно скидати на кожен початковий дзвінок. Це призводить до складного питання: як сказати, коли рекурсивна функція викликається з іншого контексту, в даному випадку глобальної функції проти.
RobG

Це лише приклад для того, щоб придумати думку. Це може бути змінено, запитуючи користувача про його потреби.
Холявко

@RobG Я не розумію вашого запитання. Рекурсивну функцію не можна викликати поза закриття, оскільки це внутрішня функція. Тож немає можливості чи необхідності диференціювати контекст, оскільки існує лише один можливий контекст
слебетман

@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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.