Gardez une trace du nombre de fois qu'une fonction récursive a été appelée

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)

Le code ci-dessus prend un entier et le réduit à un seul chiffre en le multipliant par ses propres chiffres.

L'exemple est 39.

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

La console enregistrera:

27 
14 
4

Comment savoir si la fonction récursive a été appelée 3 fois?

J'ai essayé d'ajouter un compteur mais il ne se met pas à jour. J'apprécierais toute aide

chs242
la source
4
.map(Number)est redondant puisque l' *opérateur contraint les valeurs à numéroter de toute façon. ;-)
RobG
4
Quelques questions: 1) Comment comptez-vous gérer les nombres négatifs? Par exemple, le nombre -57est vraiment un -50et un -7.. quand on regarde de cette façon, cela ferait une réduction de -5x -7donnant un nombre positif 35. Ou voulez-vous qu'il ne voit que le signe négatif avec le 5et non le 7, même si le 7est également négatif. 2) Comment comptez-vous traiter les nombres qui incluent un zéro? car cela annulera automatiquement la réduction. Par conséquent, plus vous transmettez un nombre élevé, plus il est probable qu'il sera mis à zéro. L'autre option serait de sauter les zéros
Pimp Trizkit
3
Je me rends compte que mes questions ci-dessus ne concernent pas le comptage de la récursivité, mais plutôt l'aspect de résolution de casse-tête du contenu utilisé dans cette question. S'il vous plaît, pardonnez-moi.
Pimp Trizkit
3
Je suis flatté que vous aimiez ma réponse, mais à des fins pratiques, je pense que stackoverflow.com/a/59570894/1346276 est la variante générale la plus propre.
phipsgabler
2
@phipsgabler toute personne qui prend le temps d'écrire une réponse intelligente et cohérente mérite le même coup. Merci
chs242

Réponses:

25

Il s'agit d'une variante presque purement académique, mais vous pouvez utiliser un combinateur à virgule fixe modifié à cet effet.

Permet de raccourcir et d'améliorer un peu votre fonction d'origine:

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

// singleDigit(123234234) == 0

À partir de cette variante, nous pouvons factoriser et curry l'appel récursif:

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

Cette fonction peut désormais être utilisée avec un combinateur à virgule fixe; spécifiquement, j'ai implémenté un combinateur Y adapté pour (strict) JavaScript comme suit:

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

où nous avons Ynormal(singleDigitF, 123234234) == 0.

Maintenant vient le tour. Puisque nous avons factorisé la récursivité vers le combinateur Y, nous pouvons compter le nombre de récursions en son sein:

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

Une vérification rapide dans le Node REPL donne:

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

Ce combinateur fonctionnera maintenant pour compter le nombre d'appels dans n'importe quelle fonction récursive écrite dans le style de singleDigitF.

