Acompanhe quantas vezes uma função recursiva foi chamada

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)

O código acima pega um número inteiro e o reduz para um único dígito multiplicando-o por seus próprios dígitos.

O exemplo é 39.

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

O console registrará:

27 
14 
4

Como faço para acompanhar que a função recursiva foi chamada 3 vezes?

Eu tentei adicionar um contador, mas ele não atualiza. Gostaria de receber qualquer ajuda

chs242
fonte
4
.map(Number)é redundante, pois o *operador coage os valores para numerar de qualquer maneira. ;-)
RobG 2/01
4
Algumas perguntas: 1) Como você pretende lidar com números negativos? Por exemplo, o número -57é realmente um -50e um -7.. quando analisado dessa maneira, faria uma redução de -5x -7produzindo um número positivo 35. Ou você deseja que ele veja apenas o sinal negativo com 5e não o 7, mesmo que o também 7seja realmente negativo. 2) Como você pretende lidar com números que incluem zero? pois isso zerará automaticamente a redução. Portanto, quanto maior o número que você passar, maior será a probabilidade de zerar. A outra opção seria pular os zeros
Pimp Trizkit
3
Percebo que minhas perguntas acima não são sobre contar a recursão, mas apenas o aspecto de solução de quebra-cabeças do conteúdo usado nesta pergunta. Por favor me perdoe.
Pimp Trizkit
3
Fico lisonjeado que você goste da minha resposta, mas, para fins práticos, acho que stackoverflow.com/a/59570894/1346276 é a variante geral mais limpa.
phipsgabler 6/01
2
@phipsgabler Quem dedica algum tempo a escrever uma resposta inteligente e coerente merece algo parecido. Obrigado
chs242 6/01

Respostas:

25

Essa é uma variante quase puramente acadêmica, mas você pode usar um combinador de ponto fixo modificado para essa finalidade.

Permite encurtar e melhorar um pouco sua função original:

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

// singleDigit(123234234) == 0

A partir dessa variante, podemos fatorar e curry a chamada recursiva:

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

Esta função agora pode ser usada com um combinador de ponto fixo; especificamente, implementei um combinador Y adaptado para JavaScript (estrito) da seguinte maneira:

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

onde nós temos Ynormal(singleDigitF, 123234234) == 0.

Agora vem o truque. Como fatoramos a recursão no combinador Y, podemos contar o número de recursões dentro dele:

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

Uma verificação rápida no nó REPL fornece:

> 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 agora funcionará para contar o número de chamadas em qualquer função recursiva escrita no estilo de singleDigitF.

(Observe que há duas fontes de obtenção de zero como resposta muito frequente: excesso numérico ( 123345456999999999tornar-se 123345457000000000etc.) e o fato de você quase certamente obter zero como um valor intermediário em algum lugar, quando o tamanho da entrada estiver aumentando.)

phipsgabler
fonte
6
Para os que rejeitam: eu realmente concordo com você que essa não é a melhor solução prática - por isso prefixei-a como "puramente acadêmica".
phipsgabler 8/01
Honestamente, é uma solução incrível e totalmente adequada para o tipo de regressão / matemática da pergunta original.
Sheraff
73

Você deve adicionar um contra-argumento à sua definição de função:

function singleDigit(num, counter = 0) {
    console.log(`called ${counter} times`)
    //...
    return singleDigit(number, counter+1)
}
singleDigit(39)
Sheraff
fonte
6
impressionante. Parece que meu contador não estava funcionando porque eu o declarei na função
chs242 02/01
7
As regras de escopo do @ chs242 determinariam que a declaração na função criaria um novo a cada invocação. stackoverflow.com/questions/500431/…
Taplar
10
@ chs242 não é que você o tenha declarado dentro da função. Tecnicamente, todos os parâmetros padrão também estão funcionando - no seu caso, é simplesmente que o valor nunca foi transferido para a próxima vez que a função foi chamada recursivamente. ae toda vez que a função é executada counteré descartada e definida como 0, a menos que você a carregue explicitamente em sua chamada recursiva, como Sheraff. AesingleDigit(number, ++counter)
zfrisch
2
direito @ zfrisch eu entendo isso agora. Obrigado por tomar o tempo para explicá-lo
chs242 02/01
35
Por favor mude ++counterpara counter+1. Eles são funcionalmente equivalentes, mas o último especifica melhor a intenção, não muda (desnecessariamente) e parâmetro e não tem a possibilidade de pós-incremento acidental. Ou melhor ainda, como é uma chamada final, use um loop.
BlueRaja - Danny Pflughoeft
37

