您的位置:首页 > 其它

深入理解递归

2018-02-11 21:40 162 查看
你怀疑过递归代码的正确性吗——看起来它似乎什么都没干,怎么能给出问题的解呢?为了打消这种疑虑,你也许会尝试着在草稿纸上一步步推演递归函数的执行过程,但当递归深度很深或者代码逻辑较复杂时,这样的尝试往往是徒劳的。“递归”的概念并不难懂(如果你连基本概念都不知道,可以访问这里),但实际应用却无法写出代码,相信这是困扰过很多人的问题。
1、我们是怎么入门的?
当不能很好地理解一个概念时,有必要回溯到第一次接触这个概念的场景:是因为当时就没听进去,还是学习的材料有问题,还是其他什么原因。几乎每个老师讲递归时都会举两个有名的例子,一个是计算Fibonacci数列,另一个是Hanoi塔问题,我们来简单复习一下:
(1)计算Fibonacci 数列
问题描述:Fibonacci数列的定义是:F0=0, F1=1, Fn=Fn-1+Fn-2
下面的代码递归地计算了Fibonacci数列的第n项:int fib(int n){
if(n==0){
return 0;
}
else if(n==1){
return 1;
}
else{
return fib(n-1)+fib(n-2);
}
}(2)Hanoi塔问题
问题描述:Hanoi塔问题最早是由法国数学家Edouard Lucas提出的,故事的背景是古印度的一座寺庙,庙里有A,B,C三根柱子,A柱插有若干碟子,从高处往低处碟子的大小递增,如下图所示:



(图片来源:维基百科,链接:点击打开链接
庙里的僧侣遵循着一种古老的传统:每天必须把插在A柱的碟子都插到C柱,并且要保证仍然是从高处往低处碟子的大小递增且搬动过程中不允许将大碟子放到小碟子上,问如果有n个碟子,僧侣们最少要搬动几次?
例如,如果碟子只有1个,那么只需搬动1次,即直接从A搬到C;如果碟子有2个,需要先把小碟子搬到中转站B,然后把大碟子搬到C,最后把B上的小碟子搬到C,共有3次搬动;如果是3个呢?需要先把最小的碟子搬到C,然后把第二大的碟子搬到B,然后把最小的碟子从C搬到B,然后把最大的碟子搬到C,然后把最小的碟子搬到A,然后把第二大的碟子搬到C,最后把最小的碟子搬到C,一共7次搬动。
如果像这样考虑问题,什么时候是个头?我们不妨换一种思维方式:设搬动n个碟子需要的次数是H(n),由于每次只需要关注最大的那个碟子,我们可以假设最大碟子上的(n-1)个碟子已经被移走了(这将花费H(n-1)次),那么我们就可以直接把A上最大的碟子移到C了(这只花费1次),然后呢?然后把剩下的(n-1)个碟子移到C上,不管中间做多么复杂的中转,它的花费仍是H(n-1)次,也就是说:H(n)=2H(n-1)+1,由此问题得解:int H(int n){
if(n==1){
return 1;
}
else{
return 2*H(n-1)+1;
}
}你也许会感到“不舒服”:我们假设最大碟子上的(n-1)个碟子已经被移走了,这样思考是不是太粗糙?其实一点也不,这里用的是数学归纳法的思想:为了求解H(n),假设H(n-1)已经得到了解,然后只要补上初始条件,即H(1)=1,整个问题就得到了解!
2、其他生动的例子
学东西讲究由浅入深、再由深入浅,其实你可以自己想一些更简单的例子出来帮助自己理解,我们这里再来看一个吧。
假如有个小朋友坐在第10排,他的作业本却被小组长扔在了第1排,第10排的小朋友要拿回自己的作业本,可以怎么办?他可以排排第9排小朋友的背,对他说:“帮我拿一下第1排的作业本。”,而第9排小朋友可以排排第8排小朋友的背,对他说:“帮我拿一下第1排的作业本。”,如此进行下去,消息终于传到了第1排小朋友的耳朵里,然后他把作业本传到了第2排,第2排又传给第3排......终于,第10排的小朋友拿到了自己的作业本,这个过程就是递归,“拍拍小朋友的背”可以类比函数调用,而小朋友们都记得自己要拿作业本的使命,是因为他们有记忆力,有脑子,可以类比“栈”。
类比是一种不严谨的思维方式,但是用来理解问题很有用,看到这里,你应该对这个概念比较了解了,准备好了吗?下面我们从另一个角度来认识一下递归。
3、数学归纳法给我们的启示
其实上面在讲Hanoi塔问题的时候已经提到了数学归纳法。常用的数学归纳法有两种:
(1)第一数学归纳法
要证明关于正整数n的命题P(n)成立,可以先证明P(1)成立,再假设P(n-1)成立,在此假设下证明P(n)成立,若得证,则问题解决。
(2)第二数学归纳法(也叫强归纳法)
要证明关于正整数n的命题P(n)成立,可以先证明P(1)成立,再假设P(1)P(2)P(3)...P(k)均成立(即对于n<=k, k∈N+都成立),在此假设下证明P(k+1)成立,若得证,则问题解决。

考虑这样一个问题:
要对一个数列{an}求和,怎么办?当然可以直接运用定义:Sn=a1+a2+...+an,而这个式子就等价于Sn=Sn-1+a1,后者是一个递归式,并且可以看作是由数学归纳法来定义的。这两个式子折射出的思维方式是不同的:习惯第一个的人很可能是那些企图去一项一项推演递归的人,而习惯后者的人则是巧用递归的人,你也许会不屑地说:“第二个式子的具体计算也要依赖第一个式子啊。”没错,但是“具体计算”这个工作是计算机帮我们干的,我们完全不用关心每步怎么算、算了几步等具体细节。
数学归纳法的思想蕴含在所谓的“良序性公理”中,这里不展开了。
4、重建你的“世界观”
说了一些知识性的东西,中场喂点鸡汤 :-)
(1)信任自己的代码
递归的跳跃性很强,以至于有时候我们觉得递归代码似乎什么都没干(但是记住这只是你的错觉,因为递归的正确性是有归纳法保证的!上面举的例子正是想说明这个问题)。所以...信任你的代码吧,你一定要觉得“嗯...它一定会给我正确的解。” :-)
(2)学会放弃
放弃掉对于细节的执着,不要去想每一步在干什么,只要关心数学归纳法中的前两步(基本情况和归纳假设)。
5、一些练习
接下来要做的就是多练习了,如果不是天才,不做一定的练习就想掌握一个不好掌握的东西肯定是不可能的,至于需要做多少练习,因人而已吧...这里的几个例子仅供参考:
(1)本题取自LeetCode 206 Reverse Linked List
反转链表可以递归来解:classSolution {
public:
ListNode* reverseList(ListNode* head) {
if(head==NULL||head->next==NULL) return head;
ListNode* node=reverseList(head->next);
head->next->next=head;
head->next=NULL;
return node;
}
}; (2)本题取自LeetCode 404 Sum of Left LeavesFind the sum of all left leaves in a given binary tree.Example: 3
/ \
9 20
/ \
15 7

