Śledź, ile razy wywołano funkcję rekurencyjną

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)

Powyższy kod przyjmuje liczbę całkowitą i redukuje ją do jednej cyfry, mnożąc ją przez własne cyfry.

Przykład to 39.

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

Konsola będzie rejestrować:

27 
14 
4

Jak sprawdzić, czy funkcja rekurencyjna została wywołana 3 razy?

Próbowałem dodać licznik, ale nie można go zaktualizować. Byłbym wdzięczny za każdą pomoc

chs242
źródło
4
.map(Number)jest zbędny, ponieważ *operator i tak zmusza wartości do numerowania. ;-)
RobG
4
Kilka pytań: 1) Jak zamierzasz radzić sobie z liczbami ujemnymi? Na przykład liczba -57jest naprawdę a -50i -7.. patrząc na to w ten sposób, zmniejszyłaby -5x, -7dając liczbę dodatnią 35. A może chcesz, aby widział tylko znak ujemny z, 5a nie z 7, nawet jeśli 7faktycznie jest również ujemny. 2) Jak zamierzasz radzić sobie z liczbami zawierającymi zero? ponieważ spowoduje to automatyczne wyzerowanie redukcji. Dlatego im większa liczba zostanie podana, tym większe prawdopodobieństwo, że wyzeruje się. Inną opcją byłoby pominięcie zer
Pimp Trizkit
3
Zdaję sobie sprawę, że moje powyższe pytania nie dotyczą zliczania rekurencji, ale raczej aspekt rozwiązywania zagadek wykorzystany w tym pytaniu. Proszę wybacz mi.
Pimp Trizkit
3
Pochlebia mi, że podoba ci się moja odpowiedź, ale ze względów praktycznych uważam, że stackoverflow.com/a/59570894/1346276 jest najczystszym wariantem ogólnym.
phipsgabler
2
@ phipsgabler każdemu, kto poświęca czas na napisanie inteligentnej i spójnej odpowiedzi, należy się podobać. Dziękuję
chs242

Odpowiedzi:

25

Jest to wariant niemal wyłącznie akademicki, ale w tym celu można użyć zmodyfikowanego kombinatora punktów stałych .

Skróćmy i popraw nieco swoją pierwotną funkcję:

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

// singleDigit(123234234) == 0

W tym wariancie możemy wyróżnić i zakryć rekurencyjne wezwanie:

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

Tej funkcji można teraz używać z kombinatorem punktów stałych; konkretnie zaimplementowałem kombinator Y dostosowany do (ścisłego) JavaScript w następujący sposób:

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

gdzie mamy Ynormal(singleDigitF, 123234234) == 0.

Teraz nadchodzi sztuczka. Ponieważ uwzględniliśmy rekurencję do kombinatora Y, możemy policzyć w niej liczbę rekurencji:

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

Szybkie sprawdzenie w węźle REPL daje:

> 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 ]

Ten kombinator będzie teraz działać w celu zliczania liczby wywołań w dowolnej funkcji rekurencyjnej zapisanej w stylu singleDigitF.

(Zauważ, że istnieją dwa źródła uzyskania zera jako bardzo częstej odpowiedzi: przepełnienie numeryczne ( 123345456999999999staje się 123345457000000000itp.) Oraz fakt, że prawie na pewno otrzymasz gdzieś zero jako wartość pośrednią, gdy rozmiar danych wejściowych rośnie.)

phipsgabler
źródło
6
Do downvoters: Naprawdę zgadzam się z tobą, że nie jest to najlepsze praktyczne rozwiązanie - dlatego przedłożyłem je jako „czysto akademickie”.
phipsgabler
Szczerze mówiąc, jest to niesamowite rozwiązanie i całkowicie pasuje do regresji / matematyki pierwotnego pytania.
Sheraff
73

Do definicji funkcji należy dodać argument licznika:

function singleDigit(num, counter = 0) {
    console.log(`called ${counter} times`)
    //...
    return singleDigit(number, counter+1)
}
singleDigit(39)
Sheraff
źródło
6
niesamowite. Wygląda na to, że mój licznik nie działał, ponieważ zadeklarowałem to w funkcji
chs242
7
@ reguły zakresu chs242 dyktują, że zadeklarowanie go w funkcji spowoduje utworzenie nowej przy każdym wywołaniu. stackoverflow.com/questions/500431/…
Taplar
10
@ chs242 to nie tak, że zadeklarowałeś to w ramach funkcji. Technicznie to wszystko, co robią również wszystkie parametry domyślne - w twoim przypadku po prostu wartość nigdy nie jest przenoszona do następnego wywołania funkcji rekurencyjnie. ae za każdym razem, gdy funkcja counterzostanie uruchomiona, zostanie zeskrobana i ustawiona na 0, chyba że zostanie to jawnie przeniesione w rekursywnym wywołaniu, tak jak robi to Sheraff. AesingleDigit(number, ++counter)
zfrisch
2
właśnie @zfrisch Teraz to rozumiem. Dzięki za
poświęcenie
35
Zmień ++counterna counter+1. Są funkcjonalnie równoważne, ale ta druga lepiej określa intencję, nie (niepotrzebnie) mutuje i parametryzuje oraz nie ma możliwości przypadkowego późniejszego zwiększenia. Lub jeszcze lepiej, ponieważ jest to wywołanie ogonowe, zamiast tego użyj pętli.
BlueRaja - Danny Pflughoeft
37

Tradycyjnym rozwiązaniem jest przekazanie licznika jako parametru do funkcji, jak sugeruje inna odpowiedź.

Istnieje jednak inne rozwiązanie w js. Kilka innych odpowiedzi sugerowało po prostu zadeklarowanie liczby poza funkcją rekurencyjną:

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

To oczywiście działa. Jednak powoduje to, że funkcja nie jest ponownie aktywowana (nie można jej wywołać dwukrotnie poprawnie). W niektórych przypadkach możesz zignorować ten problem i po prostu upewnij się, że nie dzwonisz singleDigitdwa razy (javascript jest jednowątkowy, więc nie jest to zbyt trudne), ale jest to błąd, który czeka się, jeśli zaktualizujesz singleDigitpóźniej, aby był asynchroniczny, a także czuje się brzydki.

Rozwiązaniem jest zadeklarowanie counterzmiennej na zewnątrz, ale nie globalnie. Jest to możliwe, ponieważ javascript ma zamknięcia:

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
}

Jest to podobne do globalnego rozwiązania, ale za każdym razem dzwonisz singleDigit(co jest teraz nie rekurencyjna funkcja) będzie utworzyć nową instancję counterzmiennej.

Slebetman
źródło
1
Zmienna licznika jest dostępna tylko w singleDigitfunkcji i zapewnia alternatywny czysty sposób na wykonanie tego bez przekazywania argumentu imo. +1
AndrewL64
1
Ponieważ recursionjest teraz całkowicie izolowany, przekazywanie licznika jako ostatniego parametru powinno być całkowicie bezpieczne. Nie sądzę, aby tworzenie funkcji wewnętrznej było konieczne. Jeśli nie podoba ci się pomysł posiadania parametrów dla wyłącznej korzyści rekurencji (uważam, że użytkownik mógł z nimi zadzierać), to zablokuj je za Function#bindpomocą częściowo zastosowanej funkcji.
komendant niestandardowy
@ customcommander Tak, wspomniałem o tym w podsumowaniu w pierwszej części mojej odpowiedzi - the traditional solution is to pass the count as a parameter. Jest to alternatywne rozwiązanie w języku zamkniętym. Pod pewnymi względami łatwiej jest podążać, ponieważ jest to tylko jedna zmienna zamiast prawdopodobnie nieskończonej liczby zmiennych instancji. Innymi słowy, znajomość tego rozwiązania pomaga, gdy
śledzoną
counter--byłoby tradycyjnym sposobem rozwiązania twojego roszczenia „nie można nazwać dwa razy poprawnie”
MonkeyZeus
1
@MonkeyZeus Jaka to różnica? Ponadto, skąd miałbyś wiedzieć, jaki numer zainicjować licznik, aby zobaczyć, że jest to liczba, którą chcemy znaleźć?
slebetman
22