A solução tradicional é passar a contagem como parâmetro para a função, conforme sugerido por outra resposta.

No entanto, há outra solução em js. Algumas outras respostas sugeridas simplesmente declarando contagem fora da função recursiva:

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

Claro que isso funciona. No entanto, isso torna a função não reentrante (não pode ser chamada duas vezes corretamente). Em alguns casos, você pode ignorar esse problema e simplesmente não ligar singleDigitduas vezes (o javascript é de thread único, por isso não é muito difícil de fazer), mas esse é um bug que está esperando para acontecer se você atualizar singleDigitmais tarde para ser assíncrono e também parecer feio.

A solução é declarar a countervariável fora, mas não globalmente. Isso é possível porque o javascript possui fechamentos:

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
}

Isso é semelhante à solução global, mas cada vez que você chama singleDigit(que agora não é uma função recursiva), ela cria uma nova instância da countervariável.

slebetman
fonte
11
A variável counter está disponível apenas na singleDigitfunção e fornece uma maneira alternativa limpa de fazer isso sem passar um argumento imo. +1
AndrewL64
11
Como recursionagora está completamente isolado, deve ser totalmente seguro passar no contador como o último parâmetro. Não acho que seja necessário criar uma função interna. Se você não gosta da ideia de ter parâmetros para o benefício exclusivo da recursão (eu entendo que o usuário possa mexer com eles), prenda-os Function#bindem uma função parcialmente aplicada.
customcommander
@ customcommander Sim, eu mencionei isso em resumo na primeira parte da minha resposta - the traditional solution is to pass the count as a parameter. Esta é uma solução alternativa em um idioma que possui encerramentos. De certa forma, é mais simples de seguir, porque é apenas uma variável em vez de um número possivelmente infinito de instâncias de variáveis. De outras maneiras, conhecer esta solução ajuda quando o que você está rastreando é um objeto compartilhado (imagine construir um mapa exclusivo) ou um objeto muito grande (como uma string HTML)
slebetman
counter--seria a maneira tradicional de resolver sua reivindicação de "não pode ser chamado duas vezes corretamente"
MonkeyZeus 03/01
11
@MonkeyZeus Que diferença isso faz? Além disso, como você saberia qual número inicializar o contador para ver que é a contagem que queremos encontrar?
slebetman
22

Outra abordagem, já que você produz todos os números, é usar um gerador.

O último elemento é o seu número nreduzido a um número de dígito e, para contar quantas vezes você iterou, basta ler o comprimento da 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>


Pensamentos finais

Você pode considerar ter uma condição de retorno antecipado em sua função. Quaisquer números com um zero no que irá retornar zero.

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

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

O mesmo pode ser dito para quaisquer números feitos 1apenas.

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

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

Por fim, você não esclareceu se aceita apenas números inteiros positivos. Se você aceitar números inteiros negativos, convertê-los em seqüências de caracteres pode ser arriscado:

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

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

Solução possível:

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

mult(39)
//=> 27

mult(-39)
//=> -27
comando personalizado
fonte
ótimo. @customcommander obrigado por explicar isso com muita clareza
chs242 03/01
6

Houve muitas respostas interessantes aqui. Eu acho que minha versão oferece uma alternativa adicional interessante.

Você faz várias coisas com a função necessária. Você reduz recursivamente para um único dígito. Você registra os valores intermediários e deseja contar as chamadas recursivas feitas. Uma maneira de lidar com tudo isso é escrever uma função pura que retornará uma estrutura de dados que contenha o resultado final, as etapas executadas e a chamada contem tudo em uma:

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

Você pode registrar as etapas, se desejar, ou armazená-las para processamento adicional.

Aqui está uma versão que faz isso:

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

