Mantenga un registro de cuántas veces se llamó a una función recursiva

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)

El código anterior toma un número entero y lo reduce a un solo dígito multiplicándolo por sus propios dígitos.

El ejemplo es 39.

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

La consola registrará:

27 
14 
4

¿Cómo hago un seguimiento de que la función recursiva se llamó 3 veces?

He intentado agregar un contador pero no se actualiza. Agradecería cualquier ayuda

chs242
fuente
44
.map(Number)es redundante ya que el *operador obliga a los valores a numerar de todos modos. ;-)
RobG
44
Un par de preguntas: 1) ¿Cómo piensa tratar con números negativos? Por ejemplo, el número -57es realmente a -50y a -7... cuando se mira de esta manera, reduciría -5x -7dando un número positivo 35. ¿O desea que vea solo el signo negativo con el 5y no el 7, incluso aunque el en 7realidad también sea negativo? 2) ¿Cómo piensa tratar con números que incluyen un cero? ya que esto pondrá a cero automáticamente la reducción. Por lo tanto, cuanto mayor sea el número que ingrese, más probable será que se reduzca a cero. La otra opción sería omitir los ceros
Pimp Trizkit
3
Me doy cuenta de que mis preguntas anteriores no se refieren a contar la recursión, sino más bien al aspecto de resolución de acertijos del contenido utilizado en esta pregunta. Por favor perdoname.
Pimp Trizkit
3
Me siento halagado de que le guste mi respuesta, pero a efectos prácticos, creo que stackoverflow.com/a/59570894/1346276 es la variante general más limpia.
phipsgabler
2
@phipsgabler cualquier persona que se tome el tiempo para escribir una respuesta inteligente y coherente merece un me gusta. Gracias
chs242

Respuestas:

25

Esta es una variante casi puramente académica, pero puede usar un combinador de punto fijo modificado para este propósito.

Permite acortar y mejorar un poco tu función original:

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

// singleDigit(123234234) == 0

A partir de esta variante, podemos descomponer y cursar la llamada recursiva:

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

Esta función ahora se puede usar con un combinador de punto fijo; específicamente implementé un combinador Y adaptado para JavaScript (estricto) de la siguiente manera:

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

donde tenemos Ynormal(singleDigitF, 123234234) == 0.

Ahora viene el truco. Como hemos factorizado la recursividad al combinador Y, podemos contar el número de recursiones dentro de él:

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

Una comprobación rápida en el Node REPL proporciona:

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

Este combinador ahora funcionará para contar el número de llamadas en cualquier función recursiva escrita al estilo de singleDigitF.

(Tenga en cuenta que hay dos fuentes para obtener cero como respuesta muy frecuente: desbordamiento numérico ( 123345456999999999convertirse en 123345457000000000etc.) y el hecho de que casi seguramente obtendrá cero como valor intermedio en algún lugar, cuando el tamaño de la entrada está creciendo).

phipsgabler
fuente
66
Para los votantes negativos: estoy realmente de acuerdo con ustedes en que esta no es la mejor solución práctica, es por eso que la prefiero como "puramente académica".
phipsgabler
Honestamente, es una solución increíble y totalmente adecuada para el tipo de regresión / matemática de la pregunta original.
Sheraff
73

Debe agregar un argumento contrario a la definición de su función:

function singleDigit(num, counter = 0) {
    console.log(`called ${counter} times`)
    //...
    return singleDigit(number, counter+1)
}
singleDigit(39)
Sheraff
fuente
66
increíble. Parece que mi contador no funcionaba porque lo declaró en la función
chs242
77
Las reglas de alcance @ chs242 dictarían que declararlo en la función crearía una nueva cada invocación. stackoverflow.com/questions/500431/…
Taplar
10
@ chs242 no es que lo hayas declarado dentro de la función. Técnicamente, todos los parámetros predeterminados también lo están haciendo; en su caso, es simplemente que el valor nunca se transfirió a la próxima vez que se llamó recursivamente a la función. cada vez que la función se ejecute counterse eliminaría y establecería 0, a menos que la transfiera explícitamente en su llamada recursiva como lo hace Sheraff. AesingleDigit(number, ++counter)
zfrisch
2
correcto @zfrisch Ahora entiendo eso. Gracias por tomarse el tiempo para explicarlo
chs242
35
Por favor cambie ++countera counter+1. Son funcionalmente equivalentes, pero este último especifica la intención mejor, no muta (innecesariamente) ni parámetro, y no tiene la posibilidad de un incremento posterior accidental. O mejor aún, ya que es una llamada de cola, use un bucle en su lugar.
BlueRaja - Danny Pflughoeft
37

