CSP初赛复习-17-基础算法-递归

递归算法

递归就是函数自己直接或者间接的调用自己

在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法

递归2重要概念

1 递归式-递归函数

将原问题分为若干规模较小、相互独立、与原问题形式相同或相似的子问题

比如:斐波那契数列

n 0 1 2 3 4 5 6 7 8
f(n) 1 1 2 3 5 8 13 21 34

递归式

F(n)=F(n - 1)+F(n - 2)

2 递归边界

递归边界则是分解的尽头,如果递归式不断递归而不进行阻止,那么最后将进入无穷尽的死循环,将无法解决问题

比如:斐波那契数列

递归到

F(0)=1 F(1)=1

常见递归问题

1 斐波那契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,
指的是这样一个数列:1、1、2、3、5、8、13、21、34、……
在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(3)=2,F(n)=F(n-1)+F(n-2)(n>=4,n∈N)

分析

定义函数实现求第几项斐波那契数

int fib(int n)

入参 n表示第几项

出参 返回第n项斐波那契数

实现

递归式

fib(n)=fib(n-1)+fib(n-2)

递归出口

n=1 或 n=2

参考代码

#include<bits/stdc++.h>
using namespace std;
//求 第n项斐波那契数列数
int fib(int n){
	if(n==1 || n==2){//前2项直接返回
		return 1;
	}
	return fib(n-1)+fib(n-2);//第3项=前两项之和
}

int n;
int main(){
	cin>>n;
	cout<<fib(n)<<endl;
}

时间复杂度

递归求斐波那契数列时间复杂度:O(2^n)

递归树分析

节点单一子问题代价:函数执行过程中,除去递归调用以外的代价

比如:

int fib(int n){
	if(n==1 || n==2){//前2项直接返回
		return 1;
	}
	return fib(n-1)+fib(n-2);//第3项=前两项之和
}

1 n=1 或 n=1时,return 1 时间复杂度为O(1)

2 n>2时 执行: fib(n-1)+fib(n-2) 除去递归,只有加法运算 时间复杂度为O(1)

3 需要计算整个过程多少个O(1)

4 如果是完美二叉树,即:所有都填满的情况下为O(2^n)

因此时间复杂度粗略计算为O(2^n)

实际再(sqrt(2))^n ~2^n之间,粗略计算 O(2^n)

精确计算其时间复杂度为:((1+sqrt(5))/2)^n

2 汉诺塔

描述

约19世纪末,在欧州的商店中出售一种智力玩具,在一块铜板上有三根杆,最左边的杆上自上而下、由小到大顺序串着由64个圆盘构成的塔。目的是将最左边杆上的盘全部移到中间的杆上,条件是一次只能移动一个盘,且不允许大盘放在小盘的上面。
这是一个著名的问题,几乎所有的教材上都有这个问题。由于条件是一次只能移动一个盘,且不允许大盘放在小盘上面,所以64个盘的移动次数是:18,446,744,073,709,551,615
这是一个天文数字,若每一微秒可能计算(并不输出)一次移动,那么也需要几乎一百万年。我们仅能找出问题的解决方法并解决较小N值时的汉诺塔,但很难用计算机解决64层的汉诺塔。

假定圆盘从小到大编号为1, 2, ...

输入

输入为一个整数后面跟三个单字符字符串。
整数为盘子的数目,后三个字符表示三个杆子的编号

输出

输出每一步移动盘子的记录。一次移动一行。
每次移动的记录为例如 a->3->b 的形式,即把编号为3的盘子从a杆移至b杆

样例输入

2 a b c

样例输出

a->1->c
a->2->b
c->1->b

分析

定义函数实现把n个圆盘从a通过b移动到c

void mov(int n,char a,char c,char b)

如果想把n个圆盘从a经过b移动到c,分3步

1需要把n-1个通过c移动到b

2 需要把第n个直接移动到c

3 需要把n-1个从b,通过a移动到c