Observe que rastreamos o, stepsmas derivamos o calls. Embora possamos rastrear a contagem de chamadas com um parâmetro adicional, isso parece não ganhar nada. Também pulamos o map(Number)passo - estes serão coagidos a números em qualquer caso pela multiplicação.

Se você tiver preocupações sobre a stepsexposição desse parâmetro padrão como parte da sua API, é fácil ocultá-lo usando uma função 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, [])
}

E, em ambos os casos, pode ser um pouco mais limpo extrair a multiplicação de dígitos para uma função 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
fonte
2
Outra ótima resposta;) Observe que, quando n for negativo, digitProductretornará NaN( -39 ~> ('-' * '3') * '9'). Portanto, você pode usar um valor absoluto de n e usar -1ou 1como o valor inicial de sua redução.
customcommander
@ customcommander: na verdade, ele retornará {"digit":-39,"steps":[-39],"calls":0}, desde -39 < 9. Embora eu concorde que isso possa estar relacionado à verificação de erros: o parâmetro é um número? - é um número inteiro positivo? - etc. Acho que não vou atualizar para incluir isso. Isso captura o algoritmo, e o tratamento de erros geralmente é específico da base de código.
Scott Sauyet
6

Se você está apenas tentando contar quantas vezes ele é reduzido e não se importa especificamente com a recursão ... basta remover a recursão. O código abaixo permanece fiel à postagem original, pois não conta num <= 9como necessitando de redução. Portanto, singleDigit(8)terá count = 0e singleDigit(39)terá count = 3, exatamente como o OP e a resposta aceita estão demonstrando:

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

Não é necessário processar números 9 ou menos (ie. num <= 9). Infelizmente, o código OP será processado num <= 9mesmo que não conte. O código acima não será processado nem contado num <= 9. Apenas passa através.

Eu escolhi não usar .reduceporque fazer as contas reais era muito mais rápido de executar. E, para mim, mais fácil de entender.


Pensando mais sobre velocidade

Eu sinto que um bom código também é rápido. Se você estiver usando esse tipo de redução (que é muito usado em numerologia), pode estar precisando usá-lo em uma grande quantidade de dados. Nesse caso, a velocidade se tornará da maior importância.

O uso de ambos .map(Number)e console.log(em cada etapa de redução) é muito longo para executar e desnecessário. A simples exclusão .map(Number)do OP acelerou em cerca de 4,38x. A exclusão console.logacelerava tanto que era quase impossível testar adequadamente (eu não queria esperar).

Portanto, semelhante à resposta do comando personalizado , não usar .map(Number)nem console.logpressionar os resultados em uma matriz e usar .lengthpara counté muito muito mais rápido. Infelizmente para a resposta do comando personalizado , o uso de uma função de gerador é realmente muito lento (essa resposta é cerca de 2,68x mais lenta que o OP sem .map(Number)e console.log)

Além disso, em vez de usar .reduce, apenas usei a matemática real. Somente essa única mudança acelerou minha versão da função por um fator de 3,59x.

Finalmente, a recursão é mais lenta, ocupa espaço na pilha, usa mais memória e tem um limite de quantas vezes pode "se repetir". Ou, nesse caso, quantas etapas de redução ele pode usar para concluir a redução completa. A distribuição de sua recursão para loops iterativos mantém tudo no mesmo lugar na pilha e não tem limite teórico sobre quantas etapas de redução ele pode usar para concluir. Portanto, essas funções aqui podem "reduzir" praticamente qualquer número inteiro, limitado apenas pelo tempo de execução e por quanto tempo uma matriz pode ser.

Tudo isso em 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

A função acima é extremamente rápida. É cerca de 3,13x mais rápido que o OP (sem .map(Number)e console.log) e 8,4x mais rápido que a resposta do comando personalizado . Lembre-se de que a exclusão console.logdo OP impede a produção de um número a cada etapa da redução. Portanto, a necessidade aqui de empurrar esses resultados em uma matriz.

PT

