Tieni traccia di quante volte è stata chiamata una funzione ricorsiva


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)

Il codice sopra prende un numero intero e lo riduce a una singola cifra moltiplicandolo per le sue stesse cifre.

L'esempio è 39.

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

La console registrerà:

27 
14 
4

Come posso tenere traccia del fatto che la funzione ricorsiva è stata chiamata 3 volte?

Ho provato ad aggiungere un contatore ma non si aggiorna. Gradirei qualsiasi aiuto


4
.map(Number)è ridondante poiché l' *operatore obbliga comunque i valori a numerare. ;-)
RobG

4
Un paio di domande: 1) Come intendi trattare i numeri negativi? Ad esempio, il numero -57è davvero un -50e un -7.. se visto in questo modo, farebbe una riduzione di -5x -7dando un numero positivo 35. O vuoi che veda solo il segno negativo con il 5e non il 7, anche se anche quello 7è in realtà negativo. 2) Come intende trattare con numeri che includono uno zero? in quanto ciò azzererà automaticamente la riduzione. Pertanto, maggiore è il numero che passi, più è probabile che si azzeri. L'altra opzione sarebbe quella di saltare gli zeri
Pimp Trizkit

3
Mi rendo conto che le mie domande precedenti non riguardano il conteggio della ricorsione, ma piuttosto l'aspetto dell'enigma della risoluzione dei contenuti utilizzati in questa domanda. Ti prego, perdonami.
Pimp Trizkit,

3
Sono lusingato che ti piaccia la mia risposta, ma per scopi pratici, penso che stackoverflow.com/a/59570894/1346276 sia la variante generale più pulita.
phipsgabler

2
@phipsgabler chi si prende il tempo di scrivere una risposta intelligente e coerente merita un like. Grazie
chs242 il

Risposte:


25

Questa è una variante quasi puramente accademica, ma è possibile utilizzare un combinatore a virgola fissa modificato per questo scopo.

Consente di abbreviare e migliorare un po 'la funzione originale:

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

// singleDigit(123234234) == 0

Da questa variante, possiamo fattorizzare ed elaborare il richiamo ricorsivo:

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

Questa funzione può ora essere utilizzata con un combinatore a punto fisso; in particolare ho implementato un combinatore Y adattato per JavaScript (rigoroso) come segue:

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

dove abbiamo Ynormal(singleDigitF, 123234234) == 0.

Ora arriva il trucco. Poiché abbiamo considerato la ricorsione al combinatore Y, possiamo contare il numero di ricorsioni al suo interno:

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

Un rapido controllo nel REPL del nodo fornisce:

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

Questo combinatore ora funzionerà per contare il numero di chiamate in qualsiasi funzione ricorsiva scritta nello stile di singleDigitF.

(Nota che ci sono due fonti per ottenere zero come risposta molto frequente: overflow numerico ( 123345456999999999diventando 123345457000000000ecc.), E il fatto che quasi sicuramente otterrai zero come valore intermedio da qualche parte, quando la dimensione dell'input sta crescendo.)


6
Per i downvoter: sono davvero d'accordo con te sul fatto che questa non è la migliore soluzione pratica, per questo l'ho prefissata come "puramente accademica".
phipsgabler

Onestamente, è una soluzione fantastica e totalmente adatta al tipo di regressione / matematica della domanda originale.
Sheraff,

73

È necessario aggiungere un argomento contatore alla definizione della funzione:

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

6
eccezionale. Sembra che il mio contatore non funzionasse perché l'ho dichiarato nella funzione
chs242

7
Le regole di ambito di @ chs242 detterebbero che dichiarandolo nella funzione ne creerebbe uno nuovo ogni invocazione. stackoverflow.com/questions/500431/...
Taplar

10
@ chs242 non è che l'hai dichiarato all'interno della funzione. Tecnicamente anche tutti i parametri predefiniti stanno funzionando: nel tuo caso è semplicemente che il valore non è mai stato riportato alla volta successiva in cui la funzione è stata chiamata in modo ricorsivo. ogni volta che la funzione viene counterscartata e impostata su 0, a meno che non la porti esplicitamente nella tua chiamata ricorsiva come fa Sheraff. AesingleDigit(number, ++counter)
zfrisch

2
giusto @zfrisch lo capisco ora. Grazie per aver
dedicato

35
Per favore, cambia ++counterin counter+1. Sono funzionalmente equivalenti, ma quest'ultimo specifica meglio l'intento, non cambia (inutilmente) i parametri e i parametri e non ha la possibilità di post-incremento accidentale. O meglio ancora, dato che è un richiamo, usa invece un loop.
BlueRaja - Danny Pflughoeft il

37

La soluzione tradizionale è passare il conteggio come parametro alla funzione come suggerito da un'altra risposta.

Tuttavia, c'è un'altra soluzione in js. Alcune altre risposte suggeriscono semplicemente di dichiarare il conteggio al di fuori della funzione ricorsiva:

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

Questo ovviamente funziona. Tuttavia, ciò rende la funzione non rientrante (non può essere richiamata due volte correttamente). In alcuni casi puoi ignorare questo problema e assicurarti semplicemente di non chiamare singleDigitdue volte (javascript è a thread singolo, quindi non è troppo difficile da fare) ma questo è un bug in attesa che si verifichi se aggiorni in singleDigitseguito per essere asincrono e sembra anche brutto.

La soluzione è dichiarare la countervariabile all'esterno ma non a livello globale. Questo è possibile perché javascript ha chiusure:

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
}

