英文:
Best way to think about implementing recursive methods?
问题
所以我在想,你们中是否有人可以给我关于这个的一些建议。我已经在做一些挑战,比如(经典的)编写一个方法,使用单个递归调用计算斐波那契数列的第n个数字(即避免 return fibo(n-1) + fibo(n-2);
)。
我真的为这个问题纠结了很久,最终看了一个使用辅助方法的解决方案 -
public static int fibonacci(int n) {
if (n < 2) {
return n;
}
return fibonacci_helper(n, 1, 0);
}
public static int fibonacci_helper(int n, int previous, int current) {
if (n < 1) {
return current;
}
return fibonacci_helper(n - 1, current, previous + current);
}
我不太确定解决这种问题的方法是什么,如何快速解决(而不是先迭代地解决它,然后将其转换为尾递归,这需要很多时间)。
非常感谢一些提示,提前致谢。
英文:
so I was wondering if any of you can give me tips regarding this. I've been doing some challenges like (the classical) making a method to calculate the nth number of a Fibonacci sequence using a single recursive call (aka. avoid return fibo(n-1) + fibo(n-2);
).
I really scratched my head on that one and ended up looking at the solution that made use of a helper method -
public static int fibonacci(int n) {
if (n < 2) {
return n;
}
return fibonacci_helper(n, 1, 0);
}
public static int fibonacci_helper(int n, int previous, int current) {
if (n < 1) {
return current;
}
return fibonacci_helper(n - 1, current, previous + current);
}
I'm not really sure what approach one takes to solve questions like that quickly (without first solving it iteratively and translating that to a tail recursion, which takes a lot of time).
Would really appreciate some tips, thanks in advance.
答案1
得分: 2
你需要首先确定问题是否需要递归解决方案。通常情况下,当现有解取决于先前(已计算过的)解时,就需要递归。
首先,检查小输入(称为角落/基本案例)。然后在小输入上进行手动构建(通过干扰运行)。完成这些步骤后,你通常可以找出递归关系(就像斐波那契数列中的情况)。测试其有效性,然后使用基本案例和当前递归关系编写递归。
例如,给定的代码在二叉树中查找具有特定值的节点(如果你不知道什么是二叉树,可以查看这里:https://en.wikipedia.org/wiki/Binary_tree)
bool search(Node root, int val) {
if (root == null) // 基本案例 1
return false;
if (root.value == val) // 基本案例 2
return true;
return (search(root.left, val) || search(root.right, val)); // 对左右子树进行递归以寻找该值
}
英文:
You need to first decide if the question needs a recursive solution.Typically a recursion is needed when a present solution is dependent on some previous (already calculated) solution.
To start with , first check on small inputs(call them corner/base cases) . Then build on it (manually by dry running) on small inputs.Once you have done this, you can , in most cases , figure out the recurrence relation(like here in fibonacci).Test its validity , and then using base cases and current recurrence relation , write a recursion.
For example , the given code searches for a node with particular value in a binary tree(check out if you don't know what binary tree is: https://en.wikipedia.org/wiki/Binary_tree)
bool search(Node root,int val){
if(root==null)//base case 1
return false;
if(root.value==val)//base case 2
return true;
return(search(root.left,val)||search(root.right,val));//recursing left and right subtrees for looking out for the value
}
答案2
得分: 0
你思考的领域被称为动态规划。它的工作方式是,你试图解决的较大问题的解由较小问题的解构成,如果你保留这些解并重复使用它们,而不是多次计算它们,那么时间复杂度可以大大降低。通常的方法是考虑如何分解问题,以及为了解决问题而需要记住哪些较小问题的解。在这种情况下,你可以通过将所有结果保存在数组中以线性时间和线性空间来完成,如果你在寻找动态规划解决方案,这应该相当容易想到。当然,这可以简化,因为你不需要保留所有这些数字,但那是一个单独的问题。
通常,动态规划解决方案将是迭代的,而不是递归的,因为你需要保留大量的解来计算下一个较大的解。要将其改为使用递归,你只需弄清楚需要传递哪些解,并将其包含为参数即可。
英文:
The area you're thinking of is called Dynamic Programming. The way it works is that the solution to the larger problem you're trying to solve is composed of solutions to smaller problems, and the time complexity can be reduced dramatically if you keep those solutions and reuse them, instead of calculating them multiple times. The general approach to take is to consider how the problem can be broken down, and which solutions to the smaller problems you'll need to remember in order to solve it. In this case, you could do it in linear time and linear space by keeping all the results in an array, which should be pretty easy to think of if you're looking for a DP solution. Of course that can be simplified because you don't need to keep all those numbers, but that's a separate problem.
Typically, DP solutions will be iterative rather than recursive, because you need to keep a large number of solutions available to calculate the next larger one. To change it to use recursion, you just need to figure out which solutions you need to pass on, and include those as the parameters.
答案3
得分: 0
在纸上尝试一下,并试着发现那些不必要地重新进行的隐藏计算。然后尽量避免它们。
以下是Haskell代码部分:
f(n) = f(n-1) + f(n-2);
很明显,f(n-1) = f(n-2) + f(n-3)
不必要地重新计算了 f(n-2)
,以此类推。如果你能够同时处理两个值呢?
让 f2(n)
返回两个值,分别为 n
和 (n-1)
;然后你可以在伪代码中这样做:
f(n) = let { (a,b) := f2(n-1) } in (a+b)
现在你有两个函数,但都还没有被定义,这有什么好处呢?也把这个 f
转变为 f2
,使其返回两个值,而不是一个,正如我们所期望的那样:
f2(n) = let { (a,b) := f2(n-1) } in (a+b,a)
然后,一个递归定义中 a
被重复利用。
现在只需要添加一些边界/基础情况,并检查是否存在偏移量为1的错误。
或者,甚至更好的是,倒转时间箭头,从基础情况开始,就能够免费获得迭代版本。
递归是一个工具,它在这里是用来帮助我们的,以使问题解决变得更加容易。
英文:
Play with it on paper, and try discover hidden computations that are redone needlessly. Then try to avoid them.
<!-- language-all: haskell -->
Here you have f(n) = f(n-1) + f(n-2)
; obviously f(n-1) = f(n-2) + f(n-3)
redoes f(n-2)
needlessly, etc. etc. etc.. What if you could do the two at once?
Have f2(n)
return two values, for n
and for (n-1)
; then you do (in pseudocode)
f(n) = let { (a,b) := f2(n-1) } in (a+b)
Now you have two functions, none is yet defined, what good does it do? Turn this f
into f2
as well, so it returns two values, not one, just as we expect it to do:
f2(n) = let { (a,b) := f2(n-1) } in (a+b,a)
And voila, a recursive definition where a
is reused.
All that's left is to add some corner/edge/base case(s), and check for the off-by-1 errors.
Or even better, reverse the time arrow, start from the base case, and get your iterative version for free.
Recursion is a tool which is there to help us, to make problem solving easier.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论