讲解之间我们先回顾下递归的知识点:
1、什么是递归?
程序调用自身的编程技巧称为递归。(即一个函数在其定义中有直接或间接调用自身的一种方法)
2、递归的思想
递归的主要思考方式在于:把大事化小
它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
3、递归的作用
递归策略,只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
4、递归的理解
递归要分开:递 --递推,归 --回归。先递推后回归
5、递归的两个必要条件(与if语句搭配)
1、存在限制条件,当满足这个限制条件的时候,递归便不继续(递归出口)
2、每次递归调用之后越来越接近这个限制条件(即:将问题转化为其子问题,子问题要与原问题具有相同的解法)
注:递归没有这两个条件一定是错的,但是有也不一定对
递归虽然有优点但是也有缺点。
6、递归存在的问题:
1、递归层次太深时,那就会报错:stack overflow(栈溢出)这样的消息。系统分配给程序的栈空间是有限的,但是如果出现死递归或者死循环,这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
2、递归的过程中存在大量的重复计算(效率低)
那如何解决递归存在的问题:
1、将递归改写成迭代(非递归)。(循环就是一种迭代,迭代可以理解成循环)
2、使用static对象替代nostatic局部对象。(static是存放在静态区的)
接下来我们正式开始讲解递归的经典例题:
一、选择题
1、关于递归的描述错误的是:( )
A.存在限制条件,当满足这个限制条件的时候,递归便不再继续
B.每次递归调用之后越来越接近这个限制条件
C.递归可以无限递归下去
D.递归层次太深,会出现栈溢出现象
答案解析:
答案;C
A:正确,限制条件即递归的出口,如果限制条件满足,递归程序就可以退出了
B:正确,因为每次递归,都是将原问题进一步缩小,缩小到限制条件时,就可以往回返,直到第一次递归调用
比如递归顺序打印1234中的每一个数
C:错误,递归不能无限递归下去,否则会造成死循环和栈溢出
D:正确,因为每次递归,相当于都是一次新的函数调用,而每次函数调用系统必须给该函数划分栈帧空间,内部的递归函数没有退出,上层的递归就不能退出,栈帧就会累积许多块,如果累积超过栈的总大小,就会栈溢出。
2、关于函数的声明和定义说法正确的是:( )
A.函数的定义必须放在函数的使用之前
B.函数必须保证先声明后使用
C.函数定义在使用之后,也可以不声明
D.函数的声明就是说明函数是怎么实现的
答案解析:
答案:B
A:错误,函数的定义可以放在任意位置,函数的声明必须放在函数的使用之前
B:正确
C:错误,函数定义在使用之后,使用之前没有声明时,编译器编译时识别不了该函数
D:错误,函数的声明只是告诉编译器函数返回值类型、函数名字以及函数所需要的参数,函数定义才是说明函数是怎么实现的
知识点:
1、函数的声明
①告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。(声明只是告诉你有,但是真的有没有取决于定义)
②声明的一般形式如下:
a.返回类型 函数名(形参列表);如:int Add(int x,int y);
或
b.返回类型 函数名(形参类型列表);如:int Add(int,int);
好的风格:建议使用a格式。
③注:函数的声明以“;”结束,不能省略。
④函数的声明一般出现在函数的使用之前,要满足先声明后使用。
⑤函数的声明一般放在头文件中的
2、函数的定义
①函数的定义是指函数的具体实现,交代函数的功能实现
②函数的定义也是一种特殊的声明
③函数的定义一般放在源文件中
3、函数为什么要声明?(如生活中你使用别人的东西,你要打招呼)
①代码在编译的时候,要进行代码的扫描,是顺序从前往后扫描的。
②当函数定义出现在函数调用之后时,在主调函数前,采用函数原型对被调函数进行声明。(注:不声明,编译器会警告)
③函数定义出现在函数调用之前,可不用进行函数声明
二、编程题
1、递归实现n的k次方
编写一个函数实现n的k次方,使用递归实现。(只考虑n,k为整数的情况)
知识点:次方
1、最基本的定义:设n为任意数,k为正整数,n的k次方表示为n^k,表示k个n连乘。
2、0与正整数次方:
①一个数的零次方:任何非零数的0次方都等于1。
②0的次方:0的任何非0次方都是0;注:0的0次方无意义。
3、扩展:
①负整数次方一个非零数的-k次方=这个数的倒数的k次方(即n^-k=1.0/n^k)
分析:
1、递归出口:k==0 n^0=1
2、每次递归调用之后越来越接近这个递归出口:
①k为正整数:n的k次方:就是k个n连乘,每乘一个n,k就减1,
即n*Pow(n,k-1)
②k为负整数:n的k次方:就是这个数的倒数的-k次方,(转换为正整数次方)
即1.0/Pow(n,-k)
3、函数返回类型:double
4、图解
代码实例:
#include<stdio.h> //自定义函数——递归实现n的k次方 double Pow(int n, int k) { if (0 == k) { return 1.0; }//递归出口 else if (k > 0) { return n * Pow(n, k - 1);//k-1,每次递归调用后,接近出口 }//k>0的情况 else { return 1.0 / Pow(n, -k);//-k转化为求正整数次方 }//k<0的情况 } int main() { int n = 0; int k = 0; //输入 scanf("%d %d", &n, &k); //调用函数实现n的k次方 double ret = Pow(n, k); //输出 printf("n^k=%lf\n", ret); return 0; }
2、计算一个数的每位之和(递归实现)
写一个递归函数DigitSum(n),输入一个非负整数,返回组成它的数字之和
例如,调用DigitSum(1729),则应该返回1+7+2+9,它的和是19
输入:1729,输出:19
分析:
1、递归出口:n<10 n
2、每次递归调用之后越来越接近这个递归出口:
n%10+digitSum(n/10)(第n位+前n-1位之和)
3、图解
代码实例:
#include<stdio.h> //自定义函数——计算一个数的每位之和(递归实现) int DigitSum(int n) { if (n < 10) { return n; }//递归出口 else { return n % 10 + DigitSum(n / 10);//第n位+前n-1位之和 }//n>10 } int main() { unsigned int n = 0; //输入 scanf("%u", &n); //调用函数实现计算一个数的每位之和 int ret = DigitSum(n); //输出 printf("%u\n", ret); return 0; }
3、strlen的模拟(递归实现)
递归和非递归分别实现strlen
知识点:
1、strlen:是统计字符串长度的,统计的是‘\0’之前出现的字符个数
代码1:递归模拟实现strlen
分析:
1、递归出口:*str=='\0' return 0;
2、每次递归调用之后越来越接近这个递归出口:
my_strlen(str+1)+1;//+1向后跳一个字符
3、图解
知识点:
1、指针类型:
①指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)
②指针的类型决定指针向前或者向后走一步有多大距离(步长)
#include<stdio.h> #include<string.h> //自定义函数——递归模拟实现strlen int my_strlen(char* str) { if (*str == '\0') { return 0; }//递归出口 else { return 1 + my_strlen(str + 1);//每加1,str向后跳一个字符 } } int main() { char arr[10] = { '\0' }; //输入 gets(arr); //调用函数计算arr的长度 int ret = my_strlen(arr); //输出 printf("%d\n", ret); return 0; }
代码2:迭代模拟实现strlen
分析:循环实现
1、定义一个计数器,用来统计字符串的长度
2、遍历字符串,遇到一个字符计数器就+1,直到遇到‘\0’结束。
#include<stdio.h> //自定义函数——迭代模拟实现strlen int my_strlen(char* str) { int i = 0;//计数器 for (; *str != '\0'; str++) { i++;//遇到一个字符就+1 } return i; } int main() { char arr[10] = { '\0' }; //输入 gets(arr); //调用函数计算arr的长度 int ret = my_strlen(arr); //输出 printf("%d\n", ret); return 0; }