(Notez qu'il existe deux sources d'obtention de zéro comme réponse très fréquente: le débordement numérique ( 123345456999999999devenant 123345457000000000etc.) et le fait que vous obtiendrez presque sûrement zéro comme valeur intermédiaire quelque part, lorsque la taille de l'entrée augmente.)

phipsgabler
la source
6
Aux détracteurs: je suis vraiment d'accord avec vous que ce n'est pas la meilleure solution pratique - c'est pourquoi je l'ai préfixée comme "purement académique".
phipsgabler
Honnêtement, c'est une solution impressionnante et totalement adaptée au type de régression / mathématique de la question d'origine.
Sheraff
73

Vous devez ajouter un argument de compteur à votre définition de fonction:

function singleDigit(num, counter = 0) {
    console.log(`called ${counter} times`)
    //...
    return singleDigit(number, counter+1)
}
singleDigit(39)
Sheraff
la source
6
impressionnant. Il semble que mon compteur ne fonctionnait pas car je l'ai déclaré dans la fonction
chs242
7
Les règles de portée @ chs242 dicteraient que sa déclaration dans la fonction en créerait une nouvelle à chaque appel. stackoverflow.com/questions/500431/…
Taplar
10
@ chs242 ce n'est pas que vous l'ayez déclaré dans la fonction. Techniquement, tous les paramètres par défaut fonctionnent également - dans votre cas, c'est simplement que la valeur n'a jamais été reportée à la prochaine fois que la fonction a été appelée de manière récursive. ae chaque fois que la fonction s'exécute counterest supprimée et définie sur 0, à moins que vous ne la reportiez explicitement dans votre appel récursif comme le fait Sheraff. AesingleDigit(number, ++counter)
zfrisch
2
à droite @zfrisch Je comprends cela maintenant. Merci d'avoir pris le temps de l'expliquer
chs242
35
Veuillez changer ++counterpour counter+1. Ils sont fonctionnellement équivalents, mais ce dernier spécifie mieux l'intention, ne change pas (inutilement) et ne paramètre pas, et n'a pas la possibilité de post-incrémentation accidentelle. Ou mieux encore, puisqu'il s'agit d'un appel de queue, utilisez plutôt une boucle.
BlueRaja - Danny Pflughoeft
37

La solution traditionnelle consiste à passer le compte en tant que paramètre à la fonction comme suggéré par une autre réponse.

Cependant, il existe une autre solution dans js. Quelques autres réponses suggèrent simplement de déclarer le nombre en dehors de la fonction récursive:

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

Cela fonctionne bien sûr. Cependant, cela rend la fonction non réentrante (ne peut pas être appelée deux fois correctement). Dans certains cas, vous pouvez ignorer ce problème et assurez-vous simplement de ne pas appeler singleDigitdeux fois (javascript est un thread unique donc ce n'est pas trop difficile à faire), mais c'est un bug qui attend de se produire si vous mettez singleDigità jour plus tard pour être asynchrone et il se sent également laid.

La solution consiste à déclarer la countervariable à l'extérieur mais pas globalement. Ceci est possible car javascript a des fermetures:

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
}

Ceci est similaire à la solution globale mais chaque fois que vous appelez singleDigit(ce qui n'est plus une fonction récursive), cela créera une nouvelle instance de la countervariable.

Slebetman
la source
1
La variable compteur est uniquement disponible dans la singleDigitfonction et fournit une autre façon propre de le faire sans passer un argument imo. +1
AndrewL64
1
Puisqu'il recursionest maintenant complètement isolé, il devrait être totalement sûr de passer le compteur comme dernier paramètre. Je ne pense pas que la création d'une fonction intérieure soit nécessaire. Si vous n'aimez pas l'idée d'avoir des paramètres au seul bénéfice de la récursivité (je prends bien note que l'utilisateur pourrait jouer avec ceux-ci), alors verrouillez-les avec Function#bindune fonction partiellement appliquée.
customcommander
@customcommander Oui, je l'ai mentionné en résumé dans la première partie de ma réponse - the traditional solution is to pass the count as a parameter. Il s'agit d'une solution alternative dans une langue qui a des fermetures. À certains égards, il est plus simple à suivre car il ne s'agit que d'une variable au lieu d'un nombre éventuellement infini d'instances de variables. À d'autres égards, connaître cette solution aide lorsque la chose que vous suivez est un objet partagé (imaginez construire une carte unique) ou un très grand objet (comme une chaîne HTML)
slebetman
counter--serait la manière traditionnelle de résoudre votre réclamation de "ne peut pas être appelé deux fois correctement"
MonkeyZeus
1
@MonkeyZeus Quelle différence cela fait-il? Aussi, comment sauriez-vous quel nombre initialiser le compteur pour voir que c'est le nombre que nous voulons trouver?
slebetman
22

Une autre approche, puisque vous produisez tous les nombres, consiste à utiliser un générateur.

Le dernier élément est votre nombre nréduit à un nombre à un seul chiffre et pour compter le nombre de fois que vous l'avez répété, il suffit de lire la longueur du tableau.

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>


Dernières pensées

Vous voudrez peut-être envisager d'avoir une condition de retour précoce dans votre fonction. Tous les chiffres avec un zéro dans ce sera revenir à zéro.

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

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

La même chose peut être dite pour tous les numéros composés 1uniquement de.

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

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

Enfin, vous n'avez pas précisé si vous n'acceptez que des entiers positifs. Si vous acceptez des entiers négatifs, les convertir en chaînes peut être risqué:

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

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

Solution possible:

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

mult(39)
//=> 27

mult(-39)
//=> -27
commande personnalisée
la source
génial. @customcommander merci d'avoir expliqué cela très clairement
chs242
6

Il y a eu beaucoup de réponses intéressantes ici. Je pense que ma version offre une alternative intéressante supplémentaire.

Vous faites plusieurs choses avec votre fonction requise. Vous le réduisez récursivement à un seul chiffre. Vous enregistrez les valeurs intermédiaires et vous souhaitez un décompte des appels récursifs effectués. Une façon de gérer tout cela est d'écrire une fonction pure qui renverra une structure de données qui contient le résultat final, les étapes prises et le nombre d'appels en un seul:

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

Vous pouvez ensuite enregistrer les étapes si vous le souhaitez ou les stocker pour un traitement ultérieur.

Voici une version qui fait ça:

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

Notez que nous suivons le stepsmais dérivons le calls. Bien que nous puissions suivre le nombre d'appels avec un paramètre supplémentaire, cela ne semble rien gagner. Nous sautons également l' map(Number)étape - ceux-ci seront de toute façon contraints aux nombres par la multiplication.

Si vous craignez que ce stepsparamètre par défaut soit exposé dans le cadre de votre API, il est assez facile de le masquer en utilisant une fonction interne comme celle-ci:

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

Et dans les deux cas, il pourrait être un peu plus propre d'extraire la multiplication des chiffres dans une fonction d'assistance:

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
la source
2
Une autre excellente réponse;) Veuillez noter que lorsque n est négatif, digitProductretournera NaN( -39 ~> ('-' * '3') * '9'). Donc, vous pouvez utiliser une valeur absolue de n et utiliser -1ou 1comme valeur initiale de votre réduction.
customcommander
@customcommander: en fait, il reviendra {"digit":-39,"steps":[-39],"calls":0}, depuis -39 < 9. Bien que je convienne que cela pourrait faire une vérification d'erreur: le paramètre est-il un nombre? - est-ce un entier positif? - etc. Je ne pense pas que je vais mettre à jour pour inclure cela. Cela capture l'algorithme et la gestion des erreurs est souvent spécifique à sa base de code.
Scott Sauyet
6

