简体   繁体   English

跟踪递归函数被调用的次数

[英]Keep track of how many times a recursive function was called

 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)

The code above takes an integer and reduces it to a single digit by multiplying it by its own digits.上面的代码采用一个整数,并通过将它乘以它自己的数字将它减少到一个数字。

Example is 39.示例为 39。

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

The console will log:控制台将记录:

27 
14 
4

How do I keep track that the recursive function was called 3 times?如何跟踪递归函数被调用了 3 次?

I have tried adding a counter but it fails to update.我曾尝试添加计数器,但无法更新。 Would appreciate any help将不胜感激任何帮助

You should add a counter argument to your function definition:您应该在函数定义中添加一个反参数:

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

The traditional solution is to pass the count as a parameter to the function as suggested by another answer.传统的解决方案是将计数作为参数传递给另一个答案所建议的函数。

However, there is another solution in js.但是,js 中还有另一种解决方案。 A few other answers suggested simply declaring count outside the recursive function:其他一些答案建议简单地在递归函数之外声明计数:

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

This of course works.这当然有效。 However this makes the function non-reentrant (cannot be called twice correctly).然而,这使得函数不可重入(不能正确调用两次)。 In some cases you can ignore this problem and simply make sure you don't call singleDigit twice (javascript is single threaded so it's not too hard to do) but this is a bug waiting to happen if you update singleDigit later to be asynchronous and it also feels ugly.在某些情况下,你可以忽略这个问题,只需确保你没有调用singleDigit两次(javascript 是单线程的,所以它不是太难做)但是如果你稍后将singleDigit更新为异步,这是一个等待发生的错误并且它也觉得丑。

The solution is to declare the counter variable outside but not globally.解决方案是在外部而不是全局声明counter变量。 This is possible because javascript has closures:这是可能的,因为 javascript 有闭包:

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
}

This is similar to the global solution but each time you call singleDigit (which is now not a recursive function) it will create a new instance of the counter variable.这类似于全局解决方案,但每次调用singleDigit (现在不是递归函数)时,它都会创建counter变量的新实例。

This is an almost purely academic variant, but you can use a modified fixed point combinator for this purpose.这几乎是纯粹的学术变体,但您可以为此目的使用修改后的定点组合器。

Lets shorten and improve your original function a bit:让我们稍微缩短和改进您的原始函数:

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

// singleDigit(123234234) == 0

From this variant, we can factor out and curry the recursive call:从这个变体中,我们可以将递归调用分解出来并进行柯里化:

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

This function can now be used with a fixed point combinator;此函数现在可以与定点组合器一起使用; specifically I implemented a Y combinator adapted for (strict) JavaScript as follows:具体来说,我实现了一个适用于(严格)JavaScript 的 Y 组合器,如下所示:

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

where we have Ynormal(singleDigitF, 123234234) == 0 .我们有Ynormal(singleDigitF, 123234234) == 0

Now comes the trick.现在诀窍来了。 Since we have factored out the recursion to the Y combinator, we can count the number of recursions within it:由于我们已经分解了对 Y 组合子的递归,我们可以计算其中的递归次数:

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

A quick check in the Node REPL gives:快速检查节点 REPL 给出:

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

This combinator will now work for counting the number of calls in any recursive function written in the style of singleDigitF .此组合器现在可用于计算以singleDigitF样式编写的任何递归函数中的调用singleDigitF

