【问题标题】:Keep track of how many times a recursive function was called跟踪调用递归函数的次数
【发布时间】:2020-04-21 13:28:17
【问题描述】:

 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)

上面的代码接受一个整数,并通过将它乘以它自己的数字将它减少到一个数字。

示例是 39。

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

控制台将记录:

27 
14 
4

如何跟踪递归函数被调用了 3 次?

我已尝试添加计数器,但无法更新。 希望有任何帮助

【问题讨论】:

  • .map(Number) 是多余的,因为 * 运算符无论如何都会强制将值编号。 ;-)
  • 几个问题:1)你打算如何处理负数?例如,数字-57 实际上是一个-50 和一个-7 .. 当这样看时,它会减少-5 x -7 产生一个正数35。或者您是否希望它只看到带有5 而不是7 的负号,即使7 实际上也是负号。 2)您打算如何处理包含零的数字?因为这将自动归零减少。因此,您传入的数字越大,它就越有可能归零。另一种选择是跳过零
  • 我意识到我上面的问题不是关于计算递归,而只是这个问题中使用的内容的解谜方面。请原谅我。
  • 很高兴你喜欢我的回答,但出于实际目的,我认为stackoverflow.com/a/59570894/1346276 是最简洁的通用变体。
  • @phipsgabler 任何花时间写出聪明而连贯的答案的人都值得点赞。谢谢

标签: javascript recursion


【解决方案1】:

您可以为此使用闭包。

只需将counter 存储到函数的闭包中即可。

示例如下:

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

【讨论】:

  • 但是这样计数器会一直计数下一次调用,每次初始调用都需要重置。这就引出了一个难题:如何判断何时从不同的上下文(在本例中为全局 vs 函数)调用递归函数。
  • 这只是为了想出一个想法的例子。可以通过询问用户的需求来修改它。
  • @RobG 我不明白你的问题。递归函数不能在闭包之外调用,因为它是一个内部函数。所以没有可能或不需要区分上下文,因为只有一个可能的上下文
  • @slebetman 计数器永远不会重置。 singleDigitDecorator() 返回的函数将在每次调用时保持递增相同的计数器。
  • @slebetman — 问题是 singleDigitDecorator 返回的函数在再次调用时不会重置其计数器。那就是需要知道何时重置计数器的函数,否则每次使用都需要一个新的函数实例。 Function.caller 的可能用例? ;-)
【解决方案2】:

为什么不在你的函数中调用console.count

编辑:在您的浏览器中尝试的片段:

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)

我可以在 Chrome 79 和 Firefox 72 中使用它

【讨论】:

  • console.count 无济于事,因为每次调用函数时计数器都会重置(如上面的答案中所解释的那样)
  • 我不明白你的问题,因为我在 Chrome 和 Firefox 中工作,我在答案中添加了一个 sn-p
【解决方案3】:

这是一个使用包装函数来简化计数器的 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)

【讨论】:

  • 在 Python 中,我更喜欢 this 之类的东西。
【解决方案4】:

这几乎是一个纯粹的学术变体,但您可以为此目的使用 modified fixed point combinator

让我们稍微缩短和改进你原来的功能:

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

// singleDigit(123234234) == 0

从这个变体中,我们可以分解并curry递归调用:

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

此函数现在可以与定点组合器一起使用;具体来说,我实现了一个适用于(严格)JavaScript 的 Y 组合器,如下所示:

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

我们有Ynormal(singleDigitF, 123234234) == 0

现在诀窍来了。由于我们已经分解出对 Y 组合器的递归,我们可以计算其中的递归数:

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

快速检查 Node 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 ]

这个组合器现在将用于计算 anysingleDigitF 样式编写的递归函数中的调用次数。

(请注意,作为一个非常常见的答案,有两个来源:数字溢出(123345456999999999 变为 123345457000000000 等),以及您几乎肯定会在某处得到零作为中间值的事实,当输入的大小正在增长。)

【讨论】:

  • 致反对者:我真的同意你的观点,这不是最好的实用解决方案——这就是为什么我将其前缀为“纯粹的学术”。
  • 老实说,这是一个很棒的解决方案,完全适合原始问题的回归/数学类型。
【解决方案5】:

如果您只是想计算它减少了多少次并且不关心递归特别是...您可以删除递归。以下代码仍然忠实于原始帖子,因为它不将 num &lt;= 9 视为需要减少。因此,singleDigit(8) 将具有 count = 0singleDigit(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;
}