Questo è simile alla soluzione globale ma ogni volta che chiami singleDigit(che ora non è una funzione ricorsiva) creerà una nuova istanza della countervariabile.


1
La variabile contatore è disponibile solo all'interno della singleDigitfunzione e fornisce un modo alternativo alternativo per farlo senza passare un argomento imo. +1
AndrewL64

1
Poiché recursionora è completamente isolato, dovrebbe essere totalmente sicuro passare il contatore come ultimo parametro. Non credo sia necessario creare una funzione interiore. Se non ti piace l'idea di avere parametri a beneficio esclusivo della ricorsione (prendo il punto che l'utente potrebbe confonderli), bloccali con Function#binduna funzione parzialmente applicata.
comandante personalizzato il

@customcommander Sì, l'ho menzionato in sintesi nella prima parte della mia risposta - the traditional solution is to pass the count as a parameter. Questa è una soluzione alternativa in una lingua con chiusure. In un certo senso è più semplice da seguire perché è solo una variabile anziché un numero forse infinito di istanze variabili. In altri modi, conoscere questa soluzione aiuta quando l'elemento che stai monitorando è un oggetto condiviso (immagina di costruire una mappa unica) o un oggetto molto grande (come una stringa HTML)
slebetman

counter--sarebbe il modo tradizionale di risolvere la tua affermazione di "non può essere chiamato due volte correttamente"
MonkeyZeus

1
@MonkeyZeus Che differenza fa? Inoltre, come faresti a sapere quale numero inizializzare il contatore per vedere che è il conteggio che vogliamo trovare?
Slebetman,

22

Un altro approccio, poiché si producono tutti i numeri, è quello di utilizzare un generatore.

L'ultimo elemento è il tuo numero nridotto a un numero a una cifra e per contare quante volte hai ripetuto, leggi la lunghezza dell'array.

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>


Pensieri finali

Potresti considerare di avere una condizione di ritorno precoce nella tua funzione. Eventuali numeri con uno zero in esso saranno restituire zero.

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

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

Lo stesso si può dire per qualsiasi numero composto 1solo da.

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

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

Infine, non hai chiarito se accetti solo numeri interi positivi. Se si accettano numeri interi negativi, il loro cast in stringhe può essere rischioso:

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

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

Possibile soluzione:

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

mult(39)
//=> 27

mult(-39)
//=> -27

grande. @customcommander grazie per averlo spiegato molto chiaramente
chs242,

6

Ci sono state molte risposte interessanti qui. Penso che la mia versione offra un'ulteriore alternativa interessante.

Fai diverse cose con la tua funzione richiesta. Lo riduci ricorsivamente a una singola cifra. Si registrano i valori intermedi e si desidera un conteggio delle chiamate ricorsive effettuate. Un modo per gestire tutto ciò è scrivere una funzione pura che restituirà una struttura di dati che contiene il risultato finale, i passaggi effettuati e la chiamata contano tutti in uno:

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

È quindi possibile registrare i passaggi, se lo si desidera, o memorizzarli per ulteriori elaborazioni.

Ecco una versione che lo fa:

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

