The best time to plant a tree was 10 years ago. The second best time is now
翻译:种一棵树最好的时间是十年前,其次是现在
所谓运气,不过是机会碰巧遇到了你的努力
本章内容是对递归详细讲解,中间穿插了较多的递归经典案例,方便我们理解递归的思想以及使用递归去解决实际的问题。
什么是递归:程序调用自身的编程技巧称为递归(recursion)。递归做为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于 : 把大事化小
注意:函数在调用的时候会向内存申请空间。(递归的过程就是函数的不停调用过程)
最简单的递归函数:主函数调用主函数
#include
int main()
{
printf("hello world\n");
main();
return 0;
}
上述代码结果:死循环,栈溢出(stack overflow)
为什么会出现栈溢出呢?
要想搞懂栈溢出的原因,首先我们要明白程序运行过程中内存的划分分区
每一次函数的调用,都需要在栈区分配一定的空间(也就是说函数调用也在栈区开辟空间),调用次数太多,栈空间不够分配(被耗干),导致栈溢出。
接受一个整型值(无符号),按照顺序打印它的每一位。例如︰输入∶1234,输出1234.
要顺序打印它的每一位,就需要得到它的每一位,1234最容易得到的就是个位
1234 % 10 = 4
1234 / 10 = 123 % 10 = 3
123 / 10 = 12 % 10 = 2
12 / 10 = 1 % 10 = 1
1 / 10 = 0
#include
void print(int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf("%d", &num);//1234
print(num);//打印1 2 3 4
//print(1234)
//print(123) 4
//print(12) 3 4
//print(1) 2 3 4
// 1 2 3 4
//当()里面的数字大于9的时候就拆分,小于9,为个位数的时候停止拆分,进行打印
return 0;
}
比较上面两个递归函数,我们可以看到: 递归的两个必要条件
①存在限制条件,当满足这个限制条件的时候,递归便不再继续。
②每次递归调用之后越来越接近这个限制条件。
注意:这两个条件是必要条件,不是充分条件,也就是说递归函数一定满足这两个条件,但是满足这两个条件不一定是递归。看下面这个例子:
按F10进行调试:
每一次函数的调用,都需要在栈区分配一定的空间,调用次数太多,栈空间不够分配(被耗干),导致栈溢出。
所以我们在写递归代码的时候,一定要注意以下几点:
1、不能死递归,要有跳出条件,每次递归逼近跳出条件
2、递归层层不能太深
编写函数不允许创建临时变量,求字符串的长度。
初步解题思路:
#include
#include
//求字符串长度
int my_strlen(char* pr)
{
int count = 0;
while (*pr != '\0')
{
count++;
pr++;
}
return count;
}
int main()
{
char arr1[] = "bit";
//int len = strlen(arr1);//求字符串长度
int len = my_strlen(arr1);
//arr1是数组,数组传参,传过去的不是整个数组,而是首元素的地址
printf("%d\n", len);
return 0;
}
这种方式虽然计算出字符串的长度,但是创建了一个临时变量count,现在使用递归的方式来实现:
int my_strlen(char* pr)
{
if (*pr != '\0')
return 1 + my_strlen(pr + 1);
else
return 0;
}
//把大事化小
//my_strlen("bit")
//1+my_strlen("it")
//1+1+my_strlen("t")
//1+1+1+my_strlen("\0")
//1+1+1+0 = 3
求n的阶乘。(不考虑溢出)
#include
#include
int Fun1(int x)//迭代(循环)实现
{
int i = 0;
int y = 1;
for (i = 1; i <= x; i++)
{
y = y * i;
}
return y;
}
int Fun2(int x)//递归实现
{
if (x > 1)
return x * Fun2(x - 1);
else
return 1;
}
int main()
{
int n = 0;
int ret = 0;
printf("请输入一个数:>>\n");
scanf("%d", &n);
//ret = Fun1(n);//循环的方式
ret = Fun2(n);//递归的方式
printf("%d的阶乘为:%d\n", n, ret);
return 0;
}
求第n个斐波那契数。(不考虑溢出)
#include
int count = 0;
int Fib1(int x)//递归实现
{
if (x == 3)
count++;
if (x <= 2)
return 1;
else
return Fib1(x - 1) + Fib1(x - 2);
}
int Fib2(int x)//函数实现
{
int a = 1;
int b = 1;
int c = 1;
while (x > 2)
{
c = a + b;
a = b;
b = c;
x--;
count++;
}
return c;
}
int main()
{
int n = 0;
int ret = 0;
scanf("%d", &n);
//ret = Fib1(n);//求第n个斐波那契数
ret = Fib2(n);//循环实现
printf("%d\n", ret);
printf("count = %d\n", count);
return 0;
}
我们在主函数里面写后续要被调用的某个函数的时候,先假设要用这个函数实现什么功能,直接去用,之后再去真正定义并实现这个函数。
这种思想叫做:TDD - 测试驱动开发 先去想函数怎么用,然后再实现。
可以看出此时使用递归方式并不高效,其计算同一个数比如 3 的次数为 2 ^ n (递归方式)
而使用循环方式(n > 3时) 次数为 n - 2
那我们如何改进呢 ?
在调试factorial函数的时候,如果你的参数比较大,那就会报错 : stack overflow(栈溢出)这样的信息。系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。那如何解决上述的问题 ?
1.将递归改写成非递归。
⒉使用static对象替代nonstatic局部对象。在递归函数设计中,可以使用static对象替代nonstatic局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
提示∶
1.许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 但是这些问题的迭代实现往往比递归H实现效率更高,虽然代码的可读性稍微差些。
3.当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
编写一个函数 reverse_string(char* string)(递归实现)
实现:将参数字符串中的字符反向排列,不是逆序打印。
要求:不能使用C函数库中的字符串操作函数。
比如 : char arr[] = “abcdef”,逆序之后数组的内容变成:fedcba
#include
//#include
//int my_strlen(char* str)//非递归方法求字符串长度
//{
// int count = 0;
// while (*str != '\0')
// {
// count++;
// str++;
// }
// return count;
//}
int my_strlen(char* str)//递归方法求字符串长度
{
if (*str != '\0')
return 1 + my_strlen(str + 1);
else
return 0;
}
//void reverse_string(char* str)//非递归方法逆序字符串
//{
// //int len = strlen(str);
// int len = my_strlen(str);
// int left = 0;
// int right = len - 1;
// while (left < right)
// {
// char tmp = *(str+left);
// *(str + left) = *(str + right);
// *(str + right) = tmp;
// left++;
// right--;
// }
//
//}
void reverse_string(char* str)
{
int len = my_strlen(str);
if (len > 1)
{
char tmp = *str;
*str = *(str + len - 1);
*(str + len - 1) = '\0';
reverse_string(str + 1);
*(str + len - 1) = tmp;
}
}
int main()
{
char arr[] = "abcdefjh";
printf("逆序前:%s\n", arr);
reverse_string(arr);
printf("逆序后:%s\n", arr);
return 0;
}
写一个递归函数DigitSum(n),输入一个非负整数,返回组成它的数字之和
例如,调用DigitSum(1729),则应该返回1 + 7 + 2 + 9,它的和是19
例如输入:1729,输出:19
思路:大事化小,个位数的9最容易拿下来
#include
int DigitSum(int n)
{
if (n > 9)
{
return DigitSum(n / 10) + n % 10;
}
else
{
return n;
}
}
int main()
{
int num = 1729;
int sum = DigitSum(num);
printf("%d\n", sum);
return 0;
}
编写一个函数实现n的k次方,使用递归实现。
#include
double my_pow(int n, int k)
{
if (k > 0)
{
return n * my_pow(n, k - 1);
}
else if (k == 0)
{
return 1;
}
else
{
return 1.0 / my_pow(n, -k);
}
}
int main()
{
int n = 0;
int k = 0;
double ret = 0.0;
scanf("%d%d", &n, &k);
ret = my_pow(n, k);
printf("%lf\n", ret);
return 0;
}
递归的扩展:
经典题目:
1、汉诺塔问题
2、青蛙跳台阶问题
下篇关于递归进阶的文章会详细讲到这两个题目的思路+编程代码。