前言
在数据结构和算法的学习中,树的章节往往掺杂着大量递归分治编程技巧。很多人面对递归心里就有劝退的想法。只有对递归有更加清晰的理解才能不那么快从入门到入土。根据百度百科上的解释:程序调用自身的编程技巧称为递归(recursion)。对于许多初学者来说(特别是对于我这种非科班的小白)掌握它可能是一件非常棘手的事情。在练习递归的初期,绞尽脑汁地将递归的过程展开是我难以忘怀的痛苦经历,很多时候把自己绕的想直接退学。后来在查阅了各种的帖子学习了各种解题思路后,发现自己对于递归有一种醍醐灌顶的感觉。希望通过以下我对于递归的总结,可以 帮助更多的人掌握这一门编程小技巧。本人水平有限,希望各位大佬指正。
递归的要素
开门见山,上干货。
一.明白你设计的递归函数的作用并且很自信地认为它函数功能会实现!!!
可能我这样说你可能不知道意味着什么,下意识肯定认为这不是废话嘛,但这一点是递归函数编写成功的重中之重。
以下在递归问题的情景下进行分析,展开递归的思想
例1.汉诺塔问题
有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
(1) 每次只能移动一个盘子;
(2) 盘子只能从柱子顶端滑出移到下一根柱子;
(3) 盘子只能叠在比它大的盘子上。
请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。
输入:A = [2, 1, 0], B = [], C = []
输出:C = [2, 1, 0]
首先,我们要根据题意作图如下
很明显,由题可知用栈将所有盘子从第一根柱子移到最后一根柱子。 即将A中的盘子移到C中的行为,不需要返回值。因此构造递归函数为void类型的函数,并根据题意中的三个柱子进行函数结构体中参数的确认,如下
void move (vector<int>&A,vector<int>&B,vector<int>&C)
二.设置递归函数终止条件
1.函数可正常进行运行时的终止条件(往往是取极端值)
ps:注意函数表达式往往展现了函数的终止条件(比如在斐波那契数列中 f(n)=f(n-1)+f(n-2),因此限制条件 与n-1和n-2的情况有差异)
2.函数不可正常运行时的起始条件(通常为空子树 ,空指针等类似的情况)
如果没有及时设置递归条件,递归会陷入死循环。那如何有效的设置?其实我们想一下,递归的终止条件不过是对一个对象的状态做出判断。
例如
A==NULL || A->head!= NULL
这就是对A指针的判断,紧接着引出一个很重要的问题,对一个对象进行判断肯定要确定对象是谁吧?因此设立终止条件的前提是对递归函数进行递的过程进行分析。在汉诺塔问题中,A的初始化为N个圆盘,从高到低,使圆盘的大小从小到大进行排列。在考虑终止条件时,以圆盘为对象。
根据规则1,在函数正常运行的情况下因此直接找寻极端情况即N==1时
if(N==1){
C.push_back(A.back());
A.pop_back();
return ;
}
(Tip 1:完成此步后,再对于N=2的情景进行思考,基本上已经在脑海中对于递归函数的实现过程有了大致的思路图像,为后面的规则三(递归函数功能的实现)打下良好的基础)
Tip 2 :在终止条件中切勿使用 递归函数的调用,一定会使函数进入死循环!!!
比如
根据规则2,在函数不能正常进行的情况下的起始条件
(有少部分情况题目中说明不用考虑)
else if(N==0){
return ;
}
三.实现递归函数的功能
1.根据规则一所示 ,我们构造一个递归函数并且认为它可以成功完成函数的功能。但编程毕竟不是魔法,需要我们自己完成这个功能。 (Tip 3 :在实现递归函数的功能时依然可以通过调用递归函数的功能来实现递归函数功能的实现,这就是对递归函数本质的理解)
相信很多小伙伴会听起来很绕,举个例子应该会很容易理解。
比如在本问题中,函数的功能是将A柱的所有盘子转移到C柱中,在实现函数的功能中可以认为是分三步
1.将A柱中除了最后一个圆盘的其余(N-1)个圆盘移到B柱
2.将A柱中的最后1个圆盘移到C柱中
3.将B柱子中(n-1)个圆盘移到C柱中
图片表示:
代码表示为
move(N-1,A,C,B);
move(1,A,B,C);
move(N-1,B,A,C);
灵活一点还可对上述代码进行修改
1.将A柱中第1个圆盘移到B柱
2.将A柱中的最(n-1)个圆盘移到C柱中
3.将B柱子中1个圆盘移到C柱中
代码表示为
move(1,A,C,B);
move(N-1,A,B,C);
move(1,B,A,C);
2.在实现函数功能的过程中一般需要对两个不同的对象进行处理。根据在Tip 1 中对于N=2的情况的考虑,建立A中最后一个盘子和A中N-1个盘子的关系,将N-1个盘子看成一个整体,重复 N=2 时的过程
由1.2知
改变原有递归函数的参数,提出N-1与1之间的关系。
为了方便理解,再次提出函数的功能为,将x(常数)个 盘子从A柱移到C柱。
move(N-1,A,C,B);
move(1,A,B,C);
move(N-1,B,A,C);
由于函数的类型时void 因此返回退出即可
return ;
汉诺塔完整的递归函数如下
class Solution {
public:
//函数功能:将A上的圆盘移到 C的柱子上
void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
int n = A.size();
//将A上n个的圆盘移到C的柱子上
move(n, A, B, C);
}
void move (int n,vector<int>& A, vector<int>& B, vector<int>& C) {
/*终止条件:1.归的过程,不可再进行调用递归的语句
2.初始空的条件和最后终结的条件
3.完成后必须形成对于元素的构脑图
*/
if(n==1){
C.push_back(A.back());
A.pop_back();
return ;
}
// 实现递归函数的功能(可使用递归函数的功能协助构建)
//建立第一个递归过程和后面n-1个递归过程的关系
move(n-1,A,C,B);
move(1,A,B,C);
move(n-1,B,A,C);
return ;
}
};
由上述例子,我们可以对递归的过程有大致的了解。下面通过一系列实际的例子我们对递归的大致过程,进行进一步的巩固。
例2.两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。(ps:不能仅仅交换两个节点中的数字)
输入输出实例1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
输入输出实例2:
输入:head = []
输出:[]
C++解法
class Solution {
public:
//函数的功能:交换 函数参数中的节点以及其后面的一个节点
ListNode* swapPairs(ListNode* head) {
/*终止条件的确立
1.空链表
2.非空链表
|
如果非空链表的情况不好构建出来
小技巧:以另一种方式分情况
1.节点数为偶数的情况(0个节点为其特殊情况)
2.节点数为奇数的情况
*/
if(head==NULL || head->next == NULL){
return head;
}
//实现递归函数的功能
ListNode* Newhead = head->next;
/*建立第一个递归过程和后面n-1个递归过程的关系
易错:初学者容易将swapPairs(Newhead->next)
写成Newhead->next
*/
head->next = swapPairs(Newhead->next);
Newhead->next = head;
return Newhead;
}
};
例3.杨辉三角
给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
输入: 5
输出:
[
[1],
[1,1],
[1,2,1],
[1,3,3,1],
[1,4,6,4,1]
]
C++解法
class Solution {
public:
vector<vector<int>> generate(int numRows) {
/*由题意:在杨辉三角中,每个数是它左上方和右上方的数的和
发现每一层的vector的构成需要上面一层vector内的元素
因此需要引出第一个vector[1];
*/
vector<int>A={1};
vector<vector<int>> B = move(numRows,A);
vector<vector<int>> E;
int remark = B.size();
for(int it=0;it<remark;it++){
E.push_back(B.back());
B.pop_back();
}
return E;
}
/*函数的功能:利用杨辉三角中的每一层vector元素生成其下一层
的vector元素
*/
vector<vector<int>> move (int numRows, vector<int>A){
//设立终止条件(很简单)
if(numRows==0){
vector<vector<int>>D;
return D;
}
//实现函数的功能
vector<int>C;
C.push_back(1);
for(auto i = A.begin();(i+1)!= A.end() ; i++){
C.push_back((*i)+(*(i+1)));
}
C.push_back(1);
//建立第一个递归过程和后面n-1递归过程的关系
vector<vector<int>>B = move(numRows-1,C);
B.push_back(A);
return B;
}
};
其他比较重要的细节部分
1.递归函数中几乎不存在使用引用的情况->在像进行计数运算时一般通过
(1).终止条件处编写类似return 1类似的处理
(2).在函数功能实现部分进行+1类似的处理
进行计数运算
总结
以上就是我对于递归的一些自己的浅显的思考和理解,希望本文可以帮助处于递归困难中无法自拔的你,后续会对于树中的递归以及递归中的优化方法进行解析,让我们共同进步!!!