Si noti che seguiamo il stepsma deriviamo il calls. Mentre potremmo tracciare il conteggio delle chiamate con un parametro aggiuntivo, questo sembra non guadagnare nulla. Saltiamo anche il map(Number)passaggio: questi saranno costretti a numeri in ogni caso dalla moltiplicazione.

Se hai dubbi sul fatto che quel stepsparametro predefinito sia esposto come parte dell'API, è abbastanza facile nasconderlo utilizzando una funzione interna come questa:

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

E in entrambi i casi, potrebbe essere un po 'più pulito estrarre la moltiplicazione delle cifre in una funzione di supporto:

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
Un'altra ottima risposta;) Nota che quando n è negativo, digitProductrestituirà NaN( -39 ~> ('-' * '3') * '9'). Quindi potresti voler usare un valore assoluto di n e usare -1o 1come valore iniziale di riduzione.
comandante personalizzato il

@customcommander: in realtà, tornerà {"digit":-39,"steps":[-39],"calls":0}, da allora -39 < 9. Mentre sono d'accordo che questo potrebbe fare con qualche controllo degli errori: il parametro è un numero? - è un numero intero positivo? - ecc. Non credo che aggiornerò per includerlo. Questo cattura l'algoritmo e la gestione degli errori è spesso specifica della propria base di codice.
Scott Sauyet,

6

Se stai solo cercando di contare quante volte viene ridotto e non ti preoccupi specificamente della ricorsione ... puoi semplicemente rimuovere la ricorsione. Il codice seguente rimane fedele alla Posta originale in quanto non viene considerato num <= 9come necessitante riduzione. Pertanto, singleDigit(8)avranno count = 0e singleDigit(39)avranno count = 3, proprio come stanno dimostrando l'OP e la risposta accettata:

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

Non è necessario elaborare i numeri 9 o meno (ad es. num <= 9). Sfortunatamente il codice OP elaborerà num <= 9anche se non lo conta. Il codice sopra non elaborerà né conta num <= 9affatto. Lo passa solo attraverso.

Ho scelto di non usare .reduceperché eseguire la matematica effettiva era molto più veloce da eseguire. E, per me, più facile da capire.


Ulteriore riflessione sulla velocità

Sento che anche il buon codice è veloce. Se stai usando questo tipo di riduzione (che è molto usato in numerologia) potrebbe essere necessario utilizzarlo su una grande quantità di dati. In questo caso, la velocità diventerà la massima importanza.

L'uso di entrambi .map(Number)e console.log(ad ogni passo della riduzione) è molto lungo da eseguire e non necessario. La semplice cancellazione .map(Number)dall'OP lo ha accelerato di circa 4,38x. Eliminazioneconsole.log accelerato così tanto che era quasi impossibile testarlo correttamente (non volevo aspettarlo).

Quindi, simile alla risposta del committente personalizzato , non usare .map(Number)trasferireconsole.log i risultati in un array e usare .lengthfor countè molto più veloce. Sfortunatamente per la risposta del committente personalizzato , l'utilizzo di una funzione del generatore è davvero molto lento (tale risposta è circa 2,68 volte più lenta dell'OP senza .map(Number)econsole.log )

Inoltre, invece di usare .reduceho appena usato la matematica attuale. Solo questa singola modifica ha velocizzato la mia versione della funzione di un fattore di 3,59x.

Infine, la ricorsione è più lenta, occupa spazio nello stack, utilizza più memoria e ha un limite a quante volte può "ripetersi". Oppure, in questo caso, quanti passaggi di riduzione può utilizzare per completare la riduzione completa. Distribuire la tua ricorsione in loop iterativi mantiene tutto nello stesso posto nello stack e non ha limiti teorici su quanti passi di riduzione può usare per finire. Pertanto, queste funzioni qui possono "ridurre" quasi qualsiasi numero intero di dimensioni, limitato solo dal tempo di esecuzione e dalla lunghezza di un array.

Tutto questo in mente ...

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

La funzione sopra è estremamente veloce. È circa 3,13 volte più veloce dell'OP (senza .map(Number)e console.log) e circa 8,4 volte più veloce della risposta del committente personalizzato . Tenere presente che l'eliminazione console.logdall'OP impedisce che produca un numero in ogni fase della riduzione. Quindi, la necessità qui di inserire questi risultati in un array.

PT


