我喜欢在TopCoder网站上解决算法问题。我可以实现大多数基本的递归问题,例如回溯,dfs ......但是,每当我遇到复杂的递归时,通常需要花费数小时和数小时。当我检查其他程序员的解决方案时,我对自己感到非常羞耻。我已经编程了将近5年。我可以看到其他编程技术的重大改进,例如操纵字符串,图形,GUI ......但不是递归?任何人都可以分享一些如何处理递归问题的经验吗?谢谢!
更新
我熟悉单元测试方法。甚至在我知道单元测试之前,我经常会编写一些小的测试函数来查看结果是否符合我的要求。面对递归问题时,我自然会失去这种能力。我可以插入几个“cout”语句来查看当前结果,但是当调用深度嵌套时,我不再能够跟踪它。所以大部分时间,我要先用铅笔和纸来解决它,要么我已经完成了(不能使用常规方法,比如将它分成小块)。我觉得递归必须作为一个整体工作。
最好的问候,
陈
答案 0 :(得分:8)
我发现铅笔和纸张非常方便。将问题分成更小的块也是一个好主意,例如使用非常小的数据集。您应该做的第一件事是确定您的基本条件,即标记递归调用结束的条件。从那里你可以处理递归问题的主体,并使用更大的数据集来测试/验证它。
我还想补充一点,速度并不是成为优秀工程师的唯一资格。工程师可以拥有许多其他技能,包括能够在框外观察和思考,说服其他人了解特定行动方案,打破问题并向外行人员(利益相关者和客户)解释这些技能等等。更多。
答案 1 :(得分:6)
这是一个非常好的问题。
我得到的最佳答案是分解:分而治之。这在C ++中有点棘手,因为它不能很好地支持更高阶的函数,但你可以做到。最常见的例程是地图和折叠等。 [C ++已经有一个名为std :: accumulate的cofold]。
您必须仔细考虑的另一件事是如何构造代码以尽可能提供尾递归。很快就会认识到尾部调用并将它们视为循环,这可以减少大脑过载在各处递归。
另一项优秀技术称为 trust 。这意味着,你写了一个你可能还没有定义的函数的调用,并且你信任它会做你想做的事情而不用多说。例如,您相信它将正确访问树的节点,即使它必须调用您当前正在编写的函数。写下评论说明前后条件是什么。
另一种方法(我很抱歉)首先使用像Ocaml或Haskell这样的真正的编程语言,然后尝试将漂亮的干净代码翻译成C ++。通过这种方式,您可以更轻松地查看结构,而不会陷入家务细节,难以理解的语法,缺乏本地化以及其他内容。一旦你做对了,你可以机械地将它翻译成C ++。 (或者您可以使用Felix为您翻译)
我说对不起的原因是..如果你这样做,你就不会再想写C ++了,这将使得很难找到一份令人满意的工作。例如,在Ocaml中,只需添加列表元素(不使用折叠):
let rec addup (ls: int list) : int = match ls with
| [] -> 0 (* empty list *)
| h::t -> h + addup t (* add head to addup of tail: TRUST addup to work *)
这不是尾递归,但这是:
let addup (ls: int list) : int =
let rec helper ls sum = match ls with
| [] -> sum
| h :: t -> helper t (h+ sum)
in
helper ls 0
上述转变众所周知。当你理解它正在做什么时,第二个例程实际上更简单。我太懒了把它翻译成C ++,也许你可以对它进行转码..(算法的结构本身应该足以弄清楚语法)
答案 2 :(得分:2)
问题的哪些部分会花费数小时和数小时?
其他编码员的解决方案你自己没有想到了什么?
作为一般建议,请记住考虑基本情况,然后记住您认为必须在递归的每个级别保持的不变量。错误经常出现,因为在递归调用中没有正确保留不变量。
答案 3 :(得分:2)
我曾经去过夏令营,为喜欢编程的疯狂青少年做准备。他们教我们解决问题的“法语方法”(内部参考)(递归和其他)。
1)在您的所有者词中定义您的问题,并做一些有用的例子。
2)进行观察,考虑边缘情况,约束(例如:“算法必须处于最差O(n log n)”)
3)决定如何解决问题:图论,动态规划(复述),组合学。
从此处开始特定的递归:
4)确定“子问题”,通常可以帮助猜测约束中可能存在多少子问题,并使用它来猜测。最终,一个子问题会在你脑中“点击”。
5)选择自下而上或自上而下的算法。
6)代码!
在这些步骤中,所有内容都应该在纸上用漂亮的笔直到第6步。在编程比赛中,那些立即开始使用的人通常会有低于标准的表现。
走路总能帮我算出算法,也许它对你有帮助!
答案 4 :(得分:2)
获取 The Little Schemer 的副本并完成练习。
请不要使用Scheme而不是C ++或C#或任何您喜欢的语言来推迟本书。 Douglas Crockford说(早期版本称为 The Little LISPer ):
1974年,Daniel P. Friedman出版 一本名为 The Little的小书 利斯佩尔。它只有68页,但它 做了一件了不起的事:它可以教 你要递归地思考。它使用了一些 假装LISP的方言(这是 当时用全部大写写的)。 方言并不完全符合 任何真正的LISP。但那没关系,因为 事实并非如此,关于LISP 关于递归函数。
答案 5 :(得分:0)
尝试在c ++ 0x :)中自动记忆。原帖:http://slackito.com/2011/03/17/automatic-memoization-in-cplusplus0x/
和我的递归函数的mod:
#include <iostream>
#include <functional>
#include <map>
#include <time.h>
//maahcros
#define TIME(__x) init=clock(); __x; final=clock()-init; std::cout << "time:"<<(double)final / ((double)CLOCKS_PER_SEC)<<std::endl;
#define TIME_INIT clock_t init, final;
void sleep(unsigned int mseconds) { clock_t goal = mseconds + clock(); while (goal > clock()); }
//the original memoize
template <typename ReturnType, typename... Args>
std::function<ReturnType (Args...)> memoize(std::function<ReturnType (Args...)> func)
{
std::map<std::tuple<Args...>, ReturnType> cache;
return ([=](Args... args) mutable {
std::tuple<Args...> t(args...);
if (cache.find(t) == cache.end()) {
cache[t] = func(args...);
}
return cache[t];
});
}
/// wrapped factorial
struct factorial_class {
/// the original factorial renamed into _factorial
int _factorial(int n) {
if (n==0) return 1;
else {
std::cout<<" calculating factorial("<<n<<"-1)*"<<n<<std::endl;
sleep(100);
return factorial(n-1)*n;
}
}
/// the trick function :)
int factorial(int n) {
if (memo) return (*memo)(n);
return _factorial(n);
}
/// we're not a function, but a function object
int operator()(int n) {
return _factorial(n);
}
/// the trick holder
std::function<int(int)>* memo;
factorial_class() { memo=0; }
};
int main()
{
TIME_INIT
auto fact=factorial_class(); //virgin wrapper
auto factorial = memoize( (std::function<int(int)>(fact) ) ); //memoize with the virgin wrapper copy
fact.memo=&factorial; //spoilt wrapper
factorial = memoize( (std::function<int(int)>(fact) ) ); //a new memoize with the spoilt wrapper copy
TIME ( std::cout<<"factorial(3)="<<factorial(3)<<std::endl; ) // 3 calculations
TIME ( std::cout<<"factorial(4)="<<factorial(4)<<std::endl; ) // 1 calculation
TIME ( std::cout<<"factorial(6)="<<factorial(6)<<std::endl; ) // 2 calculations
TIME ( std::cout<<"factorial(5)="<<factorial(5)<<std::endl; ) // 0 calculations
TIME ( std::cout<<"factorial(12)="<<factorial(12)<<std::endl; )
TIME ( std::cout<<"factorial(8)="<<factorial(8)<<std::endl; )
return 0;
}
答案 6 :(得分:0)
识别Base case
:这是识别何时停止递归的情况。
Ex: if (n == null) { return 0; }
通过将问题分解为尽可能小的情况来识别sub-problem
。
然后我们可以通过编码
以两种方式解决它在head recursive
方法中,发生递归调用和处理。在处理第一个节点之前,我们处理列表的“其余部分”。这允许我们避免在递归调用中传递额外的数据。
在tail recursive
方法中,处理发生在递归调用
答案 7 :(得分:-3)
动态编程有帮助。记忆也很有帮助。
答案 8 :(得分:-3)
我认为最好避免递归。在大多数情况下,循环更优雅,更容易理解。循环也更有效,长循环不会崩溃程序堆栈溢出错误。
我发现递归是最优雅的解决方案的问题很少。通常这些问题涉及图形或表面导航。幸运的是,这个领域被研究死亡,所以你可以在网上找到大量的算法。
在包含不同类型节点的一些更简单的图形(如树)中导航时,访问者模式通常比递归更简单。