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
javascript
recursion
chs242
źródło
źródło
.map(Number)
jest zbędny, ponieważ*
operator i tak zmusza wartości do numerowania. ;-)-57
jest naprawdę a-50
i-7
.. patrząc na to w ten sposób, zmniejszyłaby-5
x,-7
dając liczbę dodatnią35
. A może chcesz, aby widział tylko znak ujemny z,5
a nie z7
, nawet jeśli7
faktycznie 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 zerOdpowiedzi:
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ę:
W tym wariancie możemy wyróżnić i zakryć rekurencyjne wezwanie:
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:
gdzie mamy
Ynormal(singleDigitF, 123234234) == 0
.Teraz nadchodzi sztuczka. Ponieważ uwzględniliśmy rekurencję do kombinatora Y, możemy policzyć w niej liczbę rekurencji:
Szybkie sprawdzenie w węźle REPL daje:
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 (
123345456999999999
staje się123345457000000000
itp.) Oraz fakt, że prawie na pewno otrzymasz gdzieś zero jako wartość pośrednią, gdy rozmiar danych wejściowych rośnie.)źródło
Do definicji funkcji należy dodać argument licznika:
źródło
counter
zostanie uruchomiona, zostanie zeskrobana i ustawiona na0
, chyba że zostanie to jawnie przeniesione w rekursywnym wywołaniu, tak jak robi to Sheraff. AesingleDigit(number, ++counter)
++counter
nacounter+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.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ą:
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
singleDigit
dwa razy (javascript jest jednowątkowy, więc nie jest to zbyt trudne), ale jest to błąd, który czeka się, jeśli zaktualizujeszsingleDigit
później, aby był asynchroniczny, a także czuje się brzydki.Rozwiązaniem jest zadeklarowanie
counter
zmiennej na zewnątrz, ale nie globalnie. Jest to możliwe, ponieważ javascript ma zamknięcia: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ęcounter
zmiennej.źródło
singleDigit
funkcji i zapewnia alternatywny czysty sposób na wykonanie tego bez przekazywania argumentu imo. +1recursion
jest 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 zaFunction#bind
pomocą częściowo zastosowanej funkcji.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, gdycounter--
byłoby tradycyjnym sposobem rozwiązania twojego roszczenia „nie można nazwać dwa razy poprawnie”Innym podejściem, ponieważ generujesz wszystkie liczby, jest użycie generatora.
Ostatnim elementem jest liczba
n
zredukowana do liczby jednocyfrowej. Aby zliczyć liczbę iteracji, po prostu przeczytaj długość tablicy.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.
To samo można powiedzieć o dowolnych wykonanych liczbach
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:
Możliwe rozwiązanie:
źródło
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:
Możesz następnie zapisać kroki, jeśli chcesz, lub zapisać je do dalszego przetwarzania.
Oto wersja, która to robi:
Pamiętaj, że śledzimy,
steps
ale wyprowadzamycalls
. 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
steps
parametr zostanie ujawniony jako część interfejsu API, łatwo go ukryć za pomocą funkcji wewnętrznej:W obu przypadkach wyodrębnienie mnożenia cyfr może być nieco czystsze:
źródło
digitProduct
zwróciNaN
(-39 ~> ('-' * '3') * '9'
). Możesz więc użyć wartości bezwzględnej n i użyć-1
lub1
jako początkowej wartości swojej redukcji.{"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.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 <= 9
za wymagający zmniejszenia. DlategosingleDigit(8)
będzie miałcount = 0
isingleDigit(39)
będzie miałcount = 3
, podobnie jak PO i zaakceptowana odpowiedź pokazują:Przetwarzanie liczb 9 lub mniejszych (tj.
num <= 9
) Nie jest konieczne . Niestety kod OP będzie przetwarzany,num <= 9
nawet jeśli go nie liczy. Powyższy kod w ogóle nie będzie przetwarzany ani liczonynum <= 9
. Po prostu mi to przekazuje.Zdecydowałem się nie używać,
.reduce
ponieważ 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)
iconsole.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ęcieconsole.log
przyspieszył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 nieconsole.log
wypychanie wyników do tablicy i używanie.length
dlacount
jest 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)
iconsole.log
)Zamiast tego
.reduce
uż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 ...
Powyższa funkcja działa bardzo szybko. Jest to około 3.13x szybszy niż OP (bez
.map(Number)
iconsole.log
) i około 8.4x szybszy niż customcommander „s odpowiedzi. Należy pamiętać, że usunięcieconsole.log
z PO uniemożliwia wygenerowanie liczby na każdym etapie redukcji. Stąd potrzeba tutaj wypchnięcia tych wyników do tablicy.PT
źródło
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.[...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.digit = num%10;
num /= 10;
? Konieczność zrobienianum - x
pierwszego, aby usunąć końcową cyfrę przed dzieleniem, prawdopodobnie zmusi kompilator JIT do wykonania oddzielnego podziału od tego, który zrobił, aby uzyskać resztę.var
to (JS nie maint
). Dlatego w razie potrzebyn /= 10;
zamienin
się na liczbę zmiennoprzecinkową.num = num/10 - x/10
moż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.Dlaczego nie zadzwonić do
console.count
swojej funkcji?Edytuj: Snippet, aby wypróbować w przeglądarce:
Mam go w Chrome 79 i Firefox 72
źródło
Możesz użyć do tego zamknięcia.
Wystarczy zapisać
counter
w zamknięciu funkcji.Oto przykład:
źródło
singleDigitDecorator()
będzie zwiększać ten sam licznik za każdym razem, gdy zostanie wywołana.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:
źródło