1
C'è molto valore educativo in questa risposta, quindi grazie per quello. I feel good code is also fast.Direi che la qualità del codice deve essere misurata rispetto a un insieme predefinito di requisiti. Se le prestazioni non sono una di queste, non ottieni nulla sostituendo il codice che chiunque può capire con il codice "veloce". Non crederesti che la quantità di codice che ho visto che è stato refactored per essere performante al punto che nessuno può più capirlo (per qualche ragione anche il codice ottimale tende a non essere documentato;). Infine, tieni presente che le liste generate in modo pigro consentono di consumare oggetti su richiesta.
comandante personalizzato il

Grazie, penso. IMHO, leggendo la matematica reale su come farlo è stato più facile da capire per me .. rispetto alle dichiarazioni [...num+''].map(Number).reduce((x,y)=> {return x*y})o anche [...String(num)].reduce((x,y)=>x*y)che sto vedendo nella maggior parte delle risposte qui. Quindi, per me, questo ha avuto l'ulteriore vantaggio di una migliore comprensione di ciò che accade ad ogni iterazione e molto più velocemente. Sì, il codice minimizzato (che ha il suo posto) è terribilmente difficile da leggere. Ma in quei casi generalmente non ci si preoccupa consapevolmente della sua leggibilità ma piuttosto del risultato finale di tagliare e incollare e andare avanti.
Pimp Trizkit,

JavaScript non ha una divisione intera, quindi puoi fare l'equivalente di C digit = num%10; num /= 10;? Dover donum - x prima di rimuovere la cifra finale prima della divisione probabilmente forzerà il compilatore JIT a fare una divisione separata da quella che ha fatto per ottenere il resto.
Peter Cordes,

Io non la penso così var s (JS non ha ints). Pertanto, n /= 10;si convertirà nin un float se necessario. num = num/10 - x/10potrebbe convertirlo in un float, che è la forma lunga dell'equazione. Quindi, devo usare la versione refactored num = (num-x)/10;per mantenerlo un numero intero. Non c'è modo che io possa trovare in JavaScript che possa darti sia il quoziente che il resto di una singola divisione. Inoltre, ci digit = num%10; num /= 10;sono due dichiarazioni separate e quindi due operazioni di divisione separate. È da un po 'che non uso C, ma ho pensato che fosse vero anche lì.
Pimp Trizkit

6

Perché non effettuare una chiamata console.countnella tua funzione?

Modifica: Snippet da provare nel tuo browser:

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)

Lo faccio funzionare in Chrome 79 e Firefox 72


console.count non aiuterebbe poiché il contatore viene resettato ogni volta che viene chiamata la funzione (come è stato spiegato nelle risposte sopra)
chs242

2
Non capisco il tuo problema, dato che funziona su Chrome e Firefox, ho aggiunto uno snippet nella mia risposta
Mistermatt

6

È possibile utilizzare la chiusura per questo.

Basta semplicemente riporlo counternella chiusura della funzione.

Ecco un esempio:

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
Ma in questo modo il contatore continua a contare sulla chiamata successiva, deve essere resettato ad ogni chiamata iniziale. Porta a una domanda difficile: come capire quando viene chiamata una funzione ricorsiva da un contesto diverso, in questo caso globale vs funzione.
RobG

Questo è solo un esempio per elaborare un pensiero. Potrebbe essere modificato chiedendo all'utente le sue esigenze.
Kholiavko,

@RobG Non capisco la tua domanda. La funzione ricorsiva non può essere chiamata al di fuori della chiusura perché è una funzione interna. Quindi non vi è alcuna possibilità o necessità di differenziare il contesto perché esiste un solo contesto possibile
slebetman

@slebetman Il contatore non viene mai ripristinato. La funzione restituita da singleDigitDecorator()continuerà a incrementare lo stesso contatore ogni volta che viene chiamata.
comandante personalizzato il

1
@ slebetman: il problema è che la funzione restituita da singleDigitDecorator non reimposta il suo contatore quando viene chiamato di nuovo. Questa è la funzione che deve sapere quando ripristinare il contatore, altrimenti è necessaria una nuova istanza della funzione per ogni utilizzo. Un possibile caso d'uso per Function.caller ? ;-)
RobG

1

Ecco una versione di Python che utilizza una funzione wrapper per semplificare il contatore, come è stato suggerito dalla risposta di slebetman: scrivo questo solo perché l'idea di base è molto chiara in questa implementazione:

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
In Python, io preferirei qualcosa di simile a questo .
phipsgabler
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.