Pimp Trizkit
fonte
11
Há muito valor educacional nesta resposta, então obrigado por isso. I feel good code is also fast.Eu diria que a qualidade do código deve ser medida em relação a um conjunto predefinido de requisitos. Se o desempenho não for um deles, você não ganha nada substituindo o código que qualquer um pode entender pelo código "rápido". Você não acreditaria que a quantidade de código que eu vi que foi refatorada tenha desempenho a ponto de ninguém mais entender (por algum motivo, o código ideal também tende a não ser documentado;). Por fim, esteja ciente de que listas geradas com preguiça permitem consumir itens sob demanda.
customcommander
Obrigado Eu acho. IMHO, lendo a matemática real de como fazer isso foi mais fácil de entender para mim .. do que as [...num+''].map(Number).reduce((x,y)=> {return x*y})ou mesmo [...String(num)].reduce((x,y)=>x*y)declarações que estou vendo na maioria das respostas aqui. Então, para mim, isso teve o benefício adicional de entender melhor o que está acontecendo a cada iteração e muito mais rápido. Sim, o código compactado (que tem seu lugar) é terrivelmente difícil de ler. Mas, nesses casos, geralmente não se preocupa conscientemente com sua legibilidade, mas apenas o resultado final para cortar e colar e seguir em frente.
Pimp Trizkit
O JavaScript não possui divisão inteira para que você possa fazer o equivalente a C digit = num%10; num /= 10;? Ter que fazer num - xprimeiro para remover o dígito à direita antes da divisão provavelmente forçará o compilador JIT a fazer uma divisão separada daquela que ele fez para obter o restante.
Peter Cordes
Acho que não. Estes são vars (JS não tem ints). Portanto, n /= 10;será convertido nem um flutuador, se necessário. num = num/10 - x/10pode convertê-lo em um flutuador, que é a forma longa da equação. Portanto, eu tenho que usar a versão refatorada num = (num-x)/10;para mantê-la em número inteiro. Não há como encontrar no JavaScript que possa fornecer o quociente e o restante de uma operação de divisão única. Além disso, digit = num%10; num /= 10;há duas instruções separadas e, portanto, duas operações de divisão separadas. Faz um tempo desde que eu usei C, mas achei que era verdade lá também.
Pimp Trizkit
6

Por que não ligar console.countna sua função?

Editar: snippet para experimentar no seu 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)

Estou trabalhando no Chrome 79 e Firefox 72

Mistermatt
fonte
console.count não ajudaria, pois o contador é redefinido toda vez que a função é chamada (como foi explicado nas respostas acima)
chs242
2
Não entendo o seu problema, pois ele está funcionando no Chrome e Firefox, adicionei um trecho na minha resposta
Mistermatt
6

Você pode usar o fechamento para isso.

Simplesmente armazene counterno fechamento da função.

Aqui está um exemplo:

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
fonte
11
Mas, dessa forma, o contador continua contando com a próxima ligação, sendo necessário redefinir a cada ligação inicial. Isso leva a uma pergunta difícil: como saber quando uma função recursiva é chamada de um contexto diferente, neste caso, função global vs.
RobG
Este é apenas um exemplo para ter uma ideia. Pode ser modificado solicitando ao usuário suas necessidades.
Kholiavko 02/01
@ RobG Não entendi sua pergunta. A função recursiva não pode ser chamada fora do fechamento porque é uma função interna. Portanto, não há possibilidade ou necessidade de diferenciar o contexto porque existe apenas um contexto possível
slebetman
@slebetman O contador nunca é reiniciado. A função retornada por singleDigitDecorator()continuará incrementando o mesmo contador toda vez que for chamada.
customcommander
11
@ slebetman - o problema é que a função retornada por singleDigitDecorator não redefine seu contador quando é chamada novamente. Essa é a função que precisa saber quando redefinir o contador, caso contrário, uma nova instância da função é necessária para cada uso. Um possível caso de uso para Function.caller ? ;-)
RobG 2/01
1

Aqui está uma versão do Python que usa uma função wrapper para simplificar o contador, conforme sugerido pela resposta de slebetman - eu escrevo isso apenas porque a ideia principal é muito clara nesta implementação:

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
fonte
11
Em Python, eu prefiro algo como este .
phipsgabler 6/01