一文带你彻底搞懂递归

一文带你彻底搞懂递归

前言:鄙人知道的并不多,但致力于把所知道的讲清楚。在学习的道路上最怕遇到忽悠文,还有一本正经扯着专业术语的深奥文,所以鄙人的愿景来了:希望你看完这篇文章后,不会觉得这是两类中的其一,并且会由衷的感慨这样的述说风格真是一种难能可贵的优秀品质!

                            “滴,前方 递归 收费站高速入口,请系好安全带 ”

目录:

1、递归基本概念

2、爬楼梯例题

3、全排列例题

4、N皇后例题

5、反转链表例题

一、递归基本概念

递归在我们生活中一直都有使用,比如一位教官想要知道自己的队伍中有多少人,那么他只需要问清队伍中最后一位同学,是第几个人就行,但是最后一位同学也不知道自己是第几个人怎么办,这个人可以问他旁边的人,在旁边的人数上加一就行,旁边人也不知道可以再问旁边人,一路 递 过去,第一个人总知道自己是第几个了吧,再一路的 归 回来,也就是有名的报数现象。

这就是一个非常标准的递归求解问题的分解过程,去的过程叫“递”,回来的过程叫“归”。基本上,所有的递归问题都可以用递推公式来表示。刚刚这个生活中的例子,我们用递推公式将它表示出来就是这样的: f(n) = f(n-1) + 1,其中 f(1) = 1。

递归需要满足的三个条件:

  1. 一个问题的解可以分解为几个子问题的解何为子问题?子问题就是数据规模更小的问题。

比如,前面讲的报数的例子,你要知道,“自己是第几个人”的问题,可以分解为“旁边的人是第几个人”这样一个子问题。

  1. 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样。

比如,报数那个例子,你求解“自己是第几个人”的思路,和旁边的人求解“自己是第几个人”的思路,是一模一样的。

  1. 存在递归终止条件把问题分解为子问题,把子问题再分解为子子问题,一层一层分解下去,不能是无限循环。

还是报数的例子,第一个人不需要再继续询问任何人,就知道自己是第一个人,也就是 f(1)=1,这就是递归的终止条件。

如何编写递归代码:

 1.定义递归函数。 

2.找到终止条件。

写递归代码的几个注意事项:

1.不要试图用人脑去分解递归过程中的每个步骤。

2.递归代码要警惕堆栈溢出。

函数调用会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时,才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。

二、爬楼梯例题(力扣网上第70题)

1.定义递归函数:

假如我们要求爬到 第6阶 有多少种不同方法,那么我们知道 第6阶 可能从 第4阶 一次爬 2 个台阶上去的,也可能是从 第5阶 一次爬 1 个台阶上去的,那么我只需要知道爬到 第4阶 的方法 和 爬到 第5阶 的方法,两者相加即为爬到第6阶 的方法。

那么爬到 第4阶 的方法和 第5阶 的方法 为多少呢?我们不知道,所以我们定义一个函数 int climbStairs(int n);

返回一个 爬到 第n阶 有多少种不同的方法值。例如求爬到 第4阶 就是 climbStairs(4);同理爬到 第5阶。

2.找到终止条件:

我们可以很显然知爬到 第1阶 为一种方法,爬到 第2阶 为两种方法(一阶一阶爬,一次爬两阶)。所以终止条件就有了。

3.代码:

明确以上两点,我们可以直接写出代码了(当思路有了,可以尝试先自己写出代码实现),如下:

int climbStairs(int n) {
      //终止条件 
     if(n == 1 )  return 1; 
     if(n == 2)  return 2; //返回两种情况的和
     reutrn climbStairs(n-1) + climbStairs(n-2);

`
4.注意事项:

1.不要试图用人脑去分解递归过程中的每个步骤。

再次强调不需要将递归过程中的每个步骤在脑袋里展开,你的脑袋能压几个栈呀?我们只需要扣住函数的定义,解决好当前层的 问题就行

2.警惕堆栈溢出(根据题目条件,选择性使用递归)。

3.尝试消除递归过程中的重叠子问题。

如图,我们求解爬到 第6阶 的方法数时,需求爬到 第4阶 和爬到 第5阶 ,求爬到 第5阶 时,也需求 爬到 第4阶, 这就进行了重复的计算,那么我们可以采用 散列表 记录第一次计算爬到 第4阶 的方法值,后续计算时,可以直接查找使用。

不赘言,直接上代码(这里采用 c++ 中的 unordered_map 实现):

unordered_map<int,int> hash; 
int climbStairs(int n) { 

    if(n==1) return 1; 
    if(n==2) return 2; 

    if(hash.count(n)) return hash[n]; 
    hash[n]=climbStairs(n-1)+climbStairs(n-2); 

    return hash[n]; 
}

三、全排列例题(力扣网上第46题)

1.定义递归函数:

先来看排列问题解决的思路,我们先从【1,2,3】 中选择【1】得【1】,再从【2,3】中选择【2】得【1,2】,最后选个【3】得【1,2,3】(用 track数组 记录得的结果)。如图:

最后一行即为排列结果。那么我们可以定义一个函数void backtrack(vector& nums,vector& track);

从 nums数组 中选择一个数,加入到 track数组 中。

2.找到终止条件:

可以很显然知道当 track数组 中的元素数量等于 nums数组 的元素 数量时,即 nums数组 中每一个数字都被选了时,即可结束。

3.代码:

有三点需要声明:

1.我们需要一个 res数组 记录最终结果。

2.我们需要一个 used数组 记录已经使用过的数字,来防止重复选择。

比如第一次我们从【1,2,3】中选择了【1】,第二次只能从【2,3】中选择,而不是再从【1,2,3】中选择。

3.撤销选择,即重新选。

比如第一次我们从【1,2,3】中选择了【1】,在第二次选择前,撤销对【1】的选择,于是我们从【1,2,3】中选择【2】。

有了思路后可以尝试先自己写代码实现!

vector<vector<int>> res; //从nums数组 中选择一个数加入到 track中。
void backtrack(vector<int>& nums, vector<int>& track) 
{ 
    //终止条件 
    if(track.size() == nums.size()) 
    { 
        res.push_back(track); 
        return; 
    } 
    for(int i=0;i<nums.size();i++) 
    { 
         
        if(uesd[i]) continue;//若used[i]为true,表示以下标为 i 的数字已经用过了。 
         
        track.push_back(nums[i]); 
        uesd[i] = true;
        //撤销选择 
        track.pop_back(); 
        uesd[i] = false;//注意 
    } 
}

这里有一个通过每次交换 两个元素 的技巧达到不用 used数组 记录已经使用过的数字的目的,有兴趣的可以参考力扣网上此题的官方题解,这里不再赘言。

四、N皇后例题

奇怪的游戏
佳哥在玩一种游戏,他需要在一个n*n的矩阵内填入n个数字,填入的数字很复杂,但幸运的是,你不需要知道需要填入什么数字。但是你要告诉佳哥应该在哪些格子中填数字。填数字只有一条规则,任何两个数字都不能处于同一行、同一列或同一斜线上,你能告诉佳哥有多少种填法吗?

比如对于样例输入4,输出2,代表两种符合要求的情况,分别是:

第一种:

0 1 0 0

0 0 0 1

1 0 0 0

0 0 1 0

第二种:

0 0 1 0

1 0 0 0

0 0 0 1

0 1 0 0

1.定义递归函数:

这题有不同种解法,这里以我的思路进行讲解。

对于这个棋盘我们一列一列的看,我们从第一列中选择任意一行填入数字,再从第二列中选择任意一行填入数字,这里要注意填入数字前要判断是否合法,即 满足两个数字都不能 处于同一行,同一列或同一斜线上。如图:

所以我们可以定义一个函数 void queen(vector<vector> board,int c); 在 board棋盘 的 c列 上选择任意一行填入数字。

2.找到终止条件:

我们可以很显然得知 当棋盘上每一列都填入了数字时,即可结束,并在 填法的总数 上加一,且记录下来。

3.代码:

有四点需要声明:

1.我们需要一个 res变量 记录 填法总数。

2.我们需要写一个 bool isValid(vector<vector> board,int r,int c) 函数,

判断在 board棋盘 的 r行,c列 上填入一个数字 是否合法。

3.撤销选择,即重新选择。

比如,第一次我们在 第一列 中选择 第一行,在第二次选择前,撤销对第一次的选择,于是我们在 第一列 中选择 第二行,

4.为了符合 OJ 平台的提交格式,这里的代码中 加入了 main函数,并初始化一个 n x n 的 board棋盘

以及导入头文件

有了思路后,尝试先自己写代码实现。

#include <bits/stdc++.h> 
using namespace std;
int res=0;//全局变量,记录 填法总数	
bool isValid(vector<vector<int>> board,int r,int c){
//判断在 board棋盘 的 r行,c列 上填入一个数字 是否合法。合法则返回 true; 
	int rt; 
	int ct; 
	for(ct=0;ct<(int)board[0].size();++ct){//判断同行。 
    	if(board[r][ct]) return false; 
	} 
	rt=r; 
	ct=c; 
	while (--rt>=0&&--ct>=0){//判断左上斜线。 
    	if(board[rt][ct]) return false; 
	} 
	rt=r; 
	ct=c; 
	while (++rt<(int)board.size() &&--ct>=0){//判断左下斜线。
 		if(board[rt][ct]) return false; 
	} 
return true; 
} 
void queen(vector<vector<int>> board,int c){
//终止条件 
 	if(c==(int)board[0].size()) ++res;//if 条件满足时,即棋盘上的每一列都填入了数字
//选择任意一行填入数字 
	for(int i=0;i<(int)board.size();++i){
    	if(isValid(board,i,c)){//判断是否合法
        	board[i][c]=1;//合法则填入 1;
        	queen(board,c+1);//对下一列进行选择         //撤销选择 
        	board[i][c]=0;//注意 
   	 	} 
	} 
} 

int main(){ 
 	int n; 
	cin>>n;
	vector<vector<int>> board(n,vector<int>(n,0));//初始化一个棋盘 
	queen(board,0);
	cout<<res; 
	return 0; 
}

对于这题有一个 二维 压缩为 一维 的技巧,这里不再赘言,感兴趣的可以参考此题的 题解区 一些用户的题解。

五、反转链表例题(力扣网上第206题)

在解决题目前,我们先定义一个链表的节点(以供后文使用)。

/**
 * Definition for singly-linked list.
* struct ListNode {
*     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
*/

1.定义递归函数:

这题我们如何定义递归函数呢?我们可以直接按照题目要求 定义一个函数

ListNode* reverseList(ListNode* head);反转一个以 head 为头节点的链表,并返回 反转后的链表 的头节点。

2.终止条件:

当 输入的 head 为总个链表 尾节点时即可结束。

3.代码:

有一点需要声明:

1.对于当前节点 head 我们需要将其 下一个节点 的 next指针 指向 head,并将 head的 next指针 指向 null;

有了思路后,尝试先自己写出代码实现。

ListNode* reverseList(ListNode* head) {
if(head==nullptr) return head;//为了符合 OJ 平台条件,这里多个判断 
//终止条件
if(head->next==nullptr) return head;
ListNode* nhead = reverseList(head->next);
    head->next->next =head; 
    head->next=nullptr;
    return nhead; 
} 

我们用一组图片来描述这些代码的操作。

比如我们要反转以 head 为头节点 的这个链表。

在这里插入图片描述

ListNode* nhead = reverseList(head->next);

在这里插入图片描述
在这里插入图片描述

head->next->next =head;

在这里插入图片描述

head->next=nullptr;

在这里插入图片描述

后记:

事实上还有一些问题并没有搞懂,如果你要问,我们如何在一开始就定义好一个递归函数去解决问题呢?这就是如何运用好递归的问题了,如本文的标题一样,此文只负责带你彻底理解递归。由于本人的水平有限,这里只能给出一些概括:、递归 是一种 编程技巧,而解题需要 算法思想,而很多算法,比如 动态规划,回溯,分治等,都是可以很好的用递归实现的,对于这方面能力的提升,鄙人也在默默的努力,而鄙人能推荐的自认为最为有效的能提高这方面能力的方法便是:

多刷题!

在此文的题目中有很多技巧性的解法,文章都一笔带过,对于有兴趣的,学有余力的学者都可以详细的去了解。

庄子曰:吾生也有涯,而知也无涯;以有涯随无涯,殆矣。对于大部分的学者需先学好最通俗实用的解法,再去

有的放矢学习其他知识!

本文部分内容参考自 labuladong 微信公众号,极客时间王争的数据结构和算法之美专栏!

本文部分题目来自 力扣网,密码岛!

本文仅供参考学习,请客观性的看待,若有错误之处麻烦指出,以免误人子弟!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

志尊威少

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值