我试图弄清楚以下示例中双重递归是如何工作的:
#include <iostream>
#include <vector>
int F(std::vector<int> &v, int a, int &b) {
if (a <= 0 || b <= 2) return a;
b -= 1;
a = F(v, a - 1, b) + F(v, a, b);
v.push_back(a);
return a;
}
int main(){
std::vector<int> v;
int b = 7;
F(v, 15, b);
for(int i=0;i<v.size();i++)
std::cout<<v[i]<<" ";
}
输出:
21 33 46 60 75
我理解 21 是如何被推入向量的。但之后,我不明白接下来会调用哪个递归(左递归还是右递归),以及如何跟踪直到得到正确的输出。
您能否一步一步地向我解释一下这个特定程序是如何执行的?
记录如下:
语言律师的专栏
不幸的是,这个例子是 UB,因为在表达式中:
v 和 b 通过引用传递,并且根据代码会产生副作用:
operator+
然后,该表达式中 左项和右项的求值不按顺序进行,也不一定从左到右进行求值。在纯文本中,编译器可以选择首先计算的任何项:而对同一内存位置的未排序的副作用是 UB,这是两个函数调用都修改的 v 的情况:
这意味着任何事情都可能发生,包括从一个调用到另一个调用获得不同的结果或者(即使在这种情况下不太可能)崩溃!
请注意,这种排序规则在不同语言中是不同的
如何纠正你的双重递归?
最简单的方法是强制排序,将递归调用分成两个不同的语句:
然后,如果愿意的话,您可以通过切换第 1 行和第 2 行来完全指定双重递归的顺序。
另一种方法是重构递归函数,使其不依赖引用,除非它们是只读的。
那么双重递归如何工作?
Paul 的回答非常详细。这里无需重复。
简而言之
C++ 的优先规则并未定义双重递归中先调用左项还是右项。事实上,根据编译器选择的顺序,您可能会使用相同的代码获得不同的结果。
深入演示
来自你的原贴:
以上是帮助防止递归永远递归的基本情况。
第一项是调用(根据假设)。最初 b=7,a=15。然后,由于 a 在调用中立即递减,而 b 在 F 中递减,因此 a 和 b 一起递减。由于 ab > 2,基本情况将在 a <=0 之前达到 b == 2。
在递归中,第一项被一次又一次地调用,直到 b == 2。只有这样,第二项才会被调用,现在 a 不会在调用中递减。在第二个 F 中,第一个 F 再次被调用(根据您的假设)。这一次,b 仍然是 2,因为它直到基本情况之后才递减。在您的调试器中,请务必观察调用堆栈并观察递归的展开。
一个棘手的问题。调用第一个F时的参数a比调用第二个F时的参数a小一。记住这一点就行。
我很好奇结果会有多大的不同。变化由添加的
a-1
标志控制。flag_am1
=flag_am1
1:确定性,首先评估 a-1 项。flag_am1
= 0:确定性,其次评估 a-1 项。flag_am1
= -1:与 OP 相同flag_am1
= -2:与 OP 类似,只是两个函数的顺序互换了。请注意,对于
flag_am1
= -2 的情况,查看四组输出时要记住的重要一点是,对于情况
flag_am1
= -1 或 = -2,您的结果可能会有所不同。输出:
C++17 中的:
您发布的输出仅当编译器在每次表达式求值
F(v, a - 1, b)
之前执行子表达式求值时才会生成。F(v, a, b)
F(v, a - 1, b) + F(v, a, b)
您可以通过交换操作数(子表达式)进行快速检查 -
F(v, a, b) + F(v, a - 1, b)
。这样,您可能会得到不同的输出。给定表达式的递归调用轨迹将类似于二叉树,但这里有一个问题 -
b
函数的参数F()
是引用。这意味着,值的任何变化b
都会反映在其被引用的任何地方。a
对于给定的参数和的值b
(a = 15
和b = 7
),一旦的值b
变为2
,条件b <= 2
就会得到满足,并且对函数的其余递归调用F()
将简单地返回的值a
(无论在该函数调用中作为参数传递什么F()
)。假设,在每次调用函数时
F()
,当评估表达式F(v, a - 1, b) + F(v, a, b)
表达式时,编译器首先评估子表达式F(v, a - 1, b)
,则递归树将是这样的:因此,您获得的输出是: