我最近在几个不同的地方看到过这样的评论:“我在学校学习了递归,但从那时起就没有使用过它,或者从那时起就觉得有必要。” (递归似乎是某一类程序员中“书本学习”的一个流行的例子。)
嗯,在Java和Ruby [1]等命令式语言中,我们通常使用迭代并避免递归,部分原因是存在堆栈溢出的风险,部分原因是因为它是这些语言中大多数程序员的风格习惯了。
现在我知道,严格来说,在这些语言中没有“必要的”递归用法:无论事情多么复杂,都可以用迭代替换递归。在这里“必要”,我在谈论以下内容:
你能想到这些语言中代码的任何特定例子,其中递归比迭代(为了清晰,效率或其他原因)要好得多,无论如何你使用递归,转换到迭代会是一个很大的损失?
在答案中已多次提到递归行走的树:如果它可用,那么它对于使用它的特定用途是什么使得递归比使用库定义的迭代器更好?
[1]:是的,我知道这些也是面向对象的语言。然而,这与这个问题没有直接关系。
答案 0 :(得分:9)
递归没有“必要的”使用。所有递归算法都可以转换为迭代算法。我似乎记得堆栈是必要的,但我无法回想起我头顶的确切结构。
实际上,如果你没有在下面使用递归(即使是在命令式语言中),你有点生气:
答案 1 :(得分:6)
当您走任何类型的树结构时,例如
同样,调用对象成员的toString()的每个toString()方法也可以被认为是递归的。所有对象序列化算法都是递归的。
答案 2 :(得分:5)
在我的工作中,递归很少用于任何算法。使用简单的循环可以更加可读地(并且有效地)解决诸如阶乘等问题。当它出现时,通常是因为您正在处理一些本质上递归的数据。例如,树结构上的节点可以递归处理。
例如,如果您要编写一个程序来遍历二叉树的节点,您可以编写一个处理一个节点的函数,并调用它来处理每个节点的子节点。这比在遍历它们时尝试维护每个子节点的所有不同状态更有效。
答案 3 :(得分:5)
最着名的例子可能是C.A.R.开发的快速排序算法。霍尔。
另一个例子是遍历目录树以查找文件。
答案 4 :(得分:4)
在我看来,当数据结构也是递归的时候,递归算法是很自然的。
def traverse(node, function):
function(this)
for each childnode in children:
traverse(childnode, function)
我不明白为什么我想要迭代地写这个。
答案 5 :(得分:1)
所有关于您正在处理的数据。
我编写了一个简单的解析器来将字符串转换为数据结构,它可能是5年来Java中唯一的例子,但我认为这是正确的方法。
字符串看起来像这样:
"{ index = 1, ID = ['A', 'B', 'C'], data = {" +
"count = 112, flags = FLAG_1 | FLAG_2 }}"
最好的抽象是树,其中所有叶节点都是原始数据类型,分支可以是数组或对象。这是典型的递归问题,非递归解决方案是可能的,但更复杂。
答案 6 :(得分:1)
递归总是可以用外部堆栈重写为迭代。但是,如果你确定不会有非常深的递归风险导致stackoverflow,那么递归是一件非常方便的事情。
一个很好的例子是遍历已知操作系统上的目录结构。您通常知道它有多深(最大路径长度有限),因此不会有堆栈溢出。通过使用外部堆栈进行迭代来做同样的事情并不方便。
答案 7 :(得分:1)
有人说“什么树”。我可能过于谨慎了,而且我知道堆栈现在很大,但我仍然不会在典型的树上使用递归。不过,我会在 balanced tree 上进行此操作。
答案 8 :(得分:0)
我有一份报告清单。我在我的类中使用包含此列表的索引器。使用索引器通过屏幕名称检索报告。在索引器中,如果该屏幕名称的报告不存在,则会加载报告并以递归方式调用自身。
public class ReportDictionary
{
private static List<Report> _reportList = null;
public ReportColumnList this[string screenName]
{
get
{
Report rc = _reportList.Find(delegate(Report obj) { return obj.ReportName == screenName; });
if (rc == null)
{
this.Load(screenName);
return this[screenName]; // Recursive call
}
else
return rc.ReportColumnList.Copy();
}
private set
{
this.Add(screenName, value);
}
}
}
使用一些额外的代码行可以在不递归的情况下完成。