跟踪多少次调用递归函数


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)

上面的代码采用一个整数,并将其乘以自己的数字,将其减少为一位数字。

示例是39。

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

控制台将记录:

27 
14 
4

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

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


4
.map(Number)这是多余的,因为*操作员无论如何都将值强制为数字。;-)
RobG

4
有两个问题:1)您打算如何处理负数?例如,数字-57实际上是a -50-7..当以这种方式查看时,它将减少-5x -7从而产生正数35。还是您希望它只看到带有5而不是的负号7,甚至7实际上也就是负的。2)您打算如何处理包含零的数字?因为这将自动将减少量归零。因此,您传入的数字越大,则归零的可能性就越大。另一种选择是跳过零
Pimp Trizkit

3
我意识到我上面的问题不是关于计算递归,而是关于此问题中使用的内容的难题解决方面。请原谅我。
Pimp Trizkit

3
我很高兴您喜欢我的回答,但出于实际目的,我认为stackoverflow.com/a/59570894/1346276是最干净的常规变体。
phipsgabler

2
@phipsgabler任何花时间写一个聪明而连贯的答案的人都应该得到一个赞。谢谢
chs242

Answers:


25

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

让我们缩短并改善您的原始功能:

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

// singleDigit(123234234) == 0

从这个变体中,我们可以排除并处理递归调用:

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 ]

现在,此组合器将用于计算以样式编写的任何递归函数中的调用次数singleDigitF

(请注意,有越来越零是一个非常常见的回答有两个来源:数字溢出(123345456999999999成为123345457000000000等),以及当输入的大小增加时,几乎肯定会在某个地方得到零作为中间值的事实。)


6
对于下注者:我真的同意您的观点,这不是最佳的实际解决方案-这就是为什么我将其前缀为“纯学术性”。
phipsgabler

老实说,这是一个很棒的解决方案,完全适合原始问题的回归/数学类型。
谢拉夫

73

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

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

6
太棒了 看来我的计数器无法正常工作,因为我在函数中声明了它
chs242

7
@ chs242范围规则将要求在函数中对其进行声明将在每次调用时创建一个新的。 stackoverflow.com/questions/500431/...
Taplar

10
@ chs242并不是您在函数中声明了它。从技术上讲,所有默认参数也都执行得很好-在您的情况下,仅是该值永远不会保留到下次递归调用该函数时。除非您像Sheraff那样在递归调用中明确地继承了该函数,否则每次运行该函数时,ae都会counter被废弃并设置为。AE0singleDigit(number, ++counter)
zfrisch

2
对@zfrisch我现在明白了。感谢您抽出
宝贵

35
请更改++countercounter+1。它们在功能上是等效的,但是后者可以更好地指定意图,不会(不必要地)进行变异和参数化,并且不会意外地增加增量。还是更好,因为这是一个尾声,所以请使用循环。
BlueRaja-Danny Pflughoeft

37

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

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

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变量的新实例。


1
counter变量仅在singleDigit函数内可用,并且提供了另一种干净的方式来执行此操作,而无需传递imo参数。+1
AndrewL64

1
由于recursion现在已完全隔离,因此将计数器作为最后一个参数传递应该是完全安全的。我认为没有必要创建内部函数。如果您不喜欢仅出于递归的目的拥有参数的想法(我确实认为用户可能会弄​​乱这些参数),则可以将它们锁定Function#bind在部分应用的函数中。
customcommander

@customcommander是的,我在答案的第一部分总结中提到了- the traditional solution is to pass the count as a parameter。这是具有闭包的语言的替代解决方案。从某些方面讲,它更容易理解,因为它只是一个变量,而不是无限数量的变量实例。从其他方面来说,当您跟踪的对象是共享对象(想象构造一个唯一的地图)或很大的对象(例如HTML字符串)时,知道此解决方案将有所帮助
slebetman

counter--将是解决您“无法正确调用两次”的
说法

