先不说理论,先来个简单的递归小例子感受一下:
字符串的反转:
#include <stdio.h>
//递归字符串反转
void reverse(char* s)
{
if( (s != NULL) && (*s != '\0') )
{
reverse(s + 1);
printf("%c", *s);
}
}
int main(int argc, char *argv[])
{
reverse("12345");
printf("\n");
printf("Press enter to continue ...");
getchar();
return 0;
}
运行截图:
首先理解函数调用时的栈:
1. 其实程序中的“函数调用栈”是栈数据结构的一种应用
2. 函数调用栈一般是从高地址向低地址增长的
1> 栈底为内存的高地址处
2> 栈顶为内存的低地址处
3. 函数调用栈中存储的数据为活动记录
举个简单例子来表示一个完整程序的调用:
程序中的栈:
1. 程序中的栈空间可以看做一个顺序栈的应用
2. 栈保存了一个函数调用所需的维护的信息
1> 函数参数,函数返回地址
2> 局部变量
3> 函数调用上下文
3. 程序栈空间在本质上是一种顺序栈
4. 程序栈空间的访问是通过函数调用进行的
5. 程序栈空间仍然遵从后进先出的规则
现在再回过头来分析上面的程序中函数的调用过程和程序中的栈:
如图:
首先:函数从main处开始执行,栈底最先保存的是main函数的活动记录,然后执行到了reverse函数,栈底会保存reverse函数的活动记录, 在reverse活动记录里面保存的参数是 s-->“12345”, 所以*s代表的就是1;
然后判断reverse函数里面的if语句里面的条件还是为真,继续递归调用自己,这个时候又会在栈上新建一条活动记录,这是(s+1)-->"2345",*s就代表了2;然后继续判断是否满足if语句里面的条件,继续满足。
继续递归调用自己,*s就代表了3
......
继续递归调用自己,*s就代表了5
继续递归调用自己,*s指向了‘\0’, 不能满足条件意味着不能进行递归,也就意味着这次递归调用结束了。递归调用结束后意味着退栈。退栈也就是意味着从栈顶把一条活用记录弹出来,弹出来之后恢复上一次的调用,从上面的图中可以看出上一次的调用包涵两个语句,其中一个是printf语句。所以先输出一个5(*s指向5),然后继续退栈,reverse调用结束后又到了printf()语句,所以就打印出了一个4(*s指向4); 继续退栈,弹出栈顶的活动记录。。。依次类推,所以结果输出了“54321”.是不是很神奇。
这里参考网上一篇通俗易懂的博文:http://chengengjie.blog.163.com/blog/static/1263313972013216105710595/
和上面说的大概过程差不多,可以对照着看。
下面开始进行递归深度实战:
递归的定义:程序自身调用自身的编程技巧称之为递归。递归有直接递归和间接递归
1.直接递归:函数在执行过程中调用自身
2.函数在执行过程中调用其他函数再经过函数调用本身
递归的四个特性:
1. 必须有可最终到达的终止条件(上面程序中i的f判断语句就是),否则程序将进入无限循环
2. 子问题在规模上比原问题小,或更接近终止条件
3. 子问题可以通过再次递归调用求解或因满足终止条件而直接求解
4. 子问题的解应能组合为整个问题的解,对比着上面简单的小例程看看,就是那么回事!
递归的数学思想:
1. 递归是一种数学上分而自治的思想
2. 递归将大型复杂问题转化为与原问题相同但规模较小的问题进行处理
3. 递归需要有边界条件,当边界条件不满足时,递归继续进行,当边界条件满足时,递归停止。
用递归解决问题首先要建立递归的模型,非常重要!(数据结构与算法到了后边的树和图算法会大量用到递归)
demo1:斐波拉契数列递归解法
斐波拉契数列大致描述(不是很清楚可以出门左拐找度娘。。。)
1 2 3 4 5 6......(表示项数)
1 1 2 3 5 8...... ,可以看出斐波拉契数列An = An-1 + An-2;在这里A1 = 1;为了运算方便定义第0项A0 = 0;对比上面的递归定义和例子看看是不是满足递归条件!即求An都可以将原问题化解为An-1 + An-2来求解,然后An-1 = An-2 + An-3,An-2 = An-3 + An-4,这样就依次转为与原问题相同但规模较小了。同时又了递推关系,还有边界条件A1 = 1,A0 = 0;所以就很容易的写出程序了。
//斐波拉契数列递归求解
#include <stdio.h>
int fibonacci(int n)
{
if( n > 1 )//首先是递推 n == 1 和 n == 0边界条件也可以放前面
{
return fibonacci(n-1) + fibonacci(n-2);
}
else if( n == 1 )
{
return 1;
}
else if( n == 0 )
{
return 0;
}
}
int main()
{
int i = 0;
for(i=1; i<=10; i++)
{
printf("fibonacci(%d) = %d\n", i, fibonacci(i));
}
printf("Press enter to continue ...");
getchar();
return 0;
}
运行截图:
demo2: strlen递归解法
//strlen递归求解
#include <stdio.h>
int strlen(const char* s)
{
if( s == NULL )//传入参数合法性判断
{
return -1;
}
else if( *s == '\0' )//边界条件
{
return 0;
}
else
{
return strlen(s+1) + 1;//建立递归模型,和上面的字符串反转模型类似
}
}
int main()
{
printf("strlen(\"12345\") = %d\n", strlen("12345"));
printf("strlen(NULL) = %d\n", strlen(NULL));
printf("strlen(\"\") = %d\n", strlen(""));//空串
printf("Press enter to continue ...");
getchar();
return 0;
}
运行截图:
这两个问题的递归模型都比较简单,下面开始来难度逐步加大一点的
demo3: 汉诺塔递归求解
问题模型:
有a、b、c三根柱子,先假设柱子a上有三个大小不同的盘子,且小的盘子始终只能放在大盘子的上面,现在要借助柱子b把柱子a上面的盘子移到柱子c上且搬移完后c柱子上小的盘子始终放在大盘子的上面,搬移的过程中任意一个时刻也必须满足小的盘子始终只能放在大盘子的上面(当然柱子上的只放一个盘子时可以放任何一个盘子)。
分析建立递归模型:
最终就变成我们想要的结果。
针对上面移动图总结一下:移动3个盘子,问题可以分解为三步:
第一步:移动两个
第二步:然后直接移动a到c
第三步:再移动两个
这样就将移动三个盘的问题转化为移动两个盘的问题!是不是很神奇!(两个移动的过程中移动的目的地不同)不过这样递推的方法已经出来了。
边界条件:第一步第二步的启示,当移动两个盘之后,a中只有一个盘的时候可以直接把a中的盘移到目的地c柱,这就相当于递归的边界,也就是当我们要移动的盘子的数目为1的时候,是可以直接从起点移动到终点的!
也就是说现在有了递推关系式!将移动三个盘子的大问题转化为了移动两个盘子的小问题!那么移动两个盘子的问题怎么解决呢?移动两个盘子的大问题肯定同样可以转化为移动一个盘子小的问题!而移动一个盘子的小问题是直接进行的!加上边界条件!OK !汉诺塔的递归模型是不是就豁然开朗了!
这里在详细讲解上图中三步的操作步骤:
a为起始柱子 b为辅助柱子 c为终点柱子
a柱子上有三个盘子要借助b柱子移动到c柱子上(移动过程中要遵循上面的规则)
那么就转化第一步:将起点a柱子上的两个盘子借助c柱子移动到终点b柱子上(第一步)
第二步:起点a柱子直接移动到终点c柱子上(图上第二步)
第三步:将起点b柱子上的两个盘子借助a柱子移动到终点c柱子上(第三步)
//汉诺塔递归求解
#include <stdio.h>
//n 代表a柱子上盘子个数, a为起始柱子,b为辅助柱子,c为目的柱子
void hanoi(int n, char a, char b, char c)
{
if( n > 0 )
{
if( n == 1 )//边界条件 当只有一个盘子的时候直接a移到c
{
printf("%c -> %c\n", a, c);
}
else
{
//对照上面图,第一步将问题分解,将起点a柱子上的盘子借助c移到b
hanoi(n-1, a, c, b);
//对照上面图,第二步直接将起点a柱子上的盘子移到C
printf("%c -> %c\n", a, c);
//对照上面的分析,第三步将起点b柱子上的两个盘子借助a柱子移到终点c柱子上
hanoi(n-1, b, a, c);
}
}
}
int main()
{
hanoi(4, 'a', 'b', 'c');
//hanoi(8, 'a', 'b', 'c');
printf("Press enter to continue ...");
getchar();
return 0;
}
运行截图:
是不是很神奇!自己可以在纸上对着移动试一试看看是不是那么回事!递归重在思想,到底适不适合用递归取决于将这个问题转变为递归的定义,对一个问题是否可以用递归思想来解决问题!
demo4: 全排列的递归解法
首先理解全排列定义:从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。当m=n时所有的排列情况叫全排列。
举个小例子:对于一个三个字符序列 a、b、c 其全排列为abc、acb、bca、bac、cab、cba六种
分析问题,建立递归思维:对于3个不同元素的全排列,我们是不是可以想象一下是不是可以通过求两个元素的全排列来求三个元素的全排列呢?然后递推可不可通过求一个元素的全排列来求两个元素的全排列呢?而一个元素的全排列很好求,就是该元素本身!也就是说一个元素的全排列就是递归的临界点(边界条件),那么现在要找的也就是递推关系式(也就说是三个元素的全排列怎么和两个元素的全排列结合在一起),那么怎么找递推关系式呢?
假设要求P(a,b,c)全排列,则只需要先把a拎出来求P(b,c) ,(a,P(b,c)),(即求a,b,c的全排列可以转化先把a提出来,来求P(b,c)的全排列)
再求(b,P(a,c)),(c,P(a,b))...
求P(b,c)的全排列可以把b先提出来求c的全排列,而c的全排列就是c本身!然后把c提出来求b的全排列!
由上面的小例子观察可以看出a、b、c的全排列中每个元素出现在第一个位置的次数是平均分配的(6/3 = 2),都为2次
//全排列递归求解
#include <stdio.h>
//三个参数 待求全排列字符数组 起始位置b,结束位置e
void permutation(char s[], int b, int e)
{
if( (0 <= b) && (b <= e) )//合法性检测
{
if( b == e )
{
printf("%s\n", s);
}
else
{
int i = 0;
//先把第一个元素拎出来,然后对剩下的元素求全排列
for(i=b; i<=e; i++)
{
//交换一下,每次把第i个元素放到第一位置,然后把第一个位置的元素放到第i个位置
char c = s[b];
s[b] = s[i];//循环需要把数组中每一个元素拎到第一个位置一次
s[i] = c;//相当于缓存拎出来的那个
//起始位置已经固定了一个元素了
permutation(s, b+1, e);//对剩下的进行全排列
//按照上面刚才的分析 一次全排列完了之后我们得到的只是部分的结果
//比如对于abc的全排列,循环一次把a拎出来 ,只求出了abc acb两种 然而还有其他的
//如上图,所以先要恢复数列保持原有的顺序然后在下一次for循环中把下一个元素拎出来
//下面的三行是交换回来 让数组中元素还是保持abc的顺序
c = s[b];
s[b] = s[i];
s[i] = c;
}
}
}
}
int main()
{
char s[] = "abcd";
permutation(s, 0, 3);
printf("Press enter to continue ...");
getchar();
return 0;
}
运行截图:
其他的情况可以自己测一测!发现如果有了递归模型思想,把思路一步一步理清楚了,写出递归算法来解决问题也没那么难!当然这个问题不是很难!不过还是学到了一种新思路!
递归与回溯
1. 递归在程序设计中也常用于需要回溯算法的场合
2. 回溯算法的基本思想:(穷举搜索算法)
1> 从问题的某一种状态出发,搜索可以到达的所有状态
2> 当某个状态到达后,可向前回退,并继续搜索其它可达状态
3> 当所有状态都达到后,回溯算法结束
3. 程序设计中可利用函数活动对象保存回溯算法的状态数据,因此可以利用递归完成回溯算法
4. 回溯算法是递归应用的重要场合,同时利用函数调用的活动对象可以保存回溯算法中重要的变量信息,递归是回溯 算法的重要实现方式!!!
demo5: 八皇后问题
问题描述:在一个8*8国际象棋盘上,有8个皇后,每个皇后占一格,要求皇后间不会出现互相“攻击”现象,即不能有任意两个皇后处在同一行、同一列或同一对角线上。求一共有多少种方法?棋盘如下图:
第一步模拟棋盘:8*8的棋盘可以用一个8*8的二维数组来模拟
第二步向棋盘上放皇后:怎么放呢?怎么放呢?
下面就来一步一步分析解决:
对于上图中的位置(i,j),问题一开始考虑皇后自上向下一行一行的放,所以对于位置(i , j)能不能放皇后只需要考虑上面的三个方向上一直到边界有没有其他皇后即可!如果三个方向上到边界都没有其他皇后,则表示(i, j)位置可以放一个皇后.反之则不行。针对图上三个方向每一个方向上的每一个位置位相对于(i, j)的偏移量分别是(-1,-1)、(-1, 0)、(-1, 1)(可以定义一个方向数组,里面只有这三个元素)。剩下的就是如何不停的去找了。
算法思路:
1. 初始化:i = 1
2. 初始化:j = 1
3. 从第i行起,恢复j的当前值,判断第j个位置
a. 位置j可放入皇后:标记位置(i,j), i++,转步骤2
b. 位置j不可放入皇后:j++,转步骤a,
c. 当j > 8时, i--,转步骤3 (回溯)
4. 结束:第8行有位置可放入皇后表示找到了一个解决方案 退出本次递归打印
直接看代码吧!
//八皇后问题递归求解
#include <stdio.h>
#define N 8
//定义一个结构体表示偏移量
typedef struct _tag_Pos
{
int ios;//i的偏移
int jos;//j的偏移
} Pos;
static char board[N+2][N+2]; //模拟8*8的棋盘加上上下左右的边界
static Pos pos[] = { {-1, -1}, {-1, 0}, {-1, 1} };//方向结构体数组
static int count = 0;//全局变量 用来统计有多少种方法
void init()//初始化棋盘
{
int i = 0;
int j = 0;
for(i=0; i<N+2; i++)//搭建棋盘边界,用“#”表示
{
board[0][i] = '#';//第0行
board[N+1][i] = '#';//第N+1行
board[i][0] = '#';
board[i][N+1] = '#';
}
for(i=1; i<=N; i++)//中间的8*8棋盘全部初始化为空格
{
for(j=1; j<=N; j++)
{
board[i][j] = ' ';//空格表示可用
}
}
}
void display()//用来打印二维数组
{
int i = 0;
int j = 1;
for(i=0; i<N+2; i++)
{
for(j=0; j<N+2; j++)
{
printf("%c", board[i][j]);
}
printf("\n");
}
}
int check(int i, int j)//如果三个方向都没有皇后就表示(i , j)这个位置可以放一个新的皇后
{
int ret = 1;
int p = 0;
for(p=0; p<3; p++)//两层循环 外层循环表示方向
{
int ni = i;
int nj = j;
//ret 为真且还没有碰到边界时就继续走
while( ret && (board[ni][nj] != '#') )//如果没有皇后就一直继续直到边界为止
{
ni = ni + pos[p].ios;
nj = nj + pos[p].jos;
ret = ret && (board[ni][nj] != '*');//这里用“*”表示皇后
}
}
return ret;
}
void find(int i)//一行一行的查找 在第i行查找
{
int j = 0;
if( i > N )//如果查找的都已经超过第八行了,说明已经找到了,递归出口
{
count++;
printf("Solution: %d\n", count);
display();
//getchar();
}
else
{
for(j=1; j<=N; j++)//第i行从第一个位置开始查找
{
if( check(i, j) )//如果当前位置可以放皇后
{
board[i][j] = '*';//那么就放一个皇后
//下面一句递归调用之后会发现 程序中运行的局部变量都被保存在函数调用的活动记录里面
find(i+1);//查找下一行
//递归结束回来之后代表位置(i, j)可能放皇后也可能不能放皇后,
//所以先清空, 继续找下一行,看影不影响下一放皇后,不影响 在放上一次的
//(比如有一种情况,位置(i,j)放了皇后之后导致下一行8个位置都不能放)
board[i][j] = ' ';
}
}
}
}
int main()
{
init();
find(1);//从第一行开始放
printf("Press enter to continue ...");
getchar();
return 0;
}
运行截图:
最后自己可以随便挑选其中的几种在纸上画画对照着看看是不是!递归,简约而不简单!
花了两个下班后的业余时间将之前学的递归知识重新温习一遍发现还是学到了不少东西!虽然很费时间花了不下5个小时了,但是自己对递归的理解也进一步加深了!今天我离大神又近了一步!