没有必要处理 9 或更少的数字(即num &lt;= 9)。不幸的是,OP 代码将处理num &lt;= 9,即使它不计算在内。上面的代码根本不会处理也不会计算num &lt;= 9。它只是通过它。

我选择不使用.reduce,因为执行实际数学运算要快得多。而且,对我来说,更容易理解。


进一步思考速度

我觉得好代码也快。如果您正在使用这种类型的缩减(在命理中经常使用),您可能需要在大量数据上使用它。在这种情况下,速度将成为重中之重。

同时使用.map(Number)console.log(在每个缩减步骤中)都非常耗时且不必要。只需从 OP 中删除 .map(Number) 即可将其加速约 4.38 倍。删除console.log 速度非常快,几乎不可能正确测试(我不想等待它)。

因此,类似于customcommander 的答案,不使用.map(Number)console.log 并将结果推送到数组中并为count 使用.length 要快得多。不幸的是,对于customcommander 的答案,使用generator function 真的很慢(该答案比没有.map(Number)console.log 的OP 慢约2.68 倍)

另外,我没有使用.reduce,而是使用了实际的数学。仅此一项更改就将我的函数版本加快了 3.59 倍。

最后,递归更慢,它占用堆栈空间,使用更多内存,并且它可以“递归”多少次。或者,在这种情况下,它可以使用多少减少步骤来完成完全减少。将您的递归推广到迭代循环使其全部位于堆栈上的同一位置,并且对于可以使用多少减少步骤来完成没有理论上的限制。因此,这里的这些函数可以“减少”几乎任何大小的整数,仅受执行时间和数组长度的限制。

记住这一切...

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

上述函数运行速度极快。它比 OP(没有 .map(Number)console.log)快约 3.13 倍,比 customcommander 的答案快约 8.4 倍。请记住,从 OP 中删除 console.log 会阻止它在每个减少步骤中产生一个数字。因此,这里需要将这些结果推送到数组中。

PT

【讨论】:

  • 这个答案有很多教育价值,所以谢谢你。 I feel good code is also fast. 我想说代码质量必须根据一组预定义的要求来衡量。如果性能不是其中之一,那么通过用“快速”代码替换任何人都可以理解的代码,您将一无所获。你不会相信我所看到的代码量已经被重构到没有人可以理解的地步(由于某种原因,最佳代码也往往没有记录;)。最后请注意,惰性生成的列表允许您按需使用项目。
  • 谢谢,我想。恕我直言,阅读如何做的实际数学对我来说更容易理解......比我在大多数答案中看到的[...num+''].map(Number).reduce((x,y)=&gt; {return x*y}) 甚至[...String(num)].reduce((x,y)=&gt;x*y) 语句。所以,对我来说,这有一个额外的好处,那就是更好地理解每次迭代中发生的事情并且更快。是的,缩小的代码(它有它的位置)非常难以阅读。但在这些情况下,人们通常有意识地不关心它的可读性,而只关心剪切和粘贴并继续前进的最终结果。
  • JavaScript 没有整数除法,所以你可以做相当于 C digit = num%10; num /= 10; 的操作吗?必须先执行num - x 以在除法之前删除尾随数字,这可能会迫使 JIT 编译器执行一个单独的除法,而不是它为得到余数所做的除法。
  • 我不这么认为。这些是vars(JS 没有ints)。因此,n /= 10; 将在需要时将n 转换为浮点数。 num = num/10 - x/10 可能会将其转换为浮点数,这是等式的长形式。因此,我必须使用重构后的版本num = (num-x)/10; 将其保持为整数。我无法在 JavaScript 中找到可以同时为您提供单个除法运算的商和余数的方法。此外,digit = num%10; num /= 10; 是两个单独的语句,因此是两个单独的除法运算。我使用 C 已经有一段时间了,但我认为那里也是如此。
【解决方案6】:

这里有很多有趣的答案。我认为我的版本提供了另一个有趣的选择。

您使用所需的功能做几件事。您递归地将其减少到一位数。您记录中间值,并且您想要进行递归调用的计数。处理这一切的一种方法是编写一个纯函数,该函数将返回一个数据结构,其中包含最终结果、所采取的步骤和调用次数:

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

然后,您可以根据需要记录这些步骤,或存储它们以供进一步处理。

这是一个可以做到这一点的版本:

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

请注意,我们跟踪steps,但派生出calls。虽然我们可以使用附加参数来跟踪调用计数,但这似乎没有任何收获。我们也跳过了map(Number) 步骤——在任何情况下,这些都会被乘法强制转换为数字。