1
@MonkeyZeus有什么区别?另外,您如何知道要初始化计数器的数字,以查看它是我们要查找的计数?
slebetman

22

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

最后一个元素是将您的数字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>


最后的想法

您可能要考虑在您的函数中有一个早返状态。其中任何零的数字都将返回零。

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感谢您非常清楚地解释了这一点
chs242

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

2
另一个好答案;)请注意,当n为负数时,digitProduct将返回NaN-39 ~> ('-' * '3') * '9')。因此,您可能想要使用n的绝对值,并使用-11作为reduce的初始值。
customcommander

@customcommander:实际上,它将{"digit":-39,"steps":[-39],"calls":0}从返回-39 < 9。尽管我同意这可能与某些错误检查有关:参数是否为数字?- 是正整数吗?-等等。我认为我不会对此进行更新。这捕获了算法,并且错误处理通常特定于一个人的代码库。
Scott Sauyet

6

如果您只是想计算减少的次数并且不特别在乎递归,则可以删除递归。以下代码忠实于原始帖子,因为它不算num <= 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 <= 9)。不幸的是,num <= 9即使不计入OP代码,它也会进行处理。上面的代码根本不会处理也不计数num <= 9。它只是通过它。

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


进一步思考速度

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

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

因此,与customcommander的答案类似,不使用.map(Number)nor console.log并将结果推入数组并使用.lengthfor count则要快得多。不幸的是,对于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

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

PT


1
这个答案有很多教育价值,所以谢谢。I feel good code is also fast.我想说代码质量必须根据一组预定义的要求进行衡量。如果性能不是其中之一,那么用“快速”代码替换任何人都可以理解的代码,您将一无所获。您不会相信我所见过的大量代码已被重构为可以执行到无法再理解的程度了(由于某种原因,最佳代码也往往是未记录的;)。最后,请注意,延迟生成的列表允许用户按需消费项目。
customcommander

我想谢谢你。恕我直言,对我而言,阅读实际的数学运算方法比在大多数答案中看到的[...num+''].map(Number).reduce((x,y)=> {return x*y})甚至什至[...String(num)].reduce((x,y)=>x*y)陈述更容易理解。因此,对我来说,这样做的另一个好处是可以更好地了解每次迭代中发生的事情而且速度更快。是的,极小的代码(有其位置)很难阅读。但是在那种情况下,人们通常有意识地不在乎其可读性,而只是剪切,粘贴并继续前进的最终结果。
Pimp Trizkit

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以来已经有一段时间了,但我认为在那里也是对的。
Pimp Trizkit

6

为什么不在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对每次调用该函数都会重置计数器没有帮助(如上面答案中所述)
chs242

2
我无法理解您的问题,因为我可以在Chrome和Firefox中使用它,因此我在答案中添加了一个
摘要

6

您可以为此使用闭包。

只需简单地存储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);


1
但是通过这种方式,计数器会继续计数下一个呼叫,因此需要在每个初始呼叫时将其重置。这就引出了一个难题:如何判断何时从不同的上下文中调用递归函数,在这种情况下是全局vs函数。
RobG

这只是为了提出想法的示例。可以通过询问用户的需求进行修改。
Kholiavko

@RobG我不明白你的问题。由于递归函数是内部函数,因此不能在闭包外部调用。因此,没有可能或不需要区分上下文,因为只有一种可能的上下文
slebetman

@slebetman计数器永远不会重置。singleDigitDecorator()每次调用时,返回的函数将使同一计数器递增。
customcommander

1
@slebetman-问题是由singleDigitDecorator返回的函数在再次调用时不会重置其计数器。该功能需要知道何时重置计数器,否则每次使用都需要一个新的功能实例。Function.caller的可能用例?;-)
RobG

1

正如slebetman的答案所建议的那样,这是一个使用包装函数简化计数器的Python版本-我之所以写此代码,仅是因为此实现中的核心思想非常明确:

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)

1
在Python,我宁愿像这样
phipsgabler
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.