La solución tradicional es pasar el conteo como parámetro a la función como lo sugiere otra respuesta.

Sin embargo, hay otra solución en js. Algunas otras respuestas sugirieron simplemente declarar el recuento fuera de la función recursiva:

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

Esto por supuesto funciona. Sin embargo, esto hace que la función no sea reentrante (no se puede llamar dos veces correctamente). En algunos casos, puede ignorar este problema y simplemente asegurarse de no llamar singleDigitdos veces (javascript tiene un solo subproceso, por lo que no es demasiado difícil de hacer), pero este es un error que espera suceder si actualizasingleDigit más tarde para ser asincrónico y también se siente feo.

La solución es declarar la countervariable afuera pero no globalmente. Esto es posible porque JavaScript tiene cierres:

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
}

Esto es similar a la solución global, pero cada vez que llame singleDigit(que ahora no es una función recursiva) creará una nueva instancia de la countervariable.

slebetman
fuente
1
La variable de contador solo está disponible dentro de la singleDigitfunción y proporciona una forma limpia alternativa de hacerlo sin pasar un argumento imo. +1
AndrewL64
1
Como recursionahora está completamente aislado, debería ser totalmente seguro pasar el contador como último parámetro. No creo que sea necesario crear una función interna. Si no le gusta la idea de tener parámetros para el único beneficio de la recursión (sí, considero que el usuario podría meterse con ellos), luego enciérrelos Function#binden una función parcialmente aplicada.
Customcommander
@customcommander Sí, mencioné esto en resumen en la primera parte de mi respuesta - the traditional solution is to pass the count as a parameter. Esta es una solución alternativa en un lenguaje que tiene cierres. De alguna manera, es más simple de seguir porque es solo una variable en lugar de un número posiblemente infinito de instancias variables. En otras formas, conocer esta solución ayuda cuando lo que está rastreando es un objeto compartido (imagine construir un mapa único) o un objeto muy grande (como una cadena HTML)
slebetman
counter--sería la forma tradicional de resolver su reclamo de "no se puede llamar dos veces correctamente"
MonkeyZeus
1
@MonkeyZeus ¿Qué diferencia hace eso? Además, ¿cómo saber qué número inicializar el contador para ver que es el recuento que queremos encontrar?
slebetman
22

Otro enfoque, ya que produce todos los números, es usar un generador.

El último elemento es su número nreducido a un número de un solo dígito y para contar cuántas veces ha iterado, solo lea la longitud de la matriz.

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>


Pensamientos finales

Es posible que desee considerar tener una condición de retorno temprano en su función. Cualquier número con un cero en ella se devolverá cero.

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

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

Lo mismo puede decirse de cualquier número compuesto 1únicamente.

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

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

Finalmente, no aclaró si acepta solo enteros positivos. Si acepta enteros negativos, convertirlos en cadenas puede ser arriesgado:

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

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

Solución posible:

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

mult(39)
//=> 27

mult(-39)
//=> -27
comandante personalizado
fuente
estupendo. @customcommander gracias por explicar esto muy claramente
chs242
6

Ha habido muchas respuestas interesantes aquí. Creo que mi versión ofrece una alternativa interesante adicional.

Haces varias cosas con tu función requerida. Lo reduce recursivamente a un solo dígito. Registra los valores intermedios y desea contar las llamadas recursivas realizadas. Una forma de manejar todo esto es escribir una función pura que devolverá una estructura de datos que contenga el resultado final, los pasos tomados y el recuento de llamadas, todo en uno:

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

Luego puede registrar los pasos si lo desea, o almacenarlos para su posterior procesamiento.

Aquí hay una versión que hace eso:

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

Tenga en cuenta que rastreamos el stepspero derivamos el calls. Si bien podríamos rastrear el conteo de llamadas con un parámetro adicional, eso parece no ganar nada. También nos saltamos elmap(Number) paso: estos se convertirán en números en cualquier caso mediante la multiplicación.

