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

chs*_*242 63 javascript recursion

 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)
Run Code Online (Sandbox Code Playgroud)

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

示例为 39。

3 x 9 = 27.
2 x 7 = 14.
1 x 4 = 4.
Run Code Online (Sandbox Code Playgroud)

控制台将记录:

27 
14 
4
Run Code Online (Sandbox Code Playgroud)

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

我曾尝试添加计数器,但无法更新。将不胜感激任何帮助

She*_*aff 76

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

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

  • 请将“++counter”更改为“counter+1”。它们在功能上是等效的,但后者更好地指定意图,不会(不必要地)变异和参数,并且不存在意外后递增的可能性。或者更好的是,因为它是尾部调用,所以使用循环代替。 (35认同)
  • @chs242 这不是您在函数中声明的。从技术上讲,所有默认参数也是如此 - 在您的情况下,只是该值永远不会延续到下一次函数被递归调用时。ae 每次函数运行时,“counter”都会被废弃并设置为“0”,*除非*您像 Sheraff 那样在递归调用中明确地携带它。Ae `singleDigit(数字, ++计数器)` (10认同)
  • @chs242 范围规则将规定在函数中声明它会在每次调用时创建一个新的。/sf/ask/35030201/ (7认同)
  • 惊人的。看起来我的计数器不起作用,因为我在函数中声明了它 (6认同)
  • 对@zfrisch 我现在明白了。感谢您花时间解释 (2认同)

sle*_*man 38

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

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

let counter = 0
function singleDigit(num) {
  counter++;
  // ..
}
Run Code Online (Sandbox Code Playgroud)

这当然有效。然而,这使得函数不可重入(不能正确调用两次)。在某些情况下,您可以忽略此问题,只需确保您不调用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
}
Run Code Online (Sandbox Code Playgroud)

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


phi*_*ler 27

这几乎是纯粹的学术变体,但您可以为此目的使用修改后的定点组合器。

让我们稍微缩短和改进您的原始函数:

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

// singleDigit(123234234) == 0
Run Code Online (Sandbox Code Playgroud)

从这个变体中,我们可以将递归调用分解出来并进行柯里化:

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

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

function Ynormal(f, ...args) {
    let Y = (g) => g(() => Y(g));
    return Y(f)(...args);
}
Run Code Online (Sandbox Code Playgroud)

我们在哪里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];
}
Run Code Online (Sandbox Code Playgroud)

快速检查节点 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 ]
Run Code Online (Sandbox Code Playgroud)

这个组合器现在将用于计算任何singleDigitF.

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

  • 对于反对者:我真的同意你的观点,这不是最好的实际解决方案——这就是为什么我将其前缀为“纯学术”。 (6认同)

cus*_*der 22

另一种方法,因为你产生所有的数字,是使用生成器。

最后一个元素是您的数字n减少到一位数,并计算您迭代了多少次,只需读取数组的长度。

const digits = [...to_single_digit(39)];
console.log(digits);
//=> [27, 14, 4]
Run Code Online (Sandbox Code Playgroud)
<script>
function* to_single_digit(n) {
  do {
    n = [...String(n)].reduce((x, y) => x * y);
    yield n;
  } while (n > 9);
}
</script>
Run Code Online (Sandbox Code Playgroud)


最后的想法

您可能需要考虑在您的函数中设置早返回条件。任何带有零的数字都将返回零。

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

// possible solution: String(n).includes('0')
Run Code Online (Sandbox Code Playgroud)

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

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

// possible solution: [...String(n)].every(n => n === '1')
Run Code Online (Sandbox Code Playgroud)

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

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

[...String(-39)].reduce((x, y) => x * y)
//=> NaN
Run Code Online (Sandbox Code Playgroud)

可能的解决方案:

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

mult(39)
//=> 27

mult(-39)
//=> -27
Run Code Online (Sandbox Code Playgroud)


Kho*_*vko 5

您可以为此使用闭包。

只需简单地存储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);
Run Code Online (Sandbox Code Playgroud)


Pim*_*kit 5

如果你只是想指望它得到了多少次下降,并且不关心递归...你可以只取出递归。下面的代码仍然忠实于原始帖子,因为它不算num <= 9作需要减少。因此,singleDigit(8)will havecount = 0singleDigit(39)will have 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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


对速度的进一步思考

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

使用.map(Number)console.log(在每个缩减步骤中)都需要非常长的执行时间并且是不必要的。简单地.map(Number)从 OP 中删除,速度提高了大约 4.38 倍。删除console.log速度如此之快,以至于几乎不可能正确测试(我不想等待它)。

因此,类似于customcommander的答案,不使用.map(Number)norconsole.log并将结果推送到数组中,而使用.lengthforcount快得多。不幸的是,对于customcommander的回答,使用生成器函数真的很慢(该答案比没有.map(Number)和的 OP 慢约 2.68 倍console.log

另外,.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
Run Code Online (Sandbox Code Playgroud)

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

PT


Sco*_*yet 5

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

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

  {
    digit: 4,
    steps: [39, 27, 14, 4],
    calls: 3
  }
Run Code Online (Sandbox Code Playgroud)

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

这是一个这样做的版本:

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))
Run Code Online (Sandbox Code Playgroud)

请注意,我们跟踪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, [])
}
Run Code Online (Sandbox Code Playgroud)

在任何一种情况下,将数字乘法提取到辅助函数中可能会更简洁一些:

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])
Run Code Online (Sandbox Code Playgroud)

  • 另一个很好的答案;)请注意,当 _n_ 为负数时, `digitProduct` 将返回 `NaN` (`-39 ~&gt; ('-' * '3') * '9'`)。因此,您可能想要使用 _n_ 的绝对值,并使用 `-1` 或 `1` 作为您的reduce 的初始值。 (2认同)

Mis*_*att 5

为什么不在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)
Run Code Online (Sandbox Code Playgroud)

我让它在 Chrome 79 和 Firefox 72 中工作

  • 我不明白你的问题,因为我在 Chrome 和 Firefox 中都可以使用它,我在答案中添加了一个片段 (2认同)