Innym podejściem, ponieważ generujesz wszystkie liczby, jest użycie generatora.

Ostatnim elementem jest liczba nzredukowana do liczby jednocyfrowej. Aby zliczyć liczbę iteracji, po prostu przeczytaj długość tablicy.

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>


Końcowe przemyślenia

Możesz rozważyć możliwość wcześniejszego powrotu do funkcji. Wszelkie numery z zerem w nim będzie powrotu do zera.

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

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

To samo można powiedzieć o dowolnych wykonanych liczbach 1.

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

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

Na koniec nie wyjaśniłeś, czy akceptujesz tylko dodatnie liczby całkowite. Jeśli zaakceptujesz liczby całkowite ujemne, rzutowanie ich na ciągi znaków może być ryzykowne:

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

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

Możliwe rozwiązanie:

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

mult(39)
//=> 27

mult(-39)
//=> -27
niestandardowy dowódca
źródło
świetny. @ customcommander dziękuję za wyjaśnienie tego bardzo jasno
chs242
6

Było tu wiele interesujących odpowiedzi. Myślę, że moja wersja oferuje dodatkową interesującą alternatywę.

Robisz kilka rzeczy z wymaganą funkcją. Rekurencyjnie redukujesz go do jednej cyfry. Rejestrujesz wartości pośrednie i chcesz uzyskać liczbę wykonanych połączeń rekurencyjnych. Jednym ze sposobów radzenia sobie z tym wszystkim jest napisanie czystej funkcji, która zwróci strukturę danych zawierającą wynik końcowy, podjęte kroki i liczbę połączeń w jednym:

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

Możesz następnie zapisać kroki, jeśli chcesz, lub zapisać je do dalszego przetwarzania.

Oto wersja, która to robi:

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

Pamiętaj, że śledzimy, stepsale wyprowadzamy calls. Chociaż możemy śledzić liczbę połączeń za pomocą dodatkowego parametru, wydaje się, że nic nie zyskuje. Pomijamy również map(Number)krok - w każdym przypadku zostaną one wymuszone na liczby przez pomnożenie.

Jeśli masz obawy, że ten domyślny stepsparametr zostanie ujawniony jako część interfejsu API, łatwo go ukryć za pomocą funkcji wewnętrznej:

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, [])
}

W obu przypadkach wyodrębnienie mnożenia cyfr może być nieco czystsze:

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])
Scott Sauyet
źródło
2
Kolejna świetna odpowiedź;) Zauważ, że gdy n jest ujemne, digitProductzwróci NaN( -39 ~> ('-' * '3') * '9'). Możesz więc użyć wartości bezwzględnej n i użyć -1lub 1jako początkowej wartości swojej redukcji.
komendant niestandardowy
@ customcommander: w rzeczywistości wróci {"digit":-39,"steps":[-39],"calls":0}, ponieważ -39 < 9. Chociaż zgadzam się, że może to zrobić z pewną kontrolą błędów: czy parametr jest liczbą? - czy jest to dodatnia liczba całkowita? - itp. Nie sądzę, żebym to zaktualizował, aby to uwzględnić. Przechwytuje to algorytm, a obsługa błędów jest często specyficzna dla bazy kodu użytkownika.
Scott Sauyet
6

Jeśli próbujesz po prostu policzyć, ile razy się zmniejsza, i nie przejmujesz się konkretnie rekurencją ... możesz po prostu usunąć rekurencję. Poniższy kod pozostaje wierny oryginalnemu wpisowi, ponieważ nie jest uważany num <= 9za wymagający zmniejszenia. Dlatego singleDigit(8)będzie miał count = 0i singleDigit(39)będzie miał count = 3, podobnie jak PO i zaakceptowana odpowiedź pokazują:

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