参考程序

#include<bits/stdc++.h>
using namespace std;

int n;
//b做过渡 a移动到c 
void mov(int n,char a,char c,char b){
	if(n==0) return;
	mov(n-1,a,b,c);//c做过度将a上的n-1个盘子移动到b 
	printf("%c->%d->%c\n",a,n,c);
	mov(n-1,b,c,a);//a做过度将b上的n-1个盘子移动到c 
}
int main(){
	char x,y,z;
	scanf("%d %c %c %c",&n,&x,&y,&z);
	mov(n,x,y,z);
	return 0;
}

时间复杂度

汉诺塔问题:O(2^n)

节点单一子问题代价:函数执行过程中,除去递归调用以外的代价,也为O(1)

节点数为2^n

3 归并排序

分析

使用归并排序,

1 从中间把数列分成左右两部分

2 把分成的左右2部分中每部分再分成左右2部分

3 一直分下去,直到分拆成1个数

4 再按刚才拆的顺序合并

参考程序

#include<bits/stdc++.h>
using namespace std;

const int N=1e5+5;
int n,a[N],tmp[N];

void merge(int L,int R,int mid){
	int i=L,j=mid+1,k=L;
	while(i<=mid && j<=R){
		if(a[i]<a[j]){
			tmp[k++]=a[i++]; 
		}else{
			tmp[k++]=a[j++];
		}
	}
	while(i<=mid) tmp[k++]=a[i++];
	while(j<=R)   tmp[k++]=a[j++];
	for(int i=L;i<=R;i++){
		a[i]=tmp[i];
	}
}

void mergeSort(int L,int R){
	if(L>=R) return;
	int mid=L+(R-L)/2;
	mergeSort(L,mid);//左半部分排序
	mergeSort(mid+1,R);//右半部分排序
	merge(L,R,mid);//合并
}

int main(){
	cin>>n;
	for(int i=0;i<n;i++){
		cin>>a[i];
	}
	
	mergeSort(0,n-1);
	
	for(int i=0;i<n;i++){
		cout<<a[i]<<" ";
	}
	return 0;
}

时间复杂度

归并排序时间复杂度为:O(n*logn)

节点单一子问题代价:函数执行过程中,除去递归调用以外的代价

归并排序被拆分了 logn层,每层合并代价为n,所以总代价为n*logn

递归优化

递归的思想简单易懂,但是如果采用直接递归来实现,存在着大量“冗余”计算,效率比较低

一般有2种优化思路

1 递归转化成递推

从小到大递推计算过程中,从小到大逐一计算,每次产生一个新数据,避免了递归的重复计算

例如:斐波那契数列-递推实现

#include<iostream>
using namespace std;
int n,f[105];
int main(){
	cin>>n;
	f[0]=1;//初始化第1个数
	f[1]=1;//初始化第2个数
	for(int i=2;i<=n;i++){//从第3个数开始 当前数等于前两个数之和
		f[i]=f[i-1]+f[i-2];
	}
	cout<<f[n];
	return 0;
}

前几项如下,可参考手工计算

n 0 1 2 3 4 5 6 7 8
f(n) 1 1 2 3 5 8 13 21 34

2 记忆化

设置一个数组存储计算过的每项的值

每次递归计算时,先判断数组中是否已经存在

如果存在直接数组获取

如果不存在递归计算后返回

例如:斐波那契数列-记忆化

#include<bits/stdc++.h>
using namespace std;
int a[50]; 
int fib(int n){
	if(a[n]!=0) return a[n];
	if(n==1){
		a[1]=1; 
		return 1;
	}
	if(n==2){
		a[2]=1; 
		return 1;
	}
	a[n]=fib(n-1)+fib(n-2);
	return a[n];
}

int n;
int main(){
	cin>>n;
	cout<<fib(n)<<endl;
}
posted @ 2023-07-26 23:01  new-code  阅读(109)  评论(0)    收藏  举报