There are two left leaves in the binary tree, with values 9 and 15 respectively. Return 24
/**
* Definition for a binary tree node.
* struct TreeNode {
*     int val;
*     TreeNode *left;
*     TreeNode *right;
*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
先找基本情形:当root为NULL,返回0;当root仅有一个左孩子,返回这个左孩子的值加上求解右子树的结果。对其他情况,递归地求左右子树并返回它们的和:class Solution {
public:
int sumOfLeftLeaves(TreeNode* root) {
if (root==NULL)
return 0;
if (root->left!=NULL&&root->left->left==NULL&&root->left->right==NULL)
return root->left->val+sumOfLeftLeaves(root->right);
return sumOfLeftLeaves(root->left)+sumOfLeftLeaves(root->right);
}
};(也许这就是理论和实践的差距了...找边界条件还是比较伤脑筋的...)
注:涉及二叉树的递归问题很多,二叉树本身就是递归定义的。更多的问题不妨去LeetCode、ZOJ、POJ等OJ上找找。
(3)本题取自LeetCode 241 Different Ways to Add ParenthesesGiven a string of numbers and operators, return all possible results from computing all the different possible ways to group numbers and operators. The valid operators are 
+
-
 and 
*
.
Example 1Input: 
"2-1-1"
.((2-1)-1) = 0
(2-(1-1)) = 2Output: 
[0, 2]
Example 2Input: 
"2*3-4*5"
(2*(3-(4*5))) = -34
((2*3)-(4*5)) = -14
((2*(3-4))*5) = -10
(2*((3-4)*5)) = -10
(((2*3)-4)*5) = 10Output: 
[-34, -14, -10, -10, 10]
Credits:
Special thanks to @mithmatt for adding this problem and creating all test cases.class Solution {
public:
vector<int> diffWaysToCompute(string input) {
vector<int> res;
for(int i=0;i<input.size();i++){
char c=input[i];
if(c=='+'||c=='-'||c=='*'){
vector<int> vec1=diffWaysToCompute(input.substr(0,i));
vector<int> vec2=diffWaysToCompute(input.substr(i+1));
for(int j=0;j<vec1.size();j++){
for(int k=0;k<vec2.size();k++){
res.push_back(c=='+'?vec1[j]+vec2[k]:c=='-'?vec1[j]-vec2[k]:vec1[j]*vec2[k]);
}
}
}
}
return (res.size()!=0)?res:vector<int>{stoi(input)};
}
};注:分治法是一种重要的算法设计思想,几乎每个用分治法的程序都会用递归,更多关于分治法的信息可以参考:点击打开链接
6、站得更高点
事实上,递归在计算理论中有很重要的地位...(突然觉得自己挖了一个很大的坑,先开个头以后写吧...
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: