C语言零基础从入门到精通之—递归

递归



一,什么是递归

1.递归的详细定义

程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

2.递归的简单理解

所谓递归从字面意思上就可以看出来,递归可以拆分成传递和回调两个过程,递归是一个不断调用自身,并返回结果的过称,当然递归也不可能无限制的调用返回,需要条件来控制递归,并且需要有程序实现不断地向递归的条件靠拢,否则就算有递归的条件,也只是不断地在原地踏步,这也就指出了递归的两个条件

3.递归的基本思想

递归的基本思想,是把规模较大的一个问题,分解成规模较小的多个处理方法相同的子问题去解决,而每一个子问题又可以继续拆分成多个更小的处理方法相同的子问题。

最重要的一点就是假设所有的子问题已经解决了,现在要基于已经解决的子问题来解决当前问题;递归就是一个逐层深入的过程,打个比方就好比,就相当于与我们编程的时候遇到一个特别难的问题,我自己不能解决,我们需要求助他人,这个难题很多人都解决不了,最后传到了编程界最牛的人手里被解决了(这就好比递归的限制条件,如果大牛都解决不了那不崩了),当大牛解决问题后答案被返还回我,这样这个问题就被我解决了,这就是递归。

C语言零基础从入门到精通之—递归_第1张图片

4.递归存在的两个必要条件

  • 存在限制条件,当满足这个限制条件的时候,递归便不再继续。

  • 每次递归调用之后越来越接近这个限制条件。

二, 如何使用递归以及使用过程中的一些问题

1.举例练习递归

例1 接受一个整形值(无符号),按顺时针打印它的每一位。例如:输入:1234,输出1 2 3 4.

#include

void print(int n)
{
     
	if (n > 9)
	{
     
		print(n / 10);
	}
	printf("%d\n", n % 10);
}

int main()
{
     
	int num = 1234;
	print(num);
	return 0;
}

代码分析:这是一个很简单的代码,目的是输入1234,输出1 2 3 4 首先进入函数print,当n大于9时执行程序,否则执行下一句程序,当输入1234时,进入判断条件,在一次调用print函数,并将n/10,变成123,在一次进入print函数,执行判断语句,变成12,再重复上述过程直到n = 1,时,再一次进入print程序时不满足条件,执行下面条件输出1,然后返回上一级输出2,依次输出3,4,具体过程如图所示

C语言零基础从入门到精通之—递归_第2张图片

例2 编写函数不允许创建临时变量,求字符串长度。

首先,我们先用一个创建临时变量代码实现求字符串长度的代码,来进一步加深不用创建临时变量求字符串长度的方法

#include

int mn_strlen(char* p)
{
     
	int count = 0;
	while (*p != '\0')
	{
     
		p++;//将指针后移一位
		count++;//如果不是\0就是自增1
	}
	return count;
}

int main()
{
     
	char arr[] = "bit";
	printf("%d\n", mn_strlen(arr));
	return 0;
}

代码分析:改代码模拟实现了strlen的功能,灵活的运用了指针,strlen函数的原理是:找到字符串结束标志\0, 并统计\0前所有字符的个数。

下面我们来看看不用创建临时变量,来实现求字符串长度的方法。

#include

int  MN_strlen(char* p)
{
     
    if (*p == '\0')
	{
     
		return 0;
	}
	else
	{
     
		return 1 + MN_strlen(p + 1);
	}
}

int main()
{
     
	char arr[] = "bit";
	printf("%d\n", MN_strlen(arr));
	return 0;
}

C语言零基础从入门到精通之—递归_第3张图片

例3 :求n的阶乘。(不考虑溢出)

#include

int Fac(int n)
{
     
	if (n <= 1)
	{
     
		return 1;
	}
	else
	{
     
		return n * Fac(n - 1);
	}
}

int main()
{
     
	int input;
	printf("请输入一个数:\n");
	scanf("%d", &input);
	printf("%d 的阶乘为 %d\n", input, Fac(input));
	return 0;
}

代码分析: 当n小于等于1时n的阶乘为1,当n大于1时n的阶乘为n乘n-1的阶乘,以此类推n-1的阶乘等于n-1乘n-2的阶乘,直到n等于1为止,返回n等于1的阶乘,从而返回n等于二的阶乘,直到返回n等于n-1的阶乘,进而求出n阶乘。

例4 :求第n个斐波那契数。 (不考虑溢出)

#include

int fib(int n)
{
     
	if (n <= 2)
	{
     
		return 1;
	}
	else
	{
     
		return fib(n - 1) + fib(n - 2);
	}
}

int main()
{
     
	int input;
	printf("请输入一个数:\n");
	scanf("%d", &input);
	printf("第n个斐波那契数为%d\n", fib(input));
	return 0;
}

在运算例3,例4时可能会发生一些问题

  • 当我们运行fib这个函数时,如果我们想要求第50个或者更大的数时我们会发现,我们通常会需要更长的时间。
  • 当我们运行Fac这个函数时,如果我们想要求1000的阶乘或者更大的数时我们会发现程序崩溃了

为什么呢?

我们发现fib在执行过程中有很多计算步骤都是重复的,下面我们将代码修改一下:

int count = 0;
int fib(int n)
{
     
	if (n == 3)
	{
     
		count++;
	}
	if (n <= 2)
	{
     
		return 1;
	}
	else
	{
     
		return fib(n - 1) + fib(n - 2);
	}
}

最后我们可看以看到输出的count是一个很大的数,这说明了这种算法在计算上有很多重复,这也是递归算法的缺点,递归算法的优点在于代码简洁,易于理解,但缺点在于时间和空间消耗较大,并且有可能发生栈溢出现象,递归中又有很多计算都是重复的,递归的本质时把一个问题分解成两个或多个小 问题,多个小问题存在重叠的部分,即存在重复计算,正如上面斐波那契数列的递归实现。

在调试fib函数的时候,如果你的参数比较大,那就会报错:stack overflow (栈溢出)这样的信息,系统分配给栈的空间是有限的,如果出现死了死循环或者死递归,这样有可能导致一直开辟空间,最终导致栈空间耗尽的情况,这种现象我们成为栈溢出。

那么如何解决这种问题呢?

三, 如何解决栈溢出问题,并对程序进行优化。

有以下两种方法:
第一种就是:

1. 将递归改为尾递归的形式:

那么什么是尾递归呢,以下内容是对尾递归的书面解释:如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码,下面我们通过一幅图片来解释一下什么时尾递归。

C语言零基础从入门到精通之—递归_第4张图片

当编译器测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高,实际上尾递归并没有像普通递归那样每次都会在栈上开辟新的空间,而是选则去覆盖之前的空间,这样下来最终相当于尾递归最后只开辟了一次栈空间,只返回了一次,这样大大减少了栈空间的开辟,提高了空间的利用率,也减少了时间的浪费。

下面我们用尾递归来重新改写一下n的阶乘。

#include

int fab(int n, int a)//每次调用函数时都将a初始化为1
{
     
	if (n < 0)
	{
     
		return 0;
	}
	if (n == 0)
	{
     
		return 1;
	}
	if (n == 1)
	{
     
		return a;//调用尾递归最后将a的值返回
	}
	if (n > 1)
	{
     
		return fab(n - 1, n * a);//开始调用递归直到n=1为止
	}
}

int main()
{
     
	int input;
	printf("请输入一个数: >\n");
	scanf("%d", &input);
	int ret = fab(input, 1);
	printf("%d\n", ret);
	return 0;
}

这样通过图片和实例,理解起来应该会很容易

第二种就是:

2. 将递归改为非递归的形式:

将递归改为非递归的形式:又细分为下面两种方法, 而这里只总结第一种方法。
1.迭代的算法:所谓迭代简单的理解理解就是重复的反馈活动,就是重复执行同一算法,并将上一次运算的结果,作为下一次运算的初始值,直到循环满足条件,停止并返回相应的值。

迭代相比较递归具有的优点是,代码运行效率高,时间和空间上没有多余的开销,相比较于递归的不足之处在于代码没有递归简洁,通俗易懂。

下面我们用迭代来写一下n的阶乘和第n个斐波拉契数。

//迭代法求n的阶乘
int fac(int n)
{
     
	int i = 0;
	int ret = 1;
	for (i = 1; i <=  n; i++)
	{
     
		ret *= i;
	}
	return ret;
}

int main()
{
     
	int input;
	printf("请输入一个数:\n");
	scanf("%d", &input);
	printf("%d 的阶乘为 %d\n", input, fac(input));
	return 0;
}

//迭代法求第n个斐波那契数
int fib(int n)
{
     
	int a = 1;
	int b = 1;
	int c = 1;
	while (n > 2)
	{
     
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}

int main()
{
     
	int input;
	printf("请输入一个数:\n");
	scanf("%d", &input);
	printf("第n个斐波那契数为%d\n", fib(input));
	return 0;
}

使用这种方法可以很好的解决递归的一些缺点,并且很好的解决了栈溢出的问题。

2.借助堆栈模拟递归的执行过程。这种方法就不具体介绍了,因为我还没有学数据结构,自己的理解也并不是很好,后期学了数据结构,就在写一篇关于这种方法的博客吧。

四, 那么递归和迭代我们应该如何选择呢

1.许多问题是以递归的形式解决的,这是因为他比非递归的形式更为清晰。

2.但是这些问题的迭代形式实现往往比递归实现效率更高,虽然代码可读性稍微差一些。

3.当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以弥补它所带来的运行时的开销。

五,函数递归的几个经典题目

  • 汉诺塔问题
  • 青蛙跳台阶问题

你可能感兴趣的:(C语言零基础从入门到精通之—递归)