Si le preocupa que ese stepsparámetro predeterminado se exponga como parte de su API, es bastante fácil ocultarlo utilizando una función interna como esta:

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

Y en cualquier caso, podría ser un poco más limpio extraer la multiplicación de dígitos en una función auxiliar:

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
fuente
2
Otra gran respuesta;) Tenga en cuenta que cuando n es negativo, digitProductdevolverá NaN( -39 ~> ('-' * '3') * '9'). Por lo tanto, es posible que desee utilizar un valor absoluto de ny usar -1o 1como el valor inicial de su reducción.
Customcommander
@customcommander: en realidad, volverá {"digit":-39,"steps":[-39],"calls":0}desde entonces -39 < 9. Si bien estoy de acuerdo en que esto podría funcionar con alguna comprobación de errores: ¿ es el parámetro un número? - ¿Es un número entero positivo? - etc. No creo que actualice para incluir eso. Esto captura el algoritmo, y el manejo de errores a menudo es específico de la base de código de uno.
Scott Sauyet
6

Si solo está tratando de contar cuántas veces se reduce y no le importa la recursión específicamente ... simplemente puede eliminarla. El siguiente código permanece fiel a la publicación original, ya que no cuenta num <= 9como una necesidad de reducción. Por lo tanto, singleDigit(8)tendrá count = 0y singleDigit(39)tendrá count = 3, al igual que el OP y la respuesta aceptada demuestran:

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

No es necesario procesar los números 9 o menos (es decir num <= 9). Lamentablemente, el código OP se procesará num <= 9aunque no lo cuente. El código anterior no procesará ni contaránum <= 9 en absoluto. Simplemente lo pasa a través.

Elijo no usarlo .reduceporque hacer las matemáticas reales fue mucho más rápido de ejecutar. Y, para mí, más fácil de entender.


Más pensamiento sobre la velocidad

Siento que un buen código también es rápido. Si está utilizando este tipo de reducción (que se usa mucho en numerología), es posible que necesite usarlo en una gran cantidad de datos. En este caso, la velocidad se convertirá en la más importante.

Usar ambos .map(Number)y console.log(en cada paso de reducción) son muy largos de ejecutar e innecesarios. Simplemente eliminar .map(Number)del OP lo aceleró en aproximadamente 4.38x. La eliminación lo console.logaceleró tanto que era casi imposible probarlo adecuadamente (no quería esperar).

Entonces, de manera similar a la respuesta de customcommander , no usar .map(Number)ni console.logempujar los resultados a una matriz y usar .lengthfor countes mucho más rápido. Desafortunadamente para la respuesta de customcommander , el uso de una función de generador es realmente muy lento (esa respuesta es aproximadamente 2.68 veces más lenta que el OP sin .map(Number)y console.log)

Además, en lugar de usar .reduce, solo utilicé las matemáticas reales. Este solo cambio aceleró mi versión de la función en un factor de 3.59x.

Finalmente, la recursividad es más lenta, ocupa espacio en la pila, usa más memoria y tiene un límite de cuántas veces puede "repetirse". O, en este caso, cuántos pasos de reducción puede usar para finalizar la reducción completa. El despliegue de su recursión a bucles iterativos lo mantiene todo en el mismo lugar en la pila y no tiene un límite teórico sobre cuántos pasos de reducción puede usar para terminar. Por lo tanto, estas funciones aquí pueden "reducir" casi cualquier número entero de cualquier tamaño, solo limitado por el tiempo de ejecución y el tiempo que puede durar una matriz.

Todo esto en 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 función anterior se ejecuta extremadamente rápido. Es aproximadamente 3.13 veces más rápido que el OP (sin .map(Number)y console.log) y aproximadamente 8.4 veces más rápido que la respuesta de customcommander . Tenga en cuenta que eliminar console.logdel OP evita que produzca un número en cada paso de reducción. Por lo tanto, la necesidad aquí de llevar estos resultados a una matriz.

PT