(Note that there's two sources of getting zero as a very frequent answer: numeric overflow ( 123345456999999999 becoming 123345457000000000 etc.), and the fact that you will almost surely get zero as an intermediate value somewhere, when the size of the input is growing.) (请注意,有两个来源将零作为非常常见的答案:数字溢出( 123345456999999999变成123345457000000000等),以及当输入的大小增加时,您几乎肯定会在某处获得零作为中间值。 )

Another approach, since you produce all the numbers, is to use a generator.另一种方法,因为你产生所有的数字,是使用生成器。

The last element is your number n reduced to a single digit number and to count how many times you have iterated, just read the length of the array.最后一个元素是你的数字n减少到一个数字,并计算你迭代了多少次,只需读取数组的长度。

 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>


Final thoughts最后的想法

You may want to consider having a return-early condition in your function.您可能需要考虑在您的函数中设置早返回条件。 Any numbers with a zero in it will return zero.任何带有零的数字都将返回零。

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

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

The same can be said for any numbers made of 1 only.对于任何仅由1数字,情况也是如此。

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

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

Finally, you didn't clarify whether you accept only positive integers.最后,您没有说明是否只接受正整数。 If you accept negative integers then casting them to strings can be risky:如果您接受负整数,那么将它们转换为字符串可能会有风险:

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

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

Possible solution:可能的解决方案:

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

mult(39)
//=> 27

mult(-39)
//=> -27

You can use closure for this.您可以为此使用闭包。

Just simply store counter into the closure of function.只需简单地将counter存储到函数的闭包中。

Here is example:这是示例:

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

If you are just trying to count how many times it gets reduced and are not caring about the recursion specifically ... you can just remove the recursion.如果你只是想指望它得到了多少次下降,并且不关心递归...你可以只取出递归。 The below code remains faithful to the Original Post as it does not count num <= 9 as needing reduction.下面的代码仍然忠实于原始帖子,因为它不将num <= 9视为需要减少。 Therefore, singleDigit(8) will have count = 0 , and singleDigit(39) will have count = 3 , just like the OP and accepted answer are demonstrating:因此, singleDigit(8)将有count = 0 ,而singleDigit(39)将有count = 3 ,就像 OP 和接受的答案正在证明:

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

It is unnecessary to process numbers 9 or less (ie. num <= 9 ).没有必要处理 9 或更少的数字(即num <= 9 )。 Unfortunately the OP code will process num <= 9 even tho it does not count it.不幸的是,OP 代码将处理num <= 9即使它不计算它。 The code above will not process nor count num <= 9 , at all.上面的代码根本不会处理或计数num <= 9 It just passes it thru.它只是通过它。

I choose not to use .reduce because doing the actual math was much faster to execute.我选择不使用.reduce因为进行实际的数学运算执行起来要快得多。 And, for me, easier to understand.而且,对我来说,更容易理解。


Further thinking on speed对速度的进一步思考

I feel good code is also fast.我感觉好的代码也很快。 If you are using this type of reduction (which is used in numerology a lot) you might be needing to use it on a massive amount of data.如果您正在使用这种类型的归约(在命理学中经常使用),您可能需要在大量数据上使用它。 In this case, speed will become the upmost of importance.在这种情况下,速度将成为最重要的。

Using both .map(Number) and console.log (at each reduction step) are both very very long to execute and unnecessary.同时使用.map(Number)console.log (在每个减少步骤中)都需要非常长的执行时间并且没有必要。 Simply deleting .map(Number) from the OP sped it up by about 4.38x.简单地从 OP 中删除.map(Number)将其速度提高了大约 4.38 倍。 Deleting console.log sped it up so much it was almost impossible to properly test (I didn't want to wait for it).删除console.log加速了它几乎不可能正确测试(我不想等待它)。

So, similar to customcommander 's answer, not using .map(Number) nor console.log and pushing the results into an array and using .length for count is much much faster.因此,类似于customcommander的答案,不使用.map(Number)也不使用console.log并将结果推送到数组中并使用.length进行count要快得多。 Unfortunately for customcommander 's answer, using a generator function is really really slow (that answer is about 2.68x slower than the OP without .map(Number) and console.log )不幸的是,对于customcommander的答案,使用生成器函数真的很慢(该答案比没有.map(Number)console.log的 OP 慢约 2.68 倍)

Also, instead of using .reduce I just used the actual math.另外,我没有使用.reduce ,而是使用了实际的数学运算。 This single change alone sped up my version of the function by a factor of 3.59x.仅此一项更改就将我的函数版本加快了 3.59 倍。

Finally, recursion is slower, it takes up stack space, uses more memory, and has a limit to how many times it can "recur".最后,递归更慢,它占用堆栈空间,使用更多内存,并且对它可以“递归”的次数有限制。 Or, in this case, how many steps of reduction it can use to finish the full reduction.或者,在这种情况下,它可以使用多少步减少来完成完全减少。 Rolling out your recursion to iterative loops keeps it all on the same place on the stack and has no theoretical limit on how many reduction steps it can use to finish.将您的递归推广到迭代循环可将其全部放在堆栈上的同一个位置,并且对于它可以使用多少减少步骤来完成没有理论上的限制。 Thus, these functions here can "reduce" almost any sized integer, only limited by execution time and how long an array can be.因此,这里的这些函数可以“减少”几乎任何大小的整数,仅受执行时间和数组长度的限制。

All this in mind...想到这一切...

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

The above function runs extremely fast.上述功能运行速度极快。 It is about 3.13x faster than the OP (without .map(Number) and console.log ) and about 8.4x faster than customcommander 's answer.这是约3.13x比OP(不含快.map(Number)console.log )和大约8.4倍的速度比customcommander的答案。 Keep in mind that deleting console.log from the OP prevents it from producing a number at each step of reduction.请记住,从 OP 中删除console.log阻止它在每一步减少时产生一个数字。 Hence, the need here to push these results into an array.因此,这里需要将这些结果推送到数组中。

PT PT

There have been many interesting answers here.这里有很多有趣的答案。 I think my version offers an additional interesting alternative.我认为我的版本提供了一个额外的有趣选择。

You do several things with your required function.您可以使用所需的功能做几件事。 You recursively reduce it to a single digit.您递归地将其减少到一位数。 You log the intermediate values, and you would like a count of the recursive calls made.您记录中间值,并且您想要进行递归调用的计数。 One way to handle all this is to write a pure function which will return a data structure that contains the final result, the steps taken and the call count all in one:处理所有这些的一种方法是编写一个纯函数,该函数将返回一个包含最终结果、所采取的步骤和调用计数的数据结构:

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

You can then log the steps if you desire, or store them for further processing.然后,您可以根据需要记录这些步骤,或存储它们以供进一步处理。

Here is a version which does that:这是一个这样做的版本:

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

Note that we track the steps but derive the calls .请注意,我们跟踪steps但导出calls While we could track the call count with an additional parameter, that seems to gain nothing.虽然我们可以使用附加参数跟踪调用计数,但这似乎没有任何好处。 We also skip the map(Number) step -- these will be coerced to numbers in any case by the multiplication.我们还跳过了map(Number)步骤——无论如何,这些都将被乘法强制转换为数字。

If you have concerns about that defaulted steps parameter being exposed as part of your API, it's easy enough to hide it by using an internal function like this:如果您担心默认的steps参数作为 API 的一部分公开,可以使用如下内部函数轻松隐藏它:

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

And in either case, it might be a bit cleaner to extract the digit multiplication into a helper function:在任何一种情况下,将数字乘法提取到辅助函数中可能会更简洁一些:

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

Why not make a call to console.count in your function ?为什么不在你的函数中调用console.count呢?

Edit: Snippet to try in your 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)

I have it working in Chrome 79 and Firefox 72我让它在 Chrome 79 和 Firefox 72 中工作

Here's a Python version that uses a wrapper function to simplify the counter, as has been suggested by slebetman's answer — I write this only because the core idea is very clear in this implementation:这是一个使用包装函数来简化计数器的 Python 版本,正如 slebetman 的回答所建议的那样——我写这个只是因为在这个实现中核心思想非常清晰:

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)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM