Recursion 什么是递归?我应该在什么时候使用它?
邮件列表和在线讨论中经常出现的话题之一是攻读计算机科学学位的优点(或不足)。对于消极的一方来说,一个似乎反复出现的论点是,他们已经编码了好几年了,而且从来没有使用过递归Recursion 什么是递归?我应该在什么时候使用它?,recursion,computer-science,Recursion,Computer Science,邮件列表和在线讨论中经常出现的话题之一是攻读计算机科学学位的优点(或不足)。对于消极的一方来说,一个似乎反复出现的论点是,他们已经编码了好几年了,而且从来没有使用过递归 A child couldn't sleep, so her mother told her a story about a little frog, who couldn't sleep, so the frog's mother told her a story about a little bear,
A child couldn't sleep, so her mother told her a story about a little frog,
who couldn't sleep, so the frog's mother told her a story about a little bear,
who couldn't sleep, so the bear's mother told her a story about a little weasel...
who fell asleep.
...and the little bear fell asleep;
...and the little frog fell asleep;
...and the child fell asleep.
因此,问题是:
int fact(int a)
{
if(a==1)
return 1;
return a*fact(a-1);
}
一般来说,递归不一定很快(函数调用开销往往很高,因为递归函数往往很小,见上文),并且可能会遇到一些问题(堆栈溢出?)。有人说,在非琐碎的情况下,他们往往很难做到“正确”,但我并不真的相信这一点。在某些情况下,递归最有意义,也是编写特定函数最优雅、最清晰的方法。需要注意的是,有些语言更喜欢递归解决方案并对其进行优化(我想到的是LISP)。递归函数是一个调用自身的函数。我发现使用它的最常见原因是遍历树结构。例如,如果我有一个带有复选框的树状视图(比如安装一个新程序,“选择要安装的功能”页面),我可能需要一个“全选”按钮,它类似于以下内容(伪代码): 因此您可以看到,checkrecursive首先检查它所传递的节点,然后为该节点的每个子节点调用自己 您确实需要对递归小心一点。如果进入无限递归循环,将出现堆栈溢出异常:) 我想不出为什么人们不应该在适当的时候使用它。它在某些情况下有用,而在其他情况下则不有用
我认为,因为这是一种有趣的技术,一些编码人员可能会在没有真正理由的情况下更多地使用它。这给递归在某些圈子里带来了坏名声。递归是一种基于分而治之思想解决问题的方法。 基本思想是将原始问题分解为更小(更容易解决)的实例,求解这些更小的实例(通常再次使用相同的算法),然后将它们重新组合成最终的解决方案 规范示例是一个生成n的阶乘的例程。n的阶乘是通过将1和n之间的所有数字相乘来计算的。C#中的迭代解如下所示:
public int Fact(int n)
{
int fact = 1;
for( int i = 2; i <= n; i++)
{
fact = fact * i;
}
return fact;
}
// In FactRec(5)
return 5 * FactRec( 5 - 1 );
// which is
return 5 * FactRec(4);
如果我们使用参数4重新输入该方法,那么guard子句将再次阻止我们,因此我们将在以下位置结束:
// In FactRec(4)
return 4 * FactRec(3);
如果我们将这个返回值替换为上面的返回值,我们得到
// In FactRec(5)
return 5 * (4 * FactRec(3));
这将为您提供一条关于最终解决方案是如何得出的线索,因此我们将快速跟踪并展示下一步的每一步:
return 5 * (4 * FactRec(3));
return 5 * (4 * (3 * FactRec(2)));
return 5 * (4 * (3 * (2 * FactRec(1))));
return 5 * (4 * (3 * (2 * (1))));
当基本情况被触发时,将发生最终替换。在这一点上,我们有一个简单的algrebraic公式要求解,它首先等同于阶乘的定义
值得注意的是,对该方法的每次调用都会触发一个基本情况,或者对参数更接近基本情况的同一方法进行调用(通常称为递归调用)。如果不是这种情况,那么该方法将永远运行。递归最适合于我喜欢称之为“分形问题”的东西,在这里,你处理的是一个大东西,它由大东西的更小版本组成,每个版本都是大东西的更小版本,依此类推。如果必须遍历或搜索树或嵌套的相同结构之类的对象,则可能会遇到一个很适合递归的问题
A child couldn't sleep, so her mother told her a story about a little frog,
who couldn't sleep, so the frog's mother told her a story about a little bear,
who couldn't sleep, so the bear's mother told her a story about a little weasel...
who fell asleep.
...and the little bear fell asleep;
...and the little frog fell asleep;
...and the child fell asleep.
人们避免递归的原因有很多:
这里有一个简单的例子:一个集合中有多少个元素。(有更好的计算方法,但这是一个很好的简单递归示例。) 首先,我们需要两条规则:
count of [x x x] = 1 + count of [x x]
= 1 + (1 + count of [x])
= 1 + (1 + (1 + count of []))
= 1 + (1 + (1 + 0)))
= 1 + (1 + (1))
= 1 + (2)
= 3
numberOfItems(set)
if set is empty
return 0
else
remove 1 item from set
return 1 + numberOfItems(set)
struct Node {
Node* next;
};
int length(const Node* list) {
if (!list->next) {
return 1;
} else {
return 1 + length(list->next);
}
}
public int Factorial(int n)
{
if (n <= 1)
return 1;
return n * Factorial(n - 1);
}
public int fact(int n)
{
if (n==0) return 1;
else return n*fact(n-1)
}
start
Is the table empty?
yes: Count the tally marks and cheer like it's your birthday!
no: Take 1 apple and put it aside
Write down a tally mark
goto start
factorial(6) = 6*5*4*3*2*1
6 * factorial(5) = 6*(5*4*3*2*1).
factorial(n) = n*factorial(n-1)
factorial(6) = 6*factorial(5)
= 6*5*factorial(4)
= 6*5*4*factorial(3) = 6*5*4*3*factorial(2) = 6*5*4*3*2*factorial(1) = 6*5*4*3*2*1
int FloorByTen(int num)
{
if (num % 10 == 0)
return num;
else
return FloorByTen(num-1);
}
private void BuildVertices(double x, double y, double len)
{
if (len > 0.002)
{
mesh.Positions.Add(new Point3D(x, y + len, -len));
mesh.Positions.Add(new Point3D(x - len, y - len, -len));
mesh.Positions.Add(new Point3D(x + len, y - len, -len));
len *= 0.5;
BuildVertices(x, y + len, len);
BuildVertices(x - len, y - len, len);
BuildVertices(x + len, y - len, len);
}
}
tree = null
| leaf(value:integer)
| node(left: tree, right:tree)
function computeSomething(x : tree) =
if x is null: base case
if x is leaf: do something with x.value
if x is node: do something with x.left,
do something with x.right,
combine the results
integer = 0 | succ(integer)
function computeSomething(x : integer) =
if x is 0 : base case
if x is succ(prev) : do something with prev
sum l =
if empty(l)
return 0
else
return head(l) + sum(tail(l))
max l =
if empty(l)
error
elsif length(l) = 1
return head(l)
else
tailmax = max(tail(l))
if head(l) > tailmax
return head(l)
else
return tailmax
a * b =
if b = 0
return 0
else
return a + (a * (b - 1))
sort(l) =
if empty(l) or length(l) = 1
return l
else
(left,right) = split l
return merge(sort(left), sort(right))
A child couldn't sleep, so her mother told her a story about a little frog,
who couldn't sleep, so the frog's mother told her a story about a little bear,
who couldn't sleep, so the bear's mother told her a story about a little weasel...
who fell asleep.
...and the little bear fell asleep;
...and the little frog fell asleep;
...and the child fell asleep.
void f() {
... f() ...
}
void f() {
... g() ...
}
void g() {
... f() ...
}
Q: Does using recursion usually make your code faster?
A: No.
Q: Does using recursion usually use less memory?
A: No.
Q: Then why use recursion?
A: It sometimes makes your code much simpler!