Si vous essayez simplement de compter combien de fois il est réduit et que vous ne vous souciez pas spécifiquement de la récursivité ... vous pouvez simplement supprimer la récursivité. Le code ci-dessous reste fidèle à la poste d'origine car il ne compte pas num <= 9comme nécessitant une réduction. Par conséquent, singleDigit(8)aura count = 0, et singleDigit(39)aura count = 3, tout comme le PO et la réponse acceptée démontrent:

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

Il n'est pas nécessaire de traiter les nombres 9 ou moins (c.-à-d. num <= 9). Malheureusement, le code OP sera traité num <= 9même s'il ne le compte pas. Le code ci-dessus ne traitera ni ne comptera pas num <= 9du tout. Il passe juste à travers.

J'ai choisi de ne pas l'utiliser, .reducecar l'exécution des calculs réels était beaucoup plus rapide à exécuter. Et, pour moi, plus facile à comprendre.


Réflexion sur la vitesse

Je pense que le bon code est également rapide. Si vous utilisez ce type de réduction (qui est beaucoup utilisé en numérologie), vous devrez peut-être l'utiliser sur une énorme quantité de données. Dans ce cas, la vitesse deviendra de la plus haute importance.

L'utilisation des deux .map(Number)et console.log(à chaque étape de réduction) est à la fois très très longue à exécuter et inutile. La simple suppression .map(Number)de l'OP l'a accéléré d'environ 4,38x. La suppression console.loga tellement accéléré qu'il était presque impossible de tester correctement (je ne voulais pas l'attendre).