如果您担心默认的 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, [])
}

无论哪种情况,将数字乘法提取到辅助函数中可能会更简洁:

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

【讨论】:

  • 另一个很好的答案 ;) 请注意,当 n 为负数时,digitProduct 将返回 NaN (-39 ~&gt; ('-' * '3') * '9')。因此,您可能希望使用 n 的绝对值并使用 -11 作为 reduce 的初始值。
  • @customcommander:实际上,它会返回{"digit":-39,"steps":[-39],"calls":0},因为-39 &lt; 9。虽然我同意这可能与一些错误检查有关:参数是一个数字吗? - 它是一个正整数吗? - 等等。我想我不是将更新以包括该内容。这捕获了算法,并且错误处理通常特定于一个人的代码库。
【解决方案7】:

你应该在你的函数定义中添加一个反参数:

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

【讨论】:

  • 太棒了。看起来我的计数器不起作用,因为我在函数中声明了它
  • @chs242 范围规则将规定在函数中声明它会在每次调用时创建一个新的。 stackoverflow.com/questions/500431/…
  • @chs242 并不是您在函数中声明了它。从技术上讲,这也是所有默认参数的作用 - 在您的情况下,只是该值永远不会延续到下一次递归调用函数时。 a.e.每次函数运行时,counter 都会被废弃并设置为 0除非您像 Sheraff 那样在递归调用中明确地将其保留。 A.e. singleDigit(number, ++counter)
  • 正确@zfrisch 我现在明白了。感谢您花时间解释它
  • 请将++counter 更改为counter+1。它们在功能上是等效的,但后者更好地指定了意图,不会(不必要地)变异和参数,并且没有意外后递增的可能性。或者更好的是,因为它是尾调用,所以使用循环代替。
【解决方案8】:

由于您生成所有数字,另一种方法是使用生成器。

最后一个元素是您的编号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>

最后的想法

您可能需要考虑在您的函数中使用 return-early 条件。任何带有零的数字返回零。

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

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

对于仅由1 组成的任何数字都可以这样说。

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

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

最后,您没有说明您是否只接受正整数。如果您接受负整数,那么将它们转换为字符串 可能 是有风险的:

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

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

可能的解决方案:

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

mult(39)
//=> 27

mult(-39)
//=> -27

【讨论】:

  • 太棒了。 @customcommander 感谢您非常清楚地解释这一点
【解决方案9】:

传统的解决方案是按照另一个答案的建议将计数作为参数传递给函数。

但是,js中还有另一种解决方案。其他一些答案建议在递归函数之外简单地声明计数:

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

这当然有效。然而,这使得函数不可重入(不能正确调用两次)。在某些情况下,您可以忽略此问题,只需确保您不调用 singleDigit 两次(javascript 是单线程的,因此不太难做到),但如果您稍后将 singleDigit 更新为,这是一个等待发生的错误是异步的,感觉也很丑。

解决方案是在外部而不是全局声明counter 变量。这是可能的,因为 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
}

这类似于全局解决方案,但每次调用singleDigit(现在不是递归函数)时,它都会创建counter 变量的新实例。

【讨论】:

  • 计数器变量仅在 singleDigit 函数中可用,并提供了另一种干净的方式来执行此操作,而无需传递参数 imo。 +1
  • 由于recursion 现在完全隔离,将计数器作为最后一个参数传递应该是完全安全的。我不认为创建内部函数是必要的。如果您不喜欢为了递归的唯一利益而使用参数的想法(我确实认为用户可能会弄​​乱这些参数),然后在部分应用的函数中使用 Function#bind 锁定它们。
  • @customcommander 是的,我在回答的第一部分总结了这一点——the traditional solution is to pass the count as a parameter。这是具有闭包的语言的替代解决方案。在某些方面它更容易理解,因为它只是一个变量,而不是可能无限数量的变量实例。在其他方面,当您跟踪的对象是共享对象(想象构建唯一的地图)或非常大的对象(例如 HTML 字符串)时,了解此解决方案会有所帮助
  • counter-- 将是解决您“不能正确调用两次”的传统方法
  • @MonkeyZeus 这有什么区别?另外,你怎么知道初始化计数器的数字是多少?
猜你喜欢
  • 2022-01-24
  • 1970-01-01
  • 1970-01-01
  • 2022-01-23
  • 2021-09-02
  • 2021-07-19
  • 2012-08-29
  • 1970-01-01
  • 2012-03-12
相关资源
最近更新 更多