递归——个神奇的算法设计思路
1、写作目的
这个是我在看到数据结构之递归的时候,突然感觉一下子对递归的迷雾有了一点点的感觉,所以想记下来,供大家交流
2、递归思想
当一个问题的完成由几个步骤重复的子问题组成,可以一一递推下去的时候,这是我们可以考虑用递归来实现,递归思想,我的感觉递归就是一个能模糊算法全过程,将一个个问题拆分为子问题,从子问题递推到原问题的结果
比如
对于求解n!的阶层的算法,可以用一个很简单的递归的算法来实现
int fun(int n){
if(n==1)
return 1;
else
return n*fun(n-1);
}
从 上 述 例 子 中 可 以 看 到 , n ! 可 以 拆 分 为 n ⋅ ( n − 1 ) ! ( n − 1 ) ! 又 可 以 拆 分 为 ( n − 1 ) ⋅ ( n − 2 ) ! . . . . . 最 后 可 以 拆 分 为 n ⋅ ( n − 1 ) ⋅ ( n − 2 ) ⋅ . . . × 1 从上述例子中可以看到,n!可以拆分为n\cdot(n-1)! \\(n-1)!又可以拆分为(n-1)\cdot(n-2)! \\..... \\最后可以拆分为n\cdot(n-1)\cdot(n-2)\cdot...\times1 从上述例子中可以看到,n!可以拆分为n⋅(n−1)!(n−1)!又可以拆分为(n−1)⋅(n−2)!.....最后可以拆分为n⋅(n−1)⋅(n−2)⋅...×1
从中,结合书上的定义,可以看到递归其实是将问题不断拆分为子问题,然后从子问题开始直到原问题
3、什么样的问题满足递归
1、递归需要满足的条件
查阅教材和自己分析总结,我感觉对于递归
一个满足递归的问题必定有一个递归模型和一个递归结束的条件即存在递归出口和递归体
什么是递归模型
递归模型是一类执行步骤相同的问题的抽象,感觉就是一个递推式,比如求n的阶层可以写出其递归模型是
f ( n ) = { n ∗ f ( n − 1 ) − − − n ≥ 1 1 − − − n = 1 f(n) = \begin{cases}n*f(n-1)---n\geq1\\1---n=1 \end{cases} f(n)={n∗f(n−1)−−−n≥11−−−n=1
这个也就是递推模型,可以看到只要找到了递推模型,递归算法的设计感觉就是完成了一半,找到递推模型之后,剩余的就是用语言描述递归模型了,这个就是体力活了哈哈哈哈递归算法设计的过程其实首先需要分析出递归算法的设计思路或者说递归模型,然后将递归模型转换为C/C++代码,递归算法的求解过程其实是将一个问题分化为若干个子问题,然后来分别求解子问题,最后来获得一个问题的解,是一种分而治之的思路,通常由总问题划分的若干个子问题是独立的
可以对照前面的代码,代码完全就是递归模型的翻译!
可以看到
对于一个问题是否可以用递归的方式实现,有三点!
- 这个问题是不是存在递归模型!
- 这个问题是不是存在递归模型!!
- 这个问题是不是存在递归模型!!!
2、汉诺塔问题分析
汉诺塔问题
对于许多的问题,其解决方法也是递归实现的,比如汉诺塔问题
汉诺塔问题指的是:将一个柱子中的所有圆盘移动到另一个柱子,移动过程需遵守以下规则:
- 每次只能移动一个圆盘,而且只能移动某个柱子上最顶部的圆盘;
- 移动过程中,必须保证每个柱子上的大圆盘都位于小圆盘的下面
假定三根柱子定义为x,y,z,有**三个盘片(依照从小到大分别编号为A,B,C)**需要从x柱子按照规则移动到z柱子上,它的移动步骤为
- A移动到z柱子
- B移动到y柱子
- A移动到y柱子
- C移动到z柱子
- A移动到x柱子
- B移动到z柱子
- A移动到z柱子
从动画效果可以看到,如果需要将一个盘片从x柱子移动到z柱子,对于三个盘片的顺序,可以从特殊的例子推导出其执行步骤
H a n o i ( n , x , y , z ) = { H a n o i ( n − 1 , x , z , y ) − − 将 第 n − 1 个 盘 片 从 x 经 z 移 动 到 y m o v e ( n , x , z ) − − 将 第 n 个 盘 子 从 x 移 动 到 z H a n o i ( n − 1 , y , x , z ) − − 将 第 n − 1 个 盘 子 从 y 经 x 移 动 到 z Hanoi(n,x,y,z)=\begin{cases} Hanoi(n-1,x,z,y)--将第n-1个盘片从x经z移动到y\\move(n,x,z)--将第n个盘子从x移动到z\\Hanoi(n-1,y,x,z)--将第n-1个盘子从y经x移动到z\end{cases} Hanoi(n,x,y,z)=⎩⎪⎨⎪⎧Hanoi(n−1,x,z,y)−−将第n−1个盘片从x经z移动到ymove(n,x,z)−−将第n个盘子从x移动到zHanoi(n−1,y,x,z)−−将第n−1个盘子从y经x移动到z
其递归模型可以为
H ( n ) = { 2 H ( n ) + 1 − − n ≥ 1 ( 递 归 体 ) 1 − − n = 1 ( 结 束 条 件 ) H(n) = \begin{cases} 2H(n)+1--n\geq1(递归体)\\1--n=1(结束条件)\end{cases} H(n)={2H(n)+1−−n≥1(递归体)1−−n=1(结束条件)
有了这个递归模型,算法设计也就soeasy了!void Hanoi(int n,char X,char Y,char Z){ if(n==1) printf("The %d form %c to %c\n",n,X,Z); //到达递归结束条件 else{ Hanoi(n-1,X,Z,Y); //将第n-1个盘片从x经z移动到y printf("The %d form %c to %c\n",n,X,Z); //将第n个盘片从x移动到z Hanoi(n-1,Y,X,Z); //将第n-1个盘片从y经x移动到z } } //测试代码 #include <string> using namespace std; void Hanoi(int n,char X,char Y,char Z){ if(n==1) printf("The %d form %c to %c\n",n,X,Z); //到达结束条件 else{ Hanoi(n-1,X,Z,Y); printf("The %d form %c to %c\n",n,X,Z); Hanoi(n-1,Y,X,Z); } } int main() { Hanoi(3,'x','y','z'); return 0; }
结果
console
The 1 form x to z
The 2 form x to y
The 1 form z to y
The 3 form x to z
The 1 form y to x
The 2 form y to z
The 1 form x to z
3、牛客网中等算法题
牛客网的原题
现在有一个只包含数字的字符串,将该字符串转化成IP地址的形式,返回所有可能的情况。
例如:
给出的字符串为"25525522135",
返回[“255.255.22.135”, “255.255.221.35”]. (顺序没有关系)
数字字符串转化成IP地址_牛客题霸_牛客网 (nowcoder.com)
然后我们稍作更改
对于一个数字字符串,输出所有合法的IP地址
我的想法
这个问题我的想法是,ip地址的形式为xxx.xxx.xxx.xxx,xxx可以为1,2,3位数,3位数时不能超过255,ip地址其实可以分解为3个
xxx.(递归体)
和1个xxx(结束出口)
那么可以看到递归模型为
f
(
n
)
=
{
1
位
数
.
+
f
(
n
−
1
)
2
位
数
.
+
f
(
n
−
1
)
3
位
数
.
+
f
(
n
−
1
)
输
出
合
法
i
p
−
−
−
n
=
1
(
结
束
条
件
)
f(n) = \begin{cases}1位数.+f(n-1)\\2位数.+f(n-1)\\3位数.+f(n-1)\\输出合法ip---n=1(结束条件)\end{cases}
f(n)=⎩⎪⎪⎪⎨⎪⎪⎪⎧1位数.+f(n−1)2位数.+f(n−1)3位数.+f(n−1)输出合法ip−−−n=1(结束条件)
那么打印IP地址的算法设计可以变为
p
r
i
n
t
I
P
(
n
,
i
p
,
c
o
r
r
i
p
)
=
{
p
r
i
n
t
I
P
(
n
−
1
,
i
p
1
,
c
o
r
r
i
p
1
)
−
−
从
原
i
p
中
提
取
1
位
p
r
i
n
t
I
P
(
n
−
1
,
i
p
2
,
c
o
r
r
i
p
2
)
−
−
从
原
i
p
中
提
取
2
位
p
r
i
n
t
I
P
(
n
−
1
,
i
p
3
,
c
o
r
r
i
p
3
)
−
−
从
原
i
p
中
提
取
3
位
i
p
地
址
合
法
就
输
出
−
−
n
=
=
1
printIP(n,ip,corrip) = \begin{cases}printIP(n-1,ip1,corrip1)--从原ip中提取1位\\printIP(n-1,ip2,corrip2)--从原ip中提取2位\\printIP(n-1,ip3,corrip3)--从原ip中提取3位\\ip地址合法就输出--n==1\end{cases}
printIP(n,ip,corrip)=⎩⎪⎪⎪⎨⎪⎪⎪⎧printIP(n−1,ip1,corrip1)−−从原ip中提取1位printIP(n−1,ip2,corrip2)−−从原ip中提取2位printIP(n−1,ip3,corrip3)−−从原ip中提取3位ip地址合法就输出−−n==1
- ip为输入的原ip地址,ip1为从ip中提取1位放入corrip给corrip1的剩余没有扫描到的字符,其它的以此类推,ip地址对应f(4)
打印所有IP地址算法实现
void printIP(string ip, int i,string corrip) {
string s; //传给下一级的目标ip地址段
if(ip.size()<=0) //如果原ip字符串为空,直接返回
return;
if(i==1&&ip.size()>=1&&ip.size()<=3){ //当道最后一个数字段的时候,如果剩下三位需要判断这三位总和是不是小于255,判断过后给予输出
if(ip.size()>1&&ip[0]=='0') //每一个ip数字段的多位数字的话都不能狗以0开头
return;
int count = 0,k; //判断是否超过255
for(k=0;k<ip.size();k++){
count = count*10+(int)(ip[k]-'0');
}
if(count>=0&&count<=255){ //ip地址符合,进行输出
corrip = corrip+ip;
cout<<corrip<<endl;
}else{
return;
}
}
else if(i==1&&ip.size()>3) //原ip地址的长度不合法,直接返回,没有任何合法的IP地址
return;
else{
string s1(++ip.begin(),ip.end()); //执行-->printIP(n-1,ip1,corrip1)--从原ip中提取1位
s = corrip+ip[0]+".";
printIP(s1,i-1,s);
if(ip.size()>2){ //执行-->printIP(n-1,ip2,corrip2)--从原ip中提取2位
if(ip[0]!='0'){ //如果是多位,那么开头的ip数值不能为0
string s2(++(++ip.begin()),ip.end());
s = corrip + ip[0]+ip[1]+".";
printIP(s2,i-1,s);
}
}
if(ip.size()>3){ //执行-->printIP(n-1,ip3,corrip3)--从原ip中提取3位
if(ip[0]!='0'){ //多位数开头不能位0
int count = 0,k;
for(k=0;k<3;k++){
count = count*10+(int)(ip[k]-'0');
}
if(count>=0&&count<=255){ //不能超过255
string s3(++(++(++ip.begin())),ip.end());
s = corrip+ip[0]+ip[1]+ip[2]+".";
printIP(s3,i-1,s);
}
}
}
}
}
//打印所有的合法IP地址
void printCorrectIPAddress(string IP){
string corrip = "";
printIP(IP,4,corrip);
}
牛客网算法题实现
牛客网的算法题其实跟上面没有区别,只是不需要输出,只是将这些合法ip添加到vector中或者其他的容器中
#include<string>
#include<vector>
class Solution {
public:
/**
*
* @param s string字符串
* @return string字符串vector
*/
//采用递归的形式获取所有合格的IP地址
void getIP(string ip,int i,string corrip,vector<string> &aimvec){
string s; //临时字符串
if(ip.size()<=0) //如果输入ip地址的ip不为0,那么这样的话
return;
if(i==1&&ip.size()>=1&&ip.size()<=3){
int count = 0,k;
if(ip.size()>1&&ip[0]=='0'){
return;
}
for(k=0;k<ip.size();k++){
count = count*10+(int)(ip[k]-'0');
}
if(count>=0&&count<=255){
corrip = corrip+ip;
aimvec.push_back(corrip);
}else{
return;
}
}
else if(i==1&&ip.size()>3)
return;
else{
string s1(++ip.begin(),ip.end());
s = corrip+ip[0]+".";
getIP(s1,i-1,s,aimvec);
if(ip.size()>2){
if(ip[0]!='0'){
string s2(++(++ip.begin()),ip.end());
s = corrip + ip[0]+ip[1]+".";
getIP(s2,i-1,s,aimvec);
}
}
if(ip.size()>3){
if(ip[0]!='0'){
int count = 0,k;
for(k=0;k<3;k++){
count = count*10+(int)(ip[k]-'0');
}
if(count>=0&&count<=255){
string s3(++(++(++ip.begin())),ip.end());
s = corrip+ip[0]+ip[1]+ip[2]+".";
getIP(s3,i-1,s,aimvec);
}
}
}
}
}
vector<string> restoreIpAddresses(string s) {
// write code here
vector<string> aimvec;
if(s.size()<=0)
return aimvec;
else{
string corrip = "";
getIP(s,4,corrip,aimvec);
}
return aimvec;
}
};
4、如何拆解递归为非递归,从什么地方思考
1、我对递归拆解为非递归的思考
其实递归也就是一个顺序执行的过程,就比如4!说先执行1x2,再执行2x3,再执行6x4得到结果,他是按照顺序来实现的,既然是按照一定顺序来实现的,那么大部分递归(不敢说全部哈哈哈)是能够使用非递归的方法来实现的,一定的顺序执行的话,那么栈与队列这两种特殊的数据结构的地位也就十分重要了,真的!利用好这两种数据结构,一定会有奇效!!!
对于前面求n!的阶层的算法
int fun(int n){
int count = 1;
int i = 1;
while(i<=n){
count = count*i; //用循环将递归的顺序实现出来
i++;
}
return count;
}
可以看到,将一个递归的算法转化位非递归的算法,重要的是如何去非递归的按顺序执行递归的顺序(可能有点绕),以及设计好合适的数据结构,来非递归的描述递归模型
2、汉诺塔问题非递归实现
其实一句话说,一个递归的过程也就是将一个问题分为一系列步骤,隐藏了其重复的多任务,而显示出其每一个任务步骤,有一个先后执行的顺序,既然任务有一个先后执行的顺序,那么采用栈和队列也能够实现这个先后执行的顺序,对于汉诺塔问题,其步骤分为
H a n o i ( n , x , y , z ) = { H a n o i ( n − 1 , x , z , y ) m o v e ( n , x , z ) H a n o i ( n − 1 , y , x , z ) Hanoi(n,x,y,z)=\begin{cases} Hanoi(n-1,x,z,y)\\move(n,x,z)\\Hanoi(n-1,y,x,z)\end{cases} Hanoi(n,x,y,z)=⎩⎪⎨⎪⎧Hanoi(n−1,x,z,y)move(n,x,z)Hanoi(n−1,y,x,z)
- 首先将任务Hanoi(n,x,y,z)进栈
- 当栈不为空时进行循环,将Hanoi(n,x,y,z)出栈,做完这件事分为三步,分别为Hanoi(n-1,x,z,y)、move(n,x,z)、Hanoi(n-1,y,x,z),当n=1时,当他可以直接移动时,进行移动
先上代码
说明,算法中的栈是笔者自己写的,大家如果复制代码可以用vector来更改栈的实现形式
#define MaxSize 50
typedef struct {
int n;
char x,y,z; //3个塔座
bool flag; //是不是可以直接移动的盘片
} ElemType;
typedef struct{
ElemType data[MaxSize]; //存放元素
int top;
} StackType;
void Hanoi2(int n,char x,char y,char z){
StackType *st;
ElemType e,e1,e2,e3;
if(n<=0) return;
InitStack(st);
e.n = n; e.x = x;e.y = y;e.z = z;
if(e.n==1){
e.flag = true;
}else{
e.flag = false;
}
Push(st,e);
while(!StackEmpty(st)){
Pop(st,e);
//先入栈的后执行,入栈顺序为Hanoi(n-1,y,x,z)-->move(n,x,z)-->Hanoi(n-1,x,z,y)
//执行顺序为Hanoi(n-1,x,z,y)-->move(n,x,z)-->Hanoi(n-1,y,x,z)
if(e.flag==false){
e1.n = e.n-1;e1.x = e.y;e1.y = e.x;e1.z = e.z;
if(e1.n==1){
e1.flag = true; //当n==1时,只有一个盘片,就可以去执行move了
}else{
e1.flag = false; //n不等于1,执行需要三步
}
Push(st,e1); //Hanoi(n-1,y,x,z)入栈,第三步执行
e2.n = e.n;e2.x = e.x;e2.y = e.y;e2.z = e.z;e2.flag = true;
Push(st,e2); //move(n,x,z)入栈,第二步执行
e3.n = e.n-1;e3.x = e.x;e3.y = e.z;e3.z = e.y;
if(e3.n==1){
e3.flag = true;
}else{
e3.flag = false;
}
Push(st,e3); //Hnaoi(n-1,x,z,y)入栈,第一步执行
}else{
printf("The %d is from %c to %c",e.n,e.x,e.z);
}
}
DestoryStack(st); //销毁栈
}
3、牛客网算法题非递归实现
上代码!
//ip数字段
struct digitalSec{
string corrip;
string ip;//剩余为查找的ip
int n; //第几个数字段
};
void PrintIP(string ip){
vector<digitalSec> stack;
digitalSec d,d1,d2,d3,*dd;
d.corrip = "";d.ip = ip;d.n = 4;
stack.push_back(d);
while(!stack.empty()){
d = stack.back();
stack.pop_back();
//执行递归模型的语句
if(d.n==1){ //到达最后一个ip段了
//剩余ip看是否满足三位数或者在三位数以下
if(d.ip.size()>=1&&d.ip.size()<=3){
//多位数ip开头不能为0,并且如果是三位数的话,不能超过255
if(d.ip.size()>1&&d.ip[0]!='0'){
int count=0,k;
for(k=0;k<d.ip.size();k++){
count = count*10+(int)(d.ip[k]-'0');
}
if(count>=0&&count<=255){
d.corrip = d.corrip + d.ip;
cout<<d.corrip<<endl;
}
}
else if(d.ip.size()==1){
d.corrip = d.corrip + d.ip;
cout<<d.corrip<<endl;
}
}
}else{
//首先执行-->printIP(n-1,ip1,corrip1)--从原ip中提取1位
if(d.ip.size()>0){
string s1(++d.ip.begin(),d.ip.end());
d1.ip = s1;
d1.corrip = d.corrip+d.ip[0]+".";
d1.n = d.n-1;
stack.push_back(d1);
}
if(d.ip.size()>1){ //执行-->printIP(n-1,ip2,corrip2)--从原ip中提取2位
if(d.ip[0]!='0'){ //两位数的时候,ip数字段的开头不能为0
string s2(++(++d.ip.begin()),d.ip.end());
d2.ip = s2;
d2.corrip = d.corrip+d.ip[0]+d.ip[1]+".";
d2.n = d.n-1;
stack.push_back(d2);
}
}
if(d.ip.size()>2){ //执行-->printIP(n-1,ip3,corrip3)--从原ip中提取3位
if(d.ip[0]!='0'){ //三位数的时候,ip数字段的开头不能为0,并且不能超过255
int count=0,k;
for(k=0;k<3;k++){
count = count*10+(int)(d.ip[k]-'0');
}
if(count>=0&&count<=255){
string s3(++(++(++d.ip.begin())),d.ip.end());
d3.ip = s3;
d3.corrip = d.corrip + d.ip[0]+d.ip[1]+d.ip[2]+".";
d3.n = d.n - 1;
stack.push_back(d3);
}
}
}
}
}
}
最后的小总结
-
递归问题他一定会存在一个递归模型,找到了递归模型,那么递归的问题也就解决一大半了哈哈哈
-
将递归的形式转化为非递归的形式,需要利用到栈和队列特定的实现方式,来模拟出递归的顺序,感觉理论上一些递归不是十分复杂的递归算法都能通过栈和队列转化成非递归的算法,使用非递归算法实现递归算法的关键感觉是如何去找到出入栈的顺序与递归顺序的联系以及数据结构的组织形式
-
这是看了递归之后的一些总结,如果有问题的话,欢迎交流呀!