第一个问题是猴子吃桃问题,记得当时刚学c语言的时候自己递了一个多小时才归出来。
貌似是个基础题。题目大意是猴子摘了一堆桃子,每天吃掉一半又多吃一个,到了第十天,就剩下一个了,问这堆桃子最初有几个。
我们先列个表,看看有什么规律,由于我们算最后的总数,所以就列一下剩余桃子的数量吧。假设最初的数量是A 则 第一天剩余 0.5*A-1 …… 第十天为 1 。 这就是说
总数 = 2 *(第一天剩余数 + 1)
到最后一天时,只剩下一只桃子。
这样,我们找到了前后两天的关系,同时,也知道了结束条件,现在我们需要了解的就是到底如何递归。所谓“递归”,就是“递”+“归”, “递”就是层层递进,“归”则是归纳结果,可见,这是一个进出的关系,递进去,归出来。既然要归出来,肯定需要有个结束条件,要不了就一直归下去不出来了。至于如何往进递,这就要靠这前后两天的关系了。
当然,递进是需要通过函数的不断调用自己来进行,一旦满足某个条件,函数不调用自己了,也就不递了,而是往出归纳,从最里面的函数返回最外面的函数,将最终结果归纳出来。同时,这个过程是严格遵循函数顺序执行过程的,所以我们在设计函数的时候就要注意如何安排顺序才能恰好按规律完成任务。这个问题其实只需要一递到底,然后逐层返回(归纳),就可以方便的解决。(本人考虑了将近2个小时终于解决)另外,我将最近见到的几个问题大概分为三类,即先递后归,边递边归,连串递归。前两种比较简单,第三种有点麻烦,本题就是先递后归的类型。
以下是源代码:
#include<stdio.h>
int left(int day)
{
int totle;
if(day == 10)
return 1;
else if(day < 10)
{
day++;
totle = (left(day)+1) * 2;
return totle;
}
}
int main()
{
printf("\n");
printf("%d\n",left(1));
}
类似问题还有一个输入字符串,然后反向输出。 也就是说递归可以实现一部分栈的功能,即先进后出。也就是说当我们一递到底之后,最后一个输入的东西(最后一个递到的函数),被最先输出。
关于函数的实现,直观上讲,可以设置一个要求,如果不满足,就一直调用自己,一旦满足,就不再调用自己而开始输出。
源代码如下:
#include<stdio.h>
#include<math.h>
#include<string.h>
int print()
{
char c;
scanf("%c",&c);
if(c != '#')
{
print();
}
printf("%c",c);
return 1;
}
int main()
{
print();
}
我们需要明确一下,递归的时候,每次调用一个函数,计算机都会为这个函数分配新的空间,这就是说,当被调函数返回的时候,调用函数中的变量依然会保持原先的值,否则也不可能实现反向输出。
与之类似,递归将二进制转换为十进制也是这个道理
以下是源码:
#include<stdio.h>
//#include<math.h>
#include<string.h>
int powa(int a, int n)
{
int i,sum = 1;
if(n != 0)
{
for(i=1; i<=n; i++)
{
sum = sum * a;
}
return sum;
}
else if(n == 0)
return 1;
}
int sum;
int bitode(int n)
{
int num;
if(n != 0)
{
scanf("%d",&num);
//bitode(--n);
//bitode(n--);
bitode(n-1);
}
if(n == 0)
{
sum = 0;
return sum;
}
if(n != 0)
{
sum = num * powa(2,n-1) + sum;
}
}
int main()
{
int number;
printf("enter the totle number \n");
scanf("%d",&number);
bitode(number);
printf("%d \n",sum);
//printf("%d\n",powa(2,3));
}
问题在这里:
if(n != 0)
{
scanf("%d",&num);
//bitode(--n);
//bitode(n--);
bitode(n-1);
}
关于以下三种迭代方式,第一种输出结果会缺少最高位,调试会发现当递归到n=0时,递归条件结束,再次进入n=0 。第二种程序无法递归,调试发现n的值始终不会变化,即无法满足结束条件。第三种方式是正确的。
我们先分析第一种情况,缺位的原因是在递归条件结束时,又一次进入相同的条件,即两次进入n=0的条件。 我们知道 -–n的作用是先使n自减,在使用n的值。同时,我们知道递归时每次调用自身,内存会为被调函数分配新的空间,而调用函数的变量仍然保存,如果程序再次返回调用程序,变量依然有效。 可见,此时调用函数中n已被自减,自减后的值再传入被调函数,这就是说当返回到调用函数时,此时的n以不是最初n的值,而变为n-1。这样,程序本该返回1,结果又成了0。 这样返回每次少一位,到最后高位就没了。
关于第二种情况,n- -,在调用时是用n,然后执行自减,但是在递归时,在被调函数中,传入的就是n的值,在调试过程中,n的值始终没有变化,永远无法满足返回条件。
只有第三种情况,n-1作为参数,传入正确的值,同时又保护了调用函数本身参数的值,这样就可以正确递归了。
现在我们来讨论一下边递边归的情况。其实这种问题思路相对更为简单,就是每次递进产生结果,单满足递归结束条件后直接退出函数。
题目要求将一个数字运用递归的方式来将一个数字分解质因数。
思路大概就是用这个数字对从2开始,到这个数字乘以0.5的范围内的数字,从小到大取余数,一旦对一个数字取余为0,就将之除以此数,然后看看分解出来的数字是否为奇数,如果是则递归结束。中间过程只要打印每次的除数即可。
从递归的角度看,前后两次都是做取余运算,函数可以复用,同时有一个检测条件,满足后递归返回。(当然什么也不返回,所有的结果都是在递进的时候得出的,所以称之为边递边归)
以下是源代码:
#include<stdio.h>
int arry[10];
int k = 0;
int isprime(int num)
{
int i;
for(i=2; i<=num-1; i++)
if(num % i == 0)
return 0;
return 1;
}
int primefactor(int num)
{
int i;
if(isprime(num))
{
//arry[k] = num;
printf("%d",num);
return 1;
}
else
{
for(i=2; i<=num-1; i++)
{
if(num%i == 0)
{
//arry[k++] = i;
printf("%d ",i);
num = num/i;
if(isprime(num))
{
//arry[k] = num;
printf("%d ",num);
return 1;
}
else
primefactor(num);
return 1;//如果去掉return 结果该如何解释
}
}
}
return 1;
}
int main()
{
int i,num;
scanf("%d",&num);
primefactor(num);
/*for(i=0; i<10; i++)
printf(" %d",arry[i]);*/
}
见原文注释行,一个关键的return。注意啦,我们要做的是找到最小到的可以分解num的素数,然后再用同样的方式分解上一次分解出的值,即一递到底。如果缺少这个return,函数将再次进入循环,这样就不是“一递到底”了。
连串递归(感觉没有抓住重点,姑且先这么称呼)
额,这就是递归中经典的经典问题,凡是讲c语言的书都讲,凡是讲我基本都没搞明白的汉诺塔问题了,上中学的时候,写作业时文曲星里面的这个游戏,感觉特别乱是真的。
遇到这种复杂的问题,先得将它简单化。就像红军打仗的时候,每场战役都是敌众我寡,但是每场战斗却基本是我众敌寡,这其中的关键就是如何分割敌人。
设想一下,有一个盘子,我们直接把它从第一个柱子移到第三个。如果有两个呢,必须将上面的盘子移到第二个柱子,才能将下面的移到第三个柱子。设想一下,如果第二个柱子上此时不止有一个盘子,而是落了一堆呢……貌似略显凌乱。不过不要紧,我们就把此时第二个柱子上当成一个就好。
这时候第一个柱子上那一片就可以轻松移到第三个上了。
第二个柱子上此时还有一个片,我们直接将它移到第三个柱子上。这样,通过第二个柱子,我们完成两个片的转移。试想,如果此时第二个柱子上不是一个片,而是两个呢?我们没办法直接将两个片放在第三个柱子上,只能先将上面的小的放在第一个柱子,然后移动第二个柱子上的片到第三个柱子,在移动第一个柱子上的到第三个上。这样,通过借助第一个柱子,我们将第二个柱子上的片片移到第三个上。(如果凌乱的化需要在纸上画一下,这是一个很清晰的过程)
此时,如果第二个上真的有一堆片片呢?是不是都在重复这个借助第一个,移到第三个的动作呢?如果觉得一堆乱的化,就当成两个吧,一个是第二个柱子上最下面的,一个是它上面的那一堆。而我们要打印的,其实就是把下面的那个移到第三个,当然,过程中又会包含有其他相同的过程。
写的一堆什么狗屁,直接上代码吧,以三个片为例,用一个数组打印出移动过程。
程序中用一维数组完成了部分二维数组的功能,这是去年做单片机点阵俄罗斯方块的时候发现的,这样做有时候会简化程序。
#include<stdio.h>
#include<math.h>
int tower_arry[9];
void init_tower(int n,int *arry) //init the first tower
{
int i;
for(i=0; i<n; i++)
{
if(i%3 != 0)
arry[i] = 0;
else
arry[i] = i/3 + 1;
}
for(i=0; i<n; i++ )
{
if(i%3 == 0)
printf("\n");
printf("%d ",arry[i]);
}
printf("\n");
}
void change_tower(int n ,int *arry, char start, char end)
{
int i,j,tmp;
//***********************change the tower********************
for(i=start-97; i<n; i+=3)// from top to botton find the first one
{
if(arry[i] != 0)
{
tmp = arry[i];
arry[i] = 0;
break;
}
}
for(i=n-(2 - (end-97))-1; i>=0; i-=3)//form botton to top find the first 0
{
if(arry[i] == 0)
{
arry[i] = tmp;
break;
}
}
//**********************display the new tower*******************
for(i=0; i<n; i++ )
{
if(i%3 == 0)
printf("\n");
printf("%d ",arry[i]);
}
printf("\n");
}
void move(int n, char a, char b, char c, int *arry)
{
if(n == 1)
{
printf("%c->%c\n",a,c);
change_tower(9,arry,a,c);
printf("\n");
return;
}
else
{
move(n-1,a,c,b,arry);
printf("%c->%c\n",a,c);
change_tower(9,arry,a,c);
printf("\n");
move(n-1,b,a,c,arry);
}
}
int main()
{
init_tower(9,tower_arry);
printf("\n");
move(3,'a','b','c',tower_arry);
}
总之,这样的问题最好是把它拆分成小的,由最简单的,逐步到复杂的,理清关系。另外,我发现把递归的过程打印出来也是一件很有趣的事。
递归与二叉树
理解了递归汉诺塔,再理解二叉树,就很容易了。记得考计算机二级的时候,基础题总喜欢问什么前序遍历,中序遍历,后序遍历,当时总是不明白这是在说什么。
当学完汉诺塔,其实我发现这个东西和二叉树是很相似的。
从宏观上看都是先做最小的工作,然后按照同样的顺序复制这个工作,在复制的时候,又需要从最小的方面开始。这样由小到大,汉诺塔是移动的层数不断增加,二叉树是子树不断增大,直到最终完成工作。
从微观上看,汉诺塔开始递归时,层层深入,最终步骤会落到“移动一个盘子”这一步骤,然后不断移动后面的盘子。同样,二叉树也是层层深入,最终步骤会精确到操作一个结点,然后再按照同样的规则(顺序)操作更多的结点。
再看具体做法:
二叉树的创建:
void creat_tree(BiTNode **root , ELEMENTTYPE (*enter)())
{
int num;
num = enter();
if(num == 0)
*root = NULL;//初始化头结点指针
else
{
*root = (BiTNode *)malloc(sizeof(BiTNode));
(*root)->data = num;
creat_tree(&((*root)->node[0]), enter);//注意理解参数 地址的地址 就是对地址(一个指针)再取地址
creat_tree(&((*root)->node[1]), enter);
}
}
递归地创建一颗二叉树。使用的是先序,就是从上到下,从左到右依次创建子树。
遍历方法:
先序:从上到下 从左到右 依次遍历
int pre_order_traveltree(BiTNode **root , void (* visit)(ELEMENTTYPE ))
{
if(*root != NULL)
{
visit((*root)->data);
pre_order_traveltree(&((*root)->node[0]), visit);
pre_order_traveltree(&((*root)->node[1]), visit);
}
else
return 1;
}
中序:从最左下角的叶子结点开始,到最左下角的根结点,到最左下角根节点的右子树。之后我们
可以把这三个结点看做一个节点(就像前文中说的把汉诺塔的一堆盘子看做一个盘子),按照开始的顺序,将三个结点中根节点的根节点看做一个“总根节点”,将根节点的另外一个子树连同它下面的两个结点看做一个大的节点。 这样,按照之前的顺序,我们将访问这个“总根节点”,访问完成总根节点之后,我们又会访问它的右子树。当然,还是按照刚才的顺序,从最左下角的结点开始,到最左下角的根结点,到最左下角根节点的右子树,这样,最左边的下三层就解决完毕了。
而我们将会根据这个顺序,将整棵树遍历。
int in_order_traveltree(BiTNode **root , void (* visit)(ELEMENTTYPE ))
{
if(*root != NULL)
{
in_order_traveltree(&((*root)->node[0]), visit);
visit((*root)->data);
in_order_traveltree(&((*root)->node[1]), visit);
}
else
return 1;
}
后序:从最左下角的叶子结点开始,到它右边的与它同根节点的叶子结点,到他俩共同的根节点。
int pos_order_traveltree(BiTNode **root , void (* visit)(ELEMENTTYPE ))
{
if(*root != NULL)
{
pos_order_traveltree(&((*root)->node[0]), visit);
pos_order_traveltree(&((*root)->node[1]), visit);
visit((*root)->data);
}
else
return 1;
}
关于二叉树递归应用的实例:
这是一个判断完全二叉树的问题。其中遍历二叉树用于判断的一个实例
int is_totle_bitree_senior(BiTNode **root, int deep)
{
static flag[2] = {0,0}; //如果检测到有缺树枝的情况,将标志位置位
static flag_return = 0; //如果确定其非二叉树,将此标志置位
if(flag_return == 0)
{
if(deep == 1) //当递归到最底层时返回1,即不做任何其他操作
return 1;
/*if(deep == 2 || deep == 3)*/
if(deep != 1) //在深度不为1的情况下判断节点有没有缺树枝
{
if((*root)->next[0] == NULL)//如果有缺,则将左右节点标志位分别置1
flag[0] = 1;
if((*root)->next[1] == NULL)
flag[1] = 1;
}
//当大于等于3时(假设深度越往下越低),只要是有缺树枝的,必然不是完全二叉树(貌似不一定需要前面有缺树枝这一条件 : (flag[0] == 1 || flag[1] == 1) )
if(deep >= 3 && (flag[0] == 1 || flag[1] == 1))//the botton root(2) loss left or right affect the deep 3
{
if((*root)->next[0] == NULL || (*root)->next[1] == NULL)//deep 3 must have full node to ensure totle tree
//return 0;
flag_return = 1;
}
//当深度为2且前面有缺少树枝时,如果有非空,则必然不是完全二叉树
, else if(deep == 2 && (flag[0] == 1 || flag[1] == 1))//the bottom root(2) loss left or right affect the deep 2
{
if((*root)->next[0] != NULL || (*root)->next[1] != NULL)//deep 2 must have no node to ensure totle tree
//return 0;
flag_return = 1;
}
}
if(flag_return == 1)
{
return 10;// if not a totle tree return 10 假设函数返回10的时候,就可判断不是完全二叉树
}
else//递归到下一级
{
is_totle_bitree_senior(&((*root)->next[0]), deep-1);
is_totle_bitree_senior(&((*root)->next[1]), deep-1);
}
}
此处使用了先序遍历,遍历到了每一层都做一个判断。前边大段的代码都是用作判断,如果判断符合要求,程序将返回一个特定值,表明此二叉树非完全。此处用静态变量定义标志位,用于保存标志位用于递归嵌套不被改变。 当flag_return值置位时,将不再进行递归。