Donc, comme pour la réponse de customcommander , ne pas utiliser .map(Number)ni console.loget pousser les résultats dans un tableau et utiliser .lengthfor countest beaucoup plus rapide. Malheureusement pour la réponse de customcommander , l'utilisation d'une fonction de générateur est vraiment très lente (cette réponse est environ 2.68x plus lente que l'OP sans .map(Number)et console.log)

En outre, au lieu d'utiliser, .reduceje viens d'utiliser les mathématiques réelles. Ce seul changement a accéléré à lui seul ma version de la fonction par un facteur de 3,59x.

Enfin, la récursivité est plus lente, elle prend de la place dans la pile, utilise plus de mémoire et a une limite au nombre de fois qu'elle peut "se reproduire". Ou, dans ce cas, combien d'étapes de réduction il peut utiliser pour terminer la réduction complète. Déployer votre récursivité en boucles itératives le maintient au même endroit sur la pile et n'a aucune limite théorique sur le nombre d'étapes de réduction qu'il peut utiliser pour terminer. Ainsi, ces fonctions peuvent ici "réduire" presque n'importe quel entier de taille, uniquement limité par le temps d'exécution et la longueur d'un tableau.

Tout cela à l'esprit ...

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 fonction ci-dessus est extrêmement rapide. Il est environ 3,13 fois plus rapide que l'OP (sans .map(Number)et console.log) et environ 8,4 fois plus rapide que la réponse de customcommander . Gardez à l'esprit que la suppression console.logde l'OP l'empêche de produire un nombre à chaque étape de réduction. D'où la nécessité ici de pousser ces résultats dans un tableau.

PT

Pimp Trizkit
la source
1
Il y a beaucoup de valeur éducative dans cette réponse, merci pour cela. I feel good code is also fast.Je dirais que la qualité du code doit être mesurée par rapport à un ensemble d'exigences prédéfinies. Si les performances n'en font pas partie, vous ne gagnez rien en remplaçant le code que tout le monde peut comprendre par du code "rapide". Vous ne croiriez pas la quantité de code que j'ai vu qui a été refactorisée pour être performante au point que personne ne peut plus la comprendre (pour une raison quelconque, le code optimal a également tendance à être non documenté;). Enfin, sachez que les listes générées paresseux permettent de consommer des articles à la demande.
customcommander
Merci je pense. À mon humble avis, la lecture des mathématiques réelles de la façon de le faire était plus facile à comprendre pour moi .. que les déclarations [...num+''].map(Number).reduce((x,y)=> {return x*y})ou même [...String(num)].reduce((x,y)=>x*y)que je vois dans la plupart des réponses ici. Donc, pour moi, cela a eu l'avantage supplémentaire de mieux comprendre ce qui se passe à chaque itération et beaucoup plus rapidement. Oui, le code minifié (qui a sa place) est terriblement difficile à lire. Mais dans ces cas, on ne se soucie généralement pas de sa lisibilité, mais plutôt du résultat final à couper et coller et passer à autre chose.
Pimp Trizkit
JavaScript n'a-t-il pas une division entière, vous pouvez donc faire l'équivalent de C digit = num%10; num /= 10;? Devoir faire d' num - xabord pour supprimer le chiffre de fin avant de diviser est susceptible de forcer le compilateur JIT à faire une division distincte de celui qu'il a fait pour obtenir le reste.
Peter Cordes
Je ne pense pas. Ce sont des vars (JS n'a pas de ints). Par conséquent, n /= 10;convertira nen un flottant si nécessaire. num = num/10 - x/10pourrait le convertir en un flottant, qui est la forme longue de l'équation. Par conséquent, je dois utiliser la version refactorisée num = (num-x)/10;pour le garder un entier.Il n'y a aucun moyen que je puisse trouver en JavaScript qui puisse vous donner à la fois le quotient et le reste d'une opération de division unique. En outre, il digit = num%10; num /= 10;y a deux déclarations distinctes et donc deux opérations de division distinctes. Cela fait un moment que je n'ai pas utilisé C, mais je pensais que c'était également le cas ici.
Pimp Trizkit
6

Pourquoi ne pas appeler console.countdans votre fonction?

Edit: Extrait à essayer dans votre navigateur:

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)

Je le fais fonctionner dans Chrome 79 et Firefox 72

Mistermatt
la source
console.count n'aiderait pas car le compteur est réinitialisé à chaque appel de la fonction (comme cela a été expliqué dans les réponses ci-dessus)
chs242
2
Je ne comprends pas votre problème car je le fais fonctionner dans Chrome et Firefox, j'ai ajouté un extrait dans ma réponse
Mistermatt
6

Vous pouvez utiliser la fermeture pour cela.

Stockez simplement counterdans la fermeture de la fonction.

Voici un exemple:

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
la source
1
Mais de cette façon, le compteur continue de compter sur le prochain appel, il doit être réinitialisé à chaque appel initial. Cela conduit à une question difficile: comment savoir quand une fonction récursive est appelée à partir d'un contexte différent, dans ce cas global vs fonction.
RobG
Ceci est juste un exemple pour arriver à une pensée. Il peut être modifié en demandant à l'utilisateur ses besoins.
Kholiavko
@RobG Je ne comprends pas votre question. La fonction récursive ne peut pas être appelée en dehors de la fermeture car il s'agit d'une fonction interne. Il n'y a donc aucune possibilité ni besoin de différencier le contexte car il n'y a qu'un seul contexte possible
slebetman
@slebetman Le compteur n'est jamais réinitialisé. La fonction renvoyée par singleDigitDecorator()continuera à incrémenter le même compteur à chaque appel.
customcommander
1
@ slebetman: le problème est que la fonction renvoyée par singleDigitDecorator ne réinitialise pas son compteur lorsqu'elle est appelée à nouveau. C'est la fonction qui doit savoir quand réinitialiser le compteur, sinon une nouvelle instance de la fonction est requise pour chaque utilisation. Un cas d'utilisation possible pour Function.caller ? ;-)
RobG
1

Voici une version Python qui utilise une fonction wrapper pour simplifier le compteur, comme cela a été suggéré par la réponse de slebetman - j'écris ceci uniquement parce que l'idée centrale est très claire dans cette implémentation:

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)
Luke Sawczak
la source
1
En Python, je préférerais quelque chose comme ça .
phipsgabler