编写递归函数时,一个非常常见的初学者错误是意外触发来自同一函数的完全冗余的递归调用。例如,考虑这个递归函数,它在二叉树(而不是二叉搜索树)中找到最大值:
int BinaryTreeMax(Tree* root) {
if (root == null) return INT_MIN;
int maxValue = root->value;
if (maxValue < BinaryTreeMax(root->left))
maxValue = BinaryTreeMax(root->left); // (1)
if (maxValue < BinaryTreeMax(root->right))
maxValue = BinaryTreeMax(root->right); // (2)
return maxValue;
}
请注意,此程序可能会在行(1)和(2)中对BinaryTreeMax
进行两次完全冗余的递归调用。我们可以重写这段代码,只需缓存之前的值就不需要这些额外的调用:
int BinaryTreeMax(Tree* root) {
if (root == null) return INT_MIN;
int maxValue = root->value;
int leftValue = BinaryTreeMax(root->left);
int rightValue = BinaryTreeMax(root->right);
if (maxValue < leftValue)
maxValue = leftValue;
if (maxValue < rightValue)
maxValue = rightValue;
return maxValue;
}
现在,我们总是进行两次递归调用。
我的问题是,是否有一个工具可以对程序进行静态或动态分析(用你喜欢的任何语言;我不太挑剔!),它可以检测程序是否完全不必要的递归调用。 “完全没必要”我的意思是
这通常可以通过手工确定,但我认为如果有一些工具可以自动标记这样的事情作为一种帮助学生获得有关如何避免简单但昂贵的错误的反馈的方式,那将是很好的在他们的计划中可能导致巨大的低效率。
有谁知道这样的工具?
答案 0 :(得分:1)
首先,你对'完全没必要'的定义是不够的。两个函数调用之间的某些代码可能会影响第二个函数调用的结果。
其次,这与递归无关,同样的问题可以应用于任何函数调用。如果之前使用完全相同的参数调用它,则没有副作用,并且两次调用之间没有代码更改了函数访问的任何数据。
现在,我非常确定一个完美的解决方案是不可能的,因为它会解决The Halting Problem,但这并不意味着没有办法检测到足够的这些情况并优化掉其中一些
有些编译器知道如何做到这一点(GCC有一个特定的标志,当它这样做时会发出警告)。这是我在2003年发现的关于这个问题的文章:http://www.cs.cmu.edu/~jsstylos/15745/final.pdf。
我找不到这方面的工具,但这可能是Eric Lipert所知道的,如果他碰巧碰到了你的问题。
答案 1 :(得分:0)
某些编译器(如GCC)确实有明确标记确定函数的方法(更准确地说,__attribute__((const))
(请参阅GCC function attributes)对函数体施加一些限制,使其结果仅依赖于从它的参数,并没有从共享的程序状态或其他非确定性函数得到依赖)。然后他们消除了对costy函数的重复调用。其他一些高级语言实现(可能是Haskell)会自动进行此测试。
真的,我不知道这种分析的工具(但如果我发现它会很高兴)。如果有一个可以正确地检测到不必要的递归,或者通常是功能评估(在语言不可知的环境中),它将是一种确定性证明。
顺便说一句,当您已经访问代码的语义树时,编写此类程序并不困难:)