在递归之前,我们先补充一下关于栈的知识
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("%p\n", &a);
printf("%p\n", &b);
return 0;
}
我们观察到在64位环境下,该程序先创建变量a,然后再创建变量b,在该环境下,栈的使用先使用低地址再使用高地址
但是,如果我们换成x86环境,即32位环境下,与64位环境的使用恰好是相反的:
栈区空间,默认是先使用高地址,再使用低地址的,具体看实现。
为了比较两个同一环境的不同结果,我们再提出一个例子:
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
32位环境下,该程序在debug下死循环打印。
32位环境下,该程序在release下正常运行。
。
好的,到这里我们总结一下debug和release
Debug通常称为调试版本,它包含调试信息,并且不会作任何优化,便于程序员调试程序:
程序员在写代码的时候,需要经常性的调试代码,就将这里设置为debug,这样编译产生的是debug版本的可执行程序,其中包含调试信息,是可以直接调试的。
Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。当程序员写完代码,测试再对程序进行测试,直到程序的质量交付给用户使用的标准,这个时候就会设置为release,编译产生的就是release版本的可执行程序,这个版本是用户使用的,无需包含调试信息等。
·
比较一下两个文件大小,哈哈,小的才是大的零头,看见悬殊了吧。
此时看在realease和32位环境下
优化的时候i的地址已经小于arr的地址了,
在realease环境下,运行该程序的结果
数组在栈中存储,随着下标增长,地址由低到高变化
所以在debug环境下,进行数组越界有可能覆盖到i的
再一次验证了栈区空间,默认是先使用高地址,再使用低地址的,具体看实现
一般在x64或者release下会发生变化。
接下来我们正式开始递归:
递归是什么?
递归是学习C语言函数绕不开的一个话题,那么什么是递归呢?
递归其实是一种解决问题的办法,在C语言中,递归就是函数自己调用自己。
废话不多说,举一个简单的例子
#include <stdio.h>
int main()
{
printf("hehe\n");
main();
return 0;
}
这是死递归,会无限递归下去
我们调试该代码,会弹出错误信息
Stack overflow 栈溢出
每一次函数调用,都要向栈区申请一块内存空间
一直进行函数调用时,一直需要向栈区申请空间,就这样无限的申请之后,栈空间最后会被放满,溢出,从而导致栈溢出现象
因此代码是不能在这里无限递归下去的
上述就是递归的形式
递归的思想:
把一个大型复杂问题层层转化为一个与原问题相似,但规模较小的子问题来求解;直到子问题不能再被拆分,递归就结束了,所以递归的思考方式就是把大事化小的过程。
递归中的递就是递推的意思,归就是回归的意思。
递归的限制条件:
递归在书写的时候,有两个必要条件:
递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件。
渐渐地接近并停下来
递归举例
1.求n的阶乘
一个正整数的阶乘是所有小于以及等于该数的正整数的积,并且0的阶乘为1.
自然数n的阶乘写作n!。
我们接下来先举一个具体的数:
3!=1*2*3
3!=3*2*1=3*2!
2!= 2*1=2*1!
1!= 1
n!=n*(n-1) , n>0
1 , n=0
接下来我们实现这个代码:
#include <stdio.h>
Fact(int n)
{
if (n == 0)
return 1;
else
return n * Fact(n - 1);
}
int main()
{
int n;
scanf("%d", &n);
int ret = Fact(n);
printf("%d\n", ret);
return 0;
}
代码运行结果:
接下来我们看一下递归的流程
2.顺序打印一个整数的每一位
正序打印,是不是与你之前做的不一样
1234中最容易得到的就是4
1234%10=4
1234/10=123
123%10=12
123/10=12
12%10=2
12/10=1
1%10=1
1/10=0
我们先做容易的
#include <stdio.h>
int main()
{
int n;
scanf("%d", &n);
while (n)
{
printf("%d ", n % 10);
n = n / 10;
}
return 0;
}
打印1234的每一位-----》1.打印123(1234/10)的每一位----->打印12的每一位----->打印1
2.打印4(1234%10) 打印3(123%10)--->打印2
#include <stdio.h>
void Print(int n)
{
if (n > 9)
{
Print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
int n;
scanf("%d", &n);
Print(n);
return 0;
}
每一次函数调用都会有自己的函数栈帧空间
递归看起来太抽象了
递归只有少量的代码,却完成了大量的运算。
截取一位优秀同学的问题:
死递归函数调用好多次后会导致栈溢出但是里面没创建任何变量,是说明函数每次调用至少会申请一定的空间吗?它申请的空间大小是怎么来的
每一次函数调用,都会向内存栈区上申请一块空间
这一块空间主要是用来存放函数中的局部变量,和函数调用过程中的上下文的信息,这个一块空间一般叫:函数的运行时堆栈,也叫函数栈帧空间。
编译会自动根据需要开辟空间的!
空间调用结束后会进行回收
Fact函数是可以产生正确的结果,但是在递归函数调用的过程中涉及一些运行时的开销。
在C语言中每一次函数调用,都要需要为本次函数调用在栈区申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。
函数不返回,函数对应的栈帧空间就一直占用,所以如果函数调用中存在递归调用的话,每一次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出。
求n的阶乘的非递归版本:
#include <stdio.h>
int Fact(int n)
{
int i = 1;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret = ret * i;
}
return ret;
}
int main()
{
int n;
scanf("%d", &n);
int ret = Fact(n);
printf("%d\n", ret);
return 0;
}
循环就是一种迭代
事实上,我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加清晰,但是这些问题的迭代实现往往比递归实现效率更高。
3.求第n个斐波那契数
斐波那契数列
1 1 2 3 5 8 13 21 34 55,,,,,,
1 2 3 4 5 6 7 8 9 10......
#include <stdio.h>
int Fib(int n)
{
if (n <= 2)
return 1;
else
return Fib(n-1) + Fib(n - 2);
}
int main()
{
int n;
scanf("%d", &n);
int ret = Fib(n);
printf("%d", ret);
return 0;
}
但是当我们输入的值想要更大的时候,它不再能算出来
此时我们发现结果出不来我们可以打开任务管理器看一下cpu的使用情况
因此菲波那契数列并不适合用递归的方法来解决
因为最高递归才50层,并且先算完左边再算右边,因此并不会导致栈溢出,但是效率特别低。
非递归方式解决:
前两个数都不用算都为1,所以想要求第n个数时,就要求n-2次。
#include <stdio.h>
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while (n >= 3)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
此时结果能出来,int是有限的,所以会是个负数
比起递归效率高多了
递归和迭代的选择
1.如果使用递归写代码,非常容易,写出的代码没问题,那就使用递归
2.如果使用递归写成的问题,是存在明显的缺陷,那就不能使用递归,得用迭代的方式处理!
3.后期在学习数据结构的时候,会经常使用递归。
递归可能会遇到的情况
#include <stdio.h>
void test(int n)
{
if (n <= 10000)
test(n + 1);
}
int main()
{
test(1);
return 0;
}
点到为止,老铁们,再见。