Pimp Trizkit
fuente
1
Hay mucho valor educativo en esta respuesta, así que gracias por eso. I feel good code is also fast.Diría que la calidad del código tiene que medirse con un conjunto predefinido de requisitos. Si el rendimiento no es uno de ellos, entonces no gana nada al reemplazar el código que cualquiera puede entender con código "rápido". No creería que la cantidad de código que he visto que se ha refactorizado es performante hasta el punto que ya nadie puede entenderlo (por alguna razón, el código óptimo también tiende a ser indocumentado). Finalmente, tenga en cuenta que las listas generadas de forma diferida le permiten a uno consumir artículos a pedido.
Customcommander
Gracias, creo. En mi humilde opinión, leer las matemáticas reales de cómo hacerlo fue más fácil de entender para mí ... que las [...num+''].map(Number).reduce((x,y)=> {return x*y})o incluso las [...String(num)].reduce((x,y)=>x*y)declaraciones que estoy viendo en la mayoría de las respuestas aquí. Entonces, para mí, esto tuvo el beneficio adicional de una mejor comprensión de lo que sucede en cada iteración y mucho más rápido. Sí, el código minificado (que tiene su lugar) es terriblemente difícil de leer. Pero en esos casos, en general, a uno no le importa conscientemente su legibilidad, sino simplemente el resultado final para cortar, pegar y seguir adelante.
Pimp Trizkit
¿JavaScript no tiene división entera para que pueda hacer el equivalente de C digit = num%10; num /= 10;? Tener que hacer num - xprimero para eliminar el dígito final antes de dividir es probable que obligue al compilador JIT a hacer una división separada de la que hizo para obtener el resto.
Peter Cordes
No lo creo. Son vars (JS no tiene ints). Por lo tanto, n /= 10;se convertirá nen un flotador si es necesario. num = num/10 - x/10podría convertirlo en un flotador, que es la forma larga de la ecuación. Por lo tanto, tengo que usar la versión refactorizada num = (num-x)/10;para mantenerlo como un número entero. No hay forma de que pueda encontrar en JavaScript que pueda brindarle el cociente y el resto de una operación de división única. Además, digit = num%10; num /= 10;hay dos declaraciones separadas y, por lo tanto, dos operaciones de división separadas. Ha pasado un tiempo desde que usé C, pero también pensé que era cierto allí.
Pimp Trizkit
6

¿Por qué no hacer una llamada console.counten su función?

Editar: Fragmento para probar en su navegador:

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 tengo trabajando en Chrome 79 y Firefox 72

Mistermatt
fuente
console.count no ayudaría ya que el contador se reinicia cada vez que se llama a la función (como se ha explicado en las respuestas anteriores)
chs242
2
No entiendo su problema, ya que lo tengo funcionando en Chrome y Firefox, agregué un fragmento en mi respuesta
Mistermatt
6

Puedes usar el cierre para esto.

Simplemente almacene counteren el cierre de la función.

Aquí hay un ejemplo:

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
fuente
1
Pero de esta manera el contador sigue contando en la próxima llamada, debe restablecerse en cada llamada inicial. Lleva a una pregunta difícil: cómo saber cuándo se llama a una función recursiva desde un contexto diferente, en este caso, función global vs función.
RobG
Este es solo un ejemplo para pensar. Puede modificarse preguntando al usuario por sus necesidades.
Kholiavko
@RobG No entiendo tu pregunta. La función recursiva no se puede llamar fuera del cierre porque es una función interna. Por lo tanto, no hay posibilidad o necesidad de diferenciar el contexto porque solo hay un contexto posible
slebetman el
@slebetman El contador nunca se reinicia. La función devuelta por singleDigitDecorator()seguirá incrementando el mismo contador cada vez que se llame.
Customcommander
1
@ slebetman: el problema es que la función devuelta por singleDigitDecorator no restablece su contador cuando se vuelve a llamar. Esa es la función que necesita saber cuándo restablecer el contador, de lo contrario, se requiere una nueva instancia de la función para cada uso. ¿Un posible caso de uso para Function.caller ? ;-)
RobG
1

Aquí hay una versión de Python que usa una función de contenedor para simplificar el contador, como ha sido sugerido por la respuesta de slebetman: escribo esto solo porque la idea central es muy clara en esta implementación:

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
fuente
1
En Python, preferiría algo como esto .
phipsgabler