Przetwarzanie liczb 9 lub mniejszych (tj. num <= 9) Nie jest konieczne . Niestety kod OP będzie przetwarzany, num <= 9nawet jeśli go nie liczy. Powyższy kod w ogóle nie będzie przetwarzany ani liczony num <= 9. Po prostu mi to przekazuje.

Zdecydowałem się nie używać, .reduceponieważ wykonanie faktycznej matematyki było znacznie szybsze do wykonania. I dla mnie łatwiejsze do zrozumienia.


Dalsze myślenie o prędkości

Uważam, że dobry kod jest również szybki. Jeśli używasz tego typu redukcji (która jest często używana w numerologii), być może będziesz musiał użyć go na ogromnej ilości danych. W takim przypadku prędkość będzie najważniejsza.

Używanie obu .map(Number)i console.log(na każdym etapie redukcji) jest bardzo długie do wykonania i niepotrzebne. Samo usunięcie .map(Number)z PO przyspieszyło go o około 4,38x. Usunięcie console.logprzyspieszyło tak bardzo, że prawie niemożliwe było prawidłowe przetestowanie (nie chciałem na to czekać).

Tak więc, podobnie jak odpowiedź customcommandera , nieużywanie .map(Number)ani nie console.logwypychanie wyników do tablicy i używanie .lengthdla countjest znacznie szybsze. Niestety dla customcommander „s odpowiedź, stosując funkcję generatora jest naprawdę bardzo powolny (które odpowiedź jest około 2.68x wolniej niż OP bez .map(Number)i console.log)

Zamiast tego .reduceużyłem właśnie faktycznej matematyki. Już ta pojedyncza zmiana przyspieszyła moją wersję funkcji o współczynnik 3,59x.

Wreszcie rekurencja jest wolniejsza, zajmuje miejsce na stosie, zużywa więcej pamięci i ma limit liczby powtórzeń. Lub, w tym przypadku, ile kroków redukcji można użyć, aby zakończyć pełną redukcję. Wdrożenie rekurencji w pętle iteracyjne utrzymuje to wszystko w tym samym miejscu na stosie i nie ma teoretycznego limitu liczby kroków redukcji, których można użyć do ukończenia. W związku z tym funkcje te mogą tutaj „zmniejszyć” liczbę całkowitą o dowolnej wielkości, ograniczoną jedynie czasem wykonania i długością tablicy.

Wszystko to na uwadze ...

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

Powyższa funkcja działa bardzo szybko. Jest to około 3.13x szybszy niż OP (bez .map(Number)i console.log) i około 8.4x szybszy niż customcommander „s odpowiedzi. Należy pamiętać, że usunięcie console.logz PO uniemożliwia wygenerowanie liczby na każdym etapie redukcji. Stąd potrzeba tutaj wypchnięcia tych wyników do tablicy.

PT

