文章目录
上课部分
例1 输入n的值,求n!
方法一
颜老板版本
#include<stdio.h>
int main()
{
double n,s,i;
scnaf("%lf",&n);
s=1;
for(i=1;i<=n;i++)
s=s*i;
printf("%lf\n",s);
return 0;
}
更新版
#include<iostream>
using namespace std;
int main()
{
int n;
cin>>n;
int res=1;
for(int i=1;i<=n;i++)res*=i;
cout<<res;
return 0;
}
方法2:
题目是求n!
所以分析一个实例 求5!
5!=12345
变化
5!=54321
变化
5!=54! 如果要计算出5!,必须先计算出4!
4!=43! 如果要计算出4!,必须先计算出3!
3!=32! 如果要计算出3!,必须先计算出2!
2!=21! 如果要计算出2!,必须先计算出1!
1!=1 1!=1
把刚才从上至下的这个分析过程 称之为 递推。
推: 表示的是推理,找求解方法,以及求解过程。
推到1!=1为止
但是这个过程其实并没有计算出5!。
什么时候开始执行5!的计算
把上述过程再从底部逆流而上,最终计算出5!
1)把1!的结果1,代入到上一个表达式中执行21,得2!的结果是2
2)把2!的结果2,代入到上一个表达式中执行32,得3!的结果是6
3)把3!的结果6,代入到上一个表达式中执行46,得4!的结果是24
4)把4!的结果24,代入到上一个表达式中执行524,得5!的结果是120
把上述过程 称之为 递归!
今天所要讲解的主题就是:递归。
递归算法是比较简单的且算是基础类算法。
但是它的扩展能力是无限。
分治法等可能会用递归的思想。
但是上述写法是不符合程序的写法的。算是数学写法。所以要改写上述的分析过程。
改为函数的写法。
因为函数写法符合程序语言的写法。
5!=54!
4!=43!
3!=32!
2!=21!
1!=1
上述5个数学写法 改写为 函数写法
fact(5)=5fact(4)
fact(4)=4fact(3)
fact(3)=3fact(2)
fact(2)=2fact(1)
fact(1)=1
上述5个表达式的样子归纳为2类
fact(n)=n*fact(n-1) 如果要执行该操作必须满足条件n>=2 (递归算法表达式)
fact(n)=1 如果要执行该操作必须满足条件n==1
代码如下
颜老板版本
#include<stdio.h>
double fact(double n)
{
double s;
if(n>=2)
s=n*fact(n-1);
else if(n==1)
s=1;
return s; //如果程序的结果需要另做他用的时候,这时候是不输出,而是做返回,返回值给调用该函数的地方
}
int main()
{
double s,n;
scanf("%lf",&n);
s=fact(n);
printf("%lf\n",s);
return 0;
}
简化
#include<stdio.h>
double fact(double n)
{
if(n>=2)
return n*fact(n-1);
else if(n==1)
return 1;
}
int main()
{
double s,n;
scanf("%lf",&n);
s=fact(n);
printf("%lf\n",s);
return 0;
}
更新版
#include<iostream>
using namespace std;
int func(int num,int sum)
{
if(!num)return sum;
func(num-1,sum*num);
}
int main()
{
int n;
cin>>n;
cout<<func(n,1);
return 0;
}
注意:
1)递归的本质其实就是循环
但是循环分2种:
a)一模一样
b)相似
2)递归的程序编写一定要注意,分析问题求解方法的过程中,第一首要重点是找到问题的求解方法。
不同的数据对象,但是求解方法是一致的。
基本上都可以思考使用递归的思想来解决问题。
3)递归函数(包含使用递归思想编写的算法或者程序)都必须满足2个要素。
a)递归算法(求解方法)
b)有终结条件(终止条件、终止情况)
4)提醒:之前给大家在群里发了教材的电子书,下来之后所有人把教材P48和49页的分析过程好好看看。
递归的定义
所谓递归是指在调用一个函数的过程中,又调用该函数本身。
函数目的是实现程序中某些功能。说明该函数在处理一类数据等问题的时候,处理的方法必须是一致的。
1 直接递归
调用函数的过程中,又调用该函数本身。
2 间接递归
如果调用一个f1函数,在f1函数的内部又调用了f2函数,而在f2函数内部又调用了f1函数
3 尾递归
如果一个函数在所有命令执行结束的时候,才执行了函数本身的再次调用。
何时使用递归?
1 很多数学中学过的定义或者定理,基本上都是递归的思想
2 数据结构中:链表、二叉树等
3 来自生活中:
求最大公约数,有另外一种比较简答写法。辗转相除法。这个用递归的思想来书写。
递归的执行过程
1 递推
2 递归
最后题型:在问题分析中一定要找求解方法。一旦遇见所谓的求解方法是一致的。那就是用递归。
例2 输入n的值,求得并输出第n个fibonacci数列的数值。
已知某个数列的第1个数和第2个数的值都是1,从第3个数值开始,每个数值等于其前2个之和。
颜老板版本
#include<stdio.h>
int main()
{
double a[1000]={1,1};
int i,n;
scanf("%d",&n);
for(i=2;i<n;i++)
a[i]=a[i-2]+a[i-1];
printf("%lf\n",a[n-1]);
return 0;
}
更新版
#include<iostream>
using namespace std;
const int N=1e5+10;
int f[N]={0,1,1};
int main()
{
int n;
cin>>n;
for(int i=3;i<=n;i++)f[i]=f[i-1]+f[i-2];
cout<<f[n];
return 0;
}
分析
换递归思想来考虑
求解方法是一致的。因为【从第3个数值开始,每个数值等于其前2个之和】
函数,书写的时候参数 有 或者 没有
【有】写参数 必须要完成某个功能必须已知的数据
【没有】不写参数 完成某个功能必须要已知任何的数据
分析:
如果要求第n个fibonacci数列的值,必须已知n,所以该方法(函数)的参数是有的,且是n
fib(n)
fib(n-2)+fib(n-1)
fib(n-4)+fib(n-3) fib(n-3)+fib(n-2)
......................................................
整个这个分析层层往下,整个分析的形式特别像二叉树
推论到最后一层,基本上都是fib(1)或者是fib(2),而它们的值都是1,作为终结条件。
颜老板版本
编写程序
#include<stdio.h>
double fib(int n)
{
if(n>=3)
return fib(n-2)+fib(n-1);
else if(n==1 || n==2)
return 1.0;
}
int main()
{
int n;
scanf("%d",&n);
printf("%lf\n",fib(n));
return 0;
}
更新版
#include<iostream>
using namespace std;
int fib(int n)
{
if(n==1||n==2)return 1;
else return fib(n-1)+fib(n-2);
}
int main()
{
int n;
cin>>n;
cout<<fib(n);
return 0;
}
分析上述方法
算法时间复杂度是多少? o(2^n)
详细分析
当n的值是 fib函数调用的次数是
1 1
2 1
3 3
4 4
…
用二叉树的思想来分析
层数 节点数
1 1 2^1-1
2 3 2^2-1
3 7 2^3-1
n 2^n-1
算法空间复杂度是多少? o(1)、
在本学期中,要求大家必须掌握6种排序算法!
以下2种熟悉的排序算法必须用递归思想来编写
例3 选择法排序
思路: 在n个数值中,求得最小的元素并把其放在开头上。
【在n个数值中,求得最大的元素并把其放在尾巴处】
1 要求得n个数值中的最小值和最小值的位置
min where
2 在和开头的数值进行交换【目的:最小值在最前面】
3 抛开开头的数值,剩下所有数值重复1,2步骤。【无论数据多少,都是同一种处理方法】
4 考虑一个什么是结束
代码如下
颜老板版本
#include<stdio.h>
//书写程序的时候尽量功能独立
//交换函数
void swap(double *p,double *q)//参数必须是已知,必须要已知交换对象的内存地址
{
double t;
t=*p;
*p=*q;
*q=t;
}
void selectsort(double a[],int n,int begin)//已知数据存储的空间,数据的个数,所有数据的起始位置
{
int where;
double min;
//如果begin起始位置等于终止位置n-1的时候,终止
if(begin==n-1)
return ;
else
{
int j;
//1 求最小值和最小值的位置
min=a[begin];
where=begin;
for(j=begin+1;j<=n-1;j++)
{
if(min>a[j])
{
min=a[j];//新的最小值
where=j; //新的最小值的位置
}
}
//如果最小值的位置就是开头的那个数值,那什么都不用做了,否则交换
if(where!=begin)
swap(&a[where],&a[begin]);//要实现交换必须要实参为数据在内存中的地址,而不是数据本身
//处理完上述之后,继续处理接下来的剩下的数值,使用同一种方法,而这里就是递归调用
selectsort(a,n,begin+1);
}
}
int main()
{
double a[1000];
int n,i;
printf("输入n的值表示数据个数:");
scanf("%d",&n);
printf("输入%d个数值:",n);
for(i=0;i<n;i++)
scanf("%lf",&a[i]);
selectsort(a,n,0);//调用selectsort函数,a数组名,n数据个数,0表示开始位置
printf("输入排序之后的数值:\n");
for(i=0;i<n;i++)
printf("%lf\n",a[i]);
return 0;
}
更新版
#include<iostream>
using namespace std;
void func(int a[],int begin,int len)
{
if(begin == len-1)return;
int tmpIdxNum=a[begin],idx=begin;
for(int i=begin+1;i<len;i++)if(tmpIdxNum>a[i]){tmpIdxNum=a[i];idx=i;}
swap(a[idx],a[begin]);
func(a,begin+1,len);
}
int main()
{
int n;
cin>>n;
const int len = n;
int a[len+10];
for(int i=0;i<len;i++)cin>>a[i];
func(a,0,len);
for(int i=0;i<len;i++)cout<<a[i];
return 0;
}
例4 冒泡法排序:
要求用递归的思想来书写
分析
算法思想:
有一个数组,且为n个数值
1)从左至右依次比较,如果左边>右边为真,则交换,否则不交换【目的,最终最大值在尾巴处】
- 接下里考虑除了尾巴处的那个数据以外其他的数值,继续执行第1)步 【递归算法】
3)每次处理的数据越来越少,到最后只剩下1个数值的时候,终止了 【终结条件】
当只剩下第0个数值的时候
颜老板版本
#include<stdio.h>
//书写程序的时候尽量功能独立
//交换函数
void swap(double *p,double *q)//参数必须是已知,必须要已知交换对象的内存地址
{
double t;
t=*p;
*p=*q;
*q=t;
}
//已知数据存储的空间,数据的个数,所有数据的终止位置
void bubblesort(double a[],int n,int end)
{
//当只剩下一个数值,第0个数值,end==0就说明只剩下一个数值。 【这里是终结条件】
if(end==0)
return ;
else
{
int j;
for(j=0;j<=end-1;j++)
{
if(a[j]>a[j+1])//左边大于右边,所以左边的数据对象的标号最后只能取end-1
{
swap(&a[j],&a[j+1]);
}
}
//接下来使用同一种方法,处理的数据要去掉末尾的数据【这里就是递归算法】
bubblesort(a,n,end-1);
}
}
int main()
{
double a[1000];
int n,i;
printf("输入n的值表示数据个数:");
scanf("%d",&n);
printf("输入%d个数值:",n);
for(i=0;i<n;i++)
scanf("%lf",&a[i]);
bubblesort(a,n,n-1);//调用selectsort函数,a数组名,n数据个数,n-1表示终止位置
printf("输入排序之后的数值:\n");
for(i=0;i<n;i++)
printf("%lf\n",a[i]);
return 0;
}
更新版
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
void func(int a[],int len)
{
if(!len)return ;
for(int i=0;i<len-1;i++)if(a[i]>a[i+1])swap(a[i],a[i+1]);
func(a,len-1);
}
int main()
{
int n;
cin>>n;
const int len = n;
int a[len+10];
memset(a,0,sizeof a);
for(int i=0;i<n;i++)cin>>a[i];
func(a,len);
for(int i=0;i<n;i++)cout<<a[i];
return 0;
}
课题总结
归纳上述2个排序算法
1)上述2个排序算法是所有排序算法中 理论最简单,最好理解的算法
2)掌握的是递归写法!
3)在这写法中已经出现了指针的概念
4)对比选择法排序和冒泡法排序【不要代入其他的排序算法来讨论】
白话: 选择法 代码长 但是效率高
冒泡法 代码短 但是效率低
专业: 选择法排序和冒泡法排序的算法时间复杂度和空间复杂度没区别!是一样的!
如果后面学了其他排序算法后,你会觉得,这2个排序算法就是渣渣!!!
今天的主题:【要求所有人必须要学会书写递归算法的程序,要求掌握6种排序算法,并且要有对比】
作业部分
题目链接:
选择的是里面的难度为“简单”
https://leetcode-cn.com/tag/recursion/
1137. 第 N 个泰波那契数
泰波那契序列 Tn 定义如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2
给你整数 n,请返回第 n 个泰波那契数 Tn 的值。
示例 1:
输入:n = 4
输出:4
解释:
T_3 = 0 + 1 + 1 = 2
T_4 = 1 + 1 + 2 = 4
示例 2:
输入:n = 25
输出:1389537
提示:
0 <= n <= 37
答案保证是一个 32 位整数,即 answer <= 2^31 - 1。
纯递推过不了34/39,复杂度太高,只能加记忆化。
AC代码
递推
class Solution {
public:
int tribonacci(int n) {
int dp[1000]={0,1,1};
if(n==0)return 0;
else if(n==1||n==2)return 1;
for(int i=3;i<=n;i++)dp[i]=dp[i-1]+dp[i-2]+dp[i-3];
return dp[n];
}
};
记忆化搜索
class Solution {
public:
int dp[38]={0,1,1};
int tribonacci(int n) {
if(n==0)return 0;
return dp[n]?dp[n]:dp[n] = tribonacci(n-1)+tribonacci(n-2)+tribonacci(n-3);
}
};
面试题10- II. 青蛙跳台阶问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。
求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入:n = 2
输出:2
示例 2:
输入:n = 7
输出:21
提示:
0 <= n <= 100
AC代码
斐波拉契变种
n == 1 走一阶
n == 2 走两阶
第三步开始 当前步由前两步决定f[n]=f[n-1]+f[n-2];
记忆化搜索
class Solution {
public:
int dp[101]={1,1,2};
int numWays(int n) {
return dp[n]?dp[n]:(dp[n] = ((numWays(n-1)+numWays(n-2))%(1000000007)));
}
};
面试题 16.11. 跳水板
你正在使用一堆木板建造跳水板。
有两种类型的木板,其中长度较短的木板长度为shorter,长度较长的木板长度为longer。
你必须正好使用k块木板。
编写一个方法,生成跳水板所有可能的长度。
返回的长度需要从小到大排列。
示例:
输入:
shorter = 1
longer = 2
k = 3
输出: {3,4,5,6}
提示:
0 < shorter <= longer
0 <= k <= 100000
AC代码
时间 5.75% 内存 100%
注:
1.思考之后,发现由于参数限制和返回值限制无法做到知用一个函数递归写完,于是思路转向爆搜,最后又转回来了
思路:
1.公式枚举可能直接放入set去重
3.放入vec数组
4.vec排序
枚举
枚举 时间75.46% 空间 100%
class Solution {
public:
vector<int> divingBoard(int shorter, int longer, int k) {
if(!k)return {};
if(shorter == longer)return{k*shorter};
vector<int>a(k+1);
for(int i=0;i<=k;i++)a[i]=shorter*(k-i)+longer*i;
return a;
}
};
递归
cclass Solution {
public:
vector<int> ans;
set<int> Set;
void dfs(int shorter, int longer, int k,int u)
{
if(u<0)return;
int t = shorter*(k-u)+longer*u;
if(!Set.count(t)&&t)Set.insert(t);
dfs(shorter,longer,k,u-1);
}
vector<int> divingBoard(int shorter, int longer, int k) {
dfs(shorter,longer,k,k);
for(auto op:Set)ans.push_back(op);
sort(ans.begin(),ans.end());
return ans;
}
};
自学部分
1 P62页,例2.12
拆分输出
【例2.12】设计一个递归算法,输出一个大于零的十进制数n的各数字位,如n=123,输出各数字位为123。
思路
直接压递归,利用回溯的思路输出
AC代码
#include<iostream>
using namespace std;
void func(int n)
{
if(n)func(n/10),cout<<n%10<<endl;
}
int main()
{
int n;
cin>>n;
func(n);
return 0;
}
n皇后的问题
P66页,求解n皇后的问题
提醒大家:书上某些程序是有错的,是不能直接执行。
n-皇后问题是指将 n 个皇后放在 n∗n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
现在给定整数n,请你输出所有的满足条件的棋子摆法。
输入格式
共一行,包含整数n。
输出格式
每个解决方案占n行,每行输出一个长度为n的字符串,用来表示完整的棋盘状态。
其中”.”表示某一个位置的方格状态为空,”Q”表示某一个位置的方格上摆着皇后。
每个方案输出完成后,输出一个空行。
输出方案的顺序任意,只要不重复且没有遗漏即可。
数据范围
1≤n≤9
输入样例:
4
输出样例:
.Q..
...Q
Q...
..Q.
..Q.
Q...
...Q
.Q..
思路
标准的DFS结构,值得注意的地方自我感觉有两点
1.分为选择和不选择两种情况
2.需要用数组构建反对角线和对角线
2.1.反对角线 y=x+b b=y-x 因为不可能是负数可以写为 b=n-x+y dg[x+y]
,
2.2.由反对角线我们的到对角线 udg[len-x+y]
AC代码
#include<iostream>
using namespace std;
const int N=2*10;
char g[N][N];
bool col[N],row[N],dg[N],udg[N];
void dfs(int x,int y,int s,int len)
{
if(x==len)x=0,y++;
if(y==len)
{
if(s==len)
{
for(int i=0;i<len;i++)cout<<g[i]<<endl;
cout<<endl;
}
return ;
}
g[x][y]='.';
dfs(x+1,y,s,len);
if(!col[x]&&!row[y]&&!dg[x+y]&&!udg[len-x+y])//行 列 对角线 反对角线 都被标记
{
g[x][y]='Q';
col[x]=row[y]=dg[x+y]=udg[len-x+y]=true;
dfs(x+1,y,s+1,len);
col[x]=row[y]=dg[x+y]=udg[len-x+y]=false;
g[x][y]='.';
}
}
int main()
{
int n;
cin>>n;
dfs(0,0,0,n);
return 0;
}
了解更多
如果你对这个写法并不够清晰或者想了解更多可以查看我写的另一篇文章n皇后问题
hanoi
2 要求掌握 汉诺塔程序的编写
思路
我们可以直接利用递归的思路
把现有的拆分掉
假设我们有N个块
n-1被拿去,想办法给到b,然后想办法再送到c
轮到第n个被拿去直接丢给c
这样我们的思路就十分清晰了
AC代码
#include<iostream>
using namespace std;
void hanoi(int n,char a,char b,char c)
{
if(!(n-1)){cout<<a<<"->"<<c<<endl;return ;}//剩下的这一个直接给c
hanoi(n-1,a,c,b);//n-1从a到b
cout<<a<<"->"<<c<<endl;
hanoi(n-1,b,a,c);//n-1从b到c
}
int main()
{
int n;
cin>>n;
hanoi(n,'a','b','c');
return 0;
}
了解更多
如果你对这个写法并不够清晰或者想了解更多可以查看我写的另一篇文章hanoi问题