Algorithm 将线性递归函数重写为尾部递归函数
我想知道,当我将一个“常规”递归函数重写为尾部递归函数时,我该如何继续?有什么策略吗 例如:我有一个简单的函数,我想把它变成一个尾部递归函数:Algorithm 将线性递归函数重写为尾部递归函数,algorithm,recursion,pseudocode,tail-recursion,Algorithm,Recursion,Pseudocode,Tail Recursion,我想知道,当我将一个“常规”递归函数重写为尾部递归函数时,我该如何继续?有什么策略吗 例如:我有一个简单的函数,我想把它变成一个尾部递归函数: int rec(int n) { if (n % 2 || n > 10) { return n; } return rec(n+1) + rec(n+2); } 非常感谢您的帮助。答案取决于您对“尾部递归”函数和“重写”函数的定义,但我将假设您对以下内容有一个非正式的理解: 没有不在原始版本中的迭代 没有原始
int rec(int n) {
if (n % 2 || n > 10) {
return n;
}
return rec(n+1) + rec(n+2);
}
非常感谢您的帮助。答案取决于您对“尾部递归”函数和“重写”函数的定义,但我将假设您对以下内容有一个非正式的理解:
- 没有不在原始版本中的迭代
- 没有原始版本中没有的显式堆栈/数组等
- 除了尾部递归调用之外,没有递归调用(这意味着,调用是
语句中的整个表达式)return
- 辅助函数可以,但相互递归不行
- 由实际考虑因素(如整数溢出、堆栈溢出、舍入错误等)导致的非等效性不受关注
那太离谱了。您应该知道,以尾部递归的方式重写函数并不总是可能的。您自己的示例中有一个函数,在非基情况下进行两次递归调用,我们显然不能将它们都更改为
return
语句中的完整表达式,因此您的示例可以重写为尾部递归的唯一原因是我们可以消除其中一次调用
那么,让我们从函数开始:
int rec(int n) {
if (n % 2 || n > 10) {
return n; // line 3
}
return rec(n+1) + rec(n+2); // line 5
}
现在,如果n
是奇数,那么我们在第3行返回n
;所以如果我们到达第5行,那么我们知道n
必须是偶数。这意味着如果我们到达第5行,那么n+1
是奇数,rec(n+1)
将是n+1
。因此,我们可以消除一个递归调用:
int rec(int n) {
if (n % 2 || n > 10) {
return n;
}
return (n+1) + rec(n+2);
}
接下来,它有助于展开一个真实的示例,以了解其外观:
rec(6) = 7 + rec(8)
= 7 + (9 + rec(10))
= 7 + (9 + (11 + rec(12)))
= 7 + (9 + (11 + (12)))
一个重要的见解是,由于加法是关联的,我们可以用相反的方式对术语进行分组:
rec(6) = ((7 + 9) + 11) + 12
这很有用,因为这意味着我们可以在继续进行时计算结果的部分和,并将其作为单独的参数传递:
int rec(int n, int sum_so_far) {
if (n % 2 || n > 10) {
return sum_so_far + n;
}
return rec(n+2, sum_so_far + (n+1));
}
。现在我们有了一个尾部递归函数,但它需要客户端传入一个额外的参数!为了解决这个问题,我们只需将其重命名为rec\u helper
,并将其封装在一个函数中,以便客户端调用:
int rec_helper(int n, int sum_so_far) {
if (n % 2 || n > 10) {
return sum_so_far + n;
}
return rec_helper(n+2, sum_so_far + (n+1));
}
int rec(int n) {
return rec_helper(n, 0);
}
正如你所见,没有一个总的战略;相反,我们需要分析函数,并利用有关整数的事实,以消除一个递归调用,然后我们需要再次执行相同的操作,将另一个递归调用转换为尾部递归调用
然而,我们所做的一个方面是一个非常常见的模式,即将递归移到一个辅助函数中,该辅助函数接受一个附加参数,其中该参数包含迄今为止计算的部分结果。我想说,在构造尾部递归函数时,我至少有90%的时间使用这种模式。答案取决于您对“尾部递归”函数和“重写”函数的定义,但我将假设大致是非正式的理解:
- 没有不在原始版本中的迭代
- 没有原始版本中没有的显式堆栈/数组等
- 除了尾部递归调用之外,没有递归调用(这意味着,调用是
语句中的整个表达式)return
- 辅助函数可以,但相互递归不行
- 由实际考虑因素(如整数溢出、堆栈溢出、舍入错误等)导致的非等效性不受关注
那太离谱了。您应该知道,以尾部递归的方式重写函数并不总是可能的。您自己的示例中有一个函数,在非基情况下进行两次递归调用,我们显然不能将它们都更改为
return
语句中的完整表达式,因此您的示例可以重写为尾部递归的唯一原因是我们可以消除其中一次调用
那么,让我们从函数开始:
int rec(int n) {
if (n % 2 || n > 10) {
return n; // line 3
}
return rec(n+1) + rec(n+2); // line 5
}
现在,如果n
是奇数,那么我们在第3行返回n
;所以如果我们到达第5行,那么我们知道n
必须是偶数。这意味着如果我们到达第5行,那么n+1
是奇数,rec(n+1)
将是n+1
。因此,我们可以消除一个递归调用:
int rec(int n) {
if (n % 2 || n > 10) {
return n;
}
return (n+1) + rec(n+2);
}
接下来,它有助于展开一个真实的示例,以了解其外观:
rec(6) = 7 + rec(8)
= 7 + (9 + rec(10))
= 7 + (9 + (11 + rec(12)))
= 7 + (9 + (11 + (12)))
一个重要的见解是,由于加法是关联的,我们可以用相反的方式对术语进行分组:
rec(6) = ((7 + 9) + 11) + 12
这很有用,因为这意味着我们可以在继续进行时计算结果的部分和,并将其作为单独的参数传递:
int rec(int n, int sum_so_far) {
if (n % 2 || n > 10) {
return sum_so_far + n;
}
return rec(n+2, sum_so_far + (n+1));
}
。现在我们有了一个尾部递归函数,但它需要客户端传入一个额外的参数!为了解决这个问题,我们只需将其重命名为rec\u helper
,并将其封装在一个函数中,以便客户端调用:
int rec_helper(int n, int sum_so_far) {
if (n % 2 || n > 10) {
return sum_so_far + n;
}
return rec_helper(n+2, sum_so_far + (n+1));
}
int rec(int n) {
return rec_helper(n, 0);
}
正如你所见,没有一个总的战略;相反,我们需要分析函数,并利用有关整数的事实,以消除一个递归调用,然后我们需要再次执行相同的操作,将另一个递归调用转换为尾部递归调用
然而,我们所做的一个方面是一个非常常见的模式,即将递归移到一个辅助函数中,该辅助函数接受一个附加参数,其中该参数包含迄今为止计算的部分结果。我想说,在构造尾部递归函数时,我至少有90%的时间使用这种模式。您需要从基本情况开始,查看最接近基本情况的默认情况的模式。在您的情况下,所有基本情况都会累加,直到n为11,因此与基本情况最接近的默认情况是
9
。下一个是7
,它包括9
,因此我们有这个模式:
n 0 1 2 3 4 5 6 7 8 9 10 11 12 13 ...
r 0 | 2 | 4 | 6 | 8 | 10 11 12 13
| | | | 10+11
| | | 8+10+11
| | 6+8+10+11
| 4+6+8+10+11
2+4+6+8+10+11
基于此,下面所有的奇数都是显而易见的