Pimp Trizkit
źródło
1
Ta odpowiedź zawiera wiele wartości edukacyjnych, więc dziękuję za to. I feel good code is also fast.Powiedziałbym, że jakość kodu musi być mierzona względem wcześniej określonego zestawu wymagań. Jeśli wydajność nie jest jedną z nich, wówczas nic nie zyskasz, zastępując kod, który każdy może zrozumieć, kodem „szybkim”. Nie uwierzyłbyś, że ilość kodu, który widziałem, została zmieniona tak, aby była wydajna do tego stopnia, że ​​nikt już go nie rozumie (z jakiegoś powodu optymalny kod jest również nieudokumentowany;). Na koniec pamiętaj, że leniwie generowane listy pozwalają spożywać przedmioty na żądanie.
komendant niestandardowy
Dziękuję myślę. IMHO, czytanie faktycznej matematyki tego, jak to zrobić, było dla mnie łatwiejsze do zrozumienia .. niż [...num+''].map(Number).reduce((x,y)=> {return x*y})nawet [...String(num)].reduce((x,y)=>x*y)oświadczenia, które widzę w większości odpowiedzi tutaj. Dla mnie miało to więc dodatkową zaletę lepszego zrozumienia tego, co dzieje się przy każdej iteracji i znacznie szybciej. Tak, zminimalizowany kod (który ma swoje miejsce) jest strasznie trudny do odczytania. Ale w takich przypadkach na ogół świadomie nie dba się o jego czytelność, a jedynie końcowy wynik cięcia, wklejania i przenoszenia.
Pimp Trizkit
Czy JavaScript nie ma podziału na liczby całkowite, więc możesz zrobić odpowiednik C digit = num%10; num /= 10;? Konieczność zrobienia num - xpierwszego, aby usunąć końcową cyfrę przed dzieleniem, prawdopodobnie zmusi kompilator JIT do wykonania oddzielnego podziału od tego, który zrobił, aby uzyskać resztę.
Peter Cordes
Nie sądzę, są varto (JS nie ma int). Dlatego w razie potrzeby n /= 10;zamieni nsię na liczbę zmiennoprzecinkową. num = num/10 - x/10może przekształcić go w liczbę zmiennoprzecinkową, która jest długą formą równania. Dlatego muszę użyć refaktoryzowanej wersji, num = (num-x)/10;aby zachować liczbę całkowitą. Nie ma sposobu, aby znaleźć w JavaScript, który może dać zarówno iloraz, jak i pozostałą część operacji pojedynczego podziału. Są też digit = num%10; num /= 10;dwie osobne instrukcje, a zatem dwie osobne operacje podziału. Minęło trochę czasu, odkąd użyłem C, ale myślałem, że to również prawda.
Pimp Trizkit
6

Dlaczego nie zadzwonić do console.countswojej funkcji?

Edytuj: Snippet, aby wypróbować w przeglądarce:

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)

Mam go w Chrome 79 i Firefox 72

Mistermatt
źródło
console.count nie pomogłoby, ponieważ licznik jest resetowany za każdym razem, gdy funkcja jest wywoływana (jak wyjaśniono w odpowiedziach powyżej)
chs242
2
Nie rozumiem twojego problemu, ponieważ
działam
6

Możesz użyć do tego zamknięcia.

Wystarczy zapisać counterw zamknięciu funkcji.

Oto przykład:

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
źródło
1
Ale w ten sposób licznik ciągle liczy na następne połączenie, należy go zresetować przy każdym pierwszym połączeniu. Prowadzi to do trudnego pytania: jak stwierdzić, kiedy funkcja rekurencyjna jest wywoływana z innego kontekstu, w tym przypadku funkcji globalnej vs.
RobG
To tylko przykład na wymyślenie myśli. Można go zmodyfikować, pytając użytkownika o jego potrzeby.
Kholiavko
@RobG Nie rozumiem twojego pytania. Funkcji rekurencyjnej nie można wywołać poza zamknięciem, ponieważ jest to funkcja wewnętrzna. Zatem nie ma możliwości ani potrzeby różnicowania kontekstu, ponieważ istnieje tylko jeden możliwy kontekst
slebetman
@slebetman Licznik nigdy się nie resetuje. Zwracana funkcja singleDigitDecorator()będzie zwiększać ten sam licznik za każdym razem, gdy zostanie wywołana.
komendant niestandardowy
1
@ slebetman - problem polega na tym, że funkcja zwrócona przez singleDigitDecorator nie resetuje licznika po ponownym wywołaniu. Jest to funkcja, która musi wiedzieć, kiedy zresetować licznik, w przeciwnym razie dla każdego użycia wymagana jest nowa instancja funkcji. Możliwy przypadek użycia dla Function.caller ? ;-)
RobG
1

Oto wersja Python, która wykorzystuje funkcję otoki w celu uproszczenia licznika, jak sugeruje odpowiedź slebetman - piszę to tylko dlatego, że podstawowa idea jest bardzo jasna w tej implementacji:

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)
Łukasz Sawczak
źródło
1
W Pythonie, wolałbym coś takiego jak ten .
phipsgabler