admin管理员组

文章数量:829187

【C语言】函数番外篇——递归

前言

在前面的文章中我们提到过C允许函数调用其自己调用自己,而这种调用便是我们今天主角——递归(recursion)。递归作为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义时有直接或者间接调用自身的一种方法,它通常把一个大型复杂问题层层转化为一个与原问题相似但规模较小的问题来求解,使用递归只需要少量的代码就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思想——大事化小

一、递归引入

我们先通过一个例子来进入递归的学习。该函数在主函数中调用fun()函数,这次调用称为“第一级递归”,然后fun()函数调用自己,这次调用称为“第二级递归”,之后过程以此类推。在该程序中共有四级递归。为了进一步了解递归的过程,我们引入一个变量n,显示它的值与地址。

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void fun(int n)
{printf("Level %d:n location %p\n", n, &n);   //#1if (n < 4)fun(n + 1);printf("LEVEL %d:n location %p\n", n, &n);   //#2
}
int main()
{fun(1);return 0;
}

程序输出为:

Level    1:n location 00B7FCE8
Level    2:n location 00B7FC10
Level    3:n location 00B7FB38
Level    4:n location 00B7FA60
LEVEL  4:n location 00B7FA60
LEVEL  3:n location 00B7FB38
LEVEL  2:n location 00B7FC10
LEVEL  1:n location 00B7FCE8

接下来我们输入地分析上述程序是如何工作的。 首先主函数main()调用了带参数1的fun()函数,执行结果是fun()中的形参为1,故打印语句#1 打印Level 1。然后由于n<4,fun()(第一级)调用实际参数为n+1的fun()函数(第二级),其形参仍然小于4继续打印语句#1 打印Level 2。以此类推,下面两次调用打印的分别是Level 3及Level 4。

当执行到n=4时(此时处于第四级),在执行完打印语句#1后,if的判断条件为假,不再调用fun()函数,而执行打印语句#2,打印完后程序返回到第三级,继续执行第三级后面的语句,在执行完第三级的程序后返回第二级,以此类推,直到返回到主函数中。

下图更为直观地展现了程序的走向。

 二、递归的基本要素

在我们初次接触递归时可能会觉得递归有些难以理解,有些绕不过来,其实简单来说递归就包含三个要素:①使用递归编写该函数的作用是什么  ②递归结束条件 ③写出等价函数关系。我们用一个简单的例子来讲解我们是怎么来编写递归函数的。

1、要素一:明确该递归函数的目的

我们编写一个函数首先应该明确的便是它的功能是什么,编写它来解决我们一个怎样的问题。我们编写一个函数来求解n!定义函数如下:

//求解n!
int fun(int n)
{}

在明确了其功能后我们来看看其第二要素

2、要素二:递归终止条件

一个递归函数必须包含能让递归调用停止的语句,否则,函数会一直调用自己,进入一个无底洞。会引起栈溢出错误(StackOverflowError)。通常,在编写递归函数时我们经常使用if或其他等价的判断条件在形参等于某个特定值时终止递归,因此这也提醒我们每次调用时形参必须不同,形参需要发生改变。

当n的值等于0或者1时,它的阶乘为1,根据要素二我们继续完善代码:

//求解n!
int fun(int n)
{if (n == 0 || n == 1){return 1;}
}

当然递归的结束条件不唯一。

3、要素三:写出等价函数关系

这个要素的要点是,通过不断缩小参数范围,缩小之后,我们可以通过一些变量写出的式子,使原函数的结果不变。

例如缩小n!,我们可以写成n!=n*(n-1)!。这样范围就变成了n-1,范围变小了,为了使函数保持不变我们乘了一个n。因此,我们找的等价关系为:fun(n)=n*fun(n-1)。

熟练这个步骤需要更多的练习才能熟练掌握。

根据要素三我们进一步完善代码:

//求解n!
int fun(int n)
{if (n == 0 || n == 1){return 1;}if (n >= 2)return fun(n - 1) * n;
}

到此,这个递归函数就完成了。熟练地写出递归函数需要一个漫长的练习过程。在后面的文章中会讲解经典的使用递归求解的问题。

三、递归与循环的区别与联系

在学习递归与循环区别之前,我们先对一个知识点进行补充,以便更好地理解上文所说的栈溢出。

1、存储区域

C语言与内存息息相关,在C语言中拥有三种不同的内存池,分别为:静态区(static)、栈区(stack)、堆区(heap)。

静态区(static):全局变量存储,在程序的整个生命周期都存在。

栈区(stack):局部变量存储(自动、连续的内存)

堆区(heap):动态存储(非常大的内存池,非连续分配)

a.静态区 

静态内存在程序的整个生命周期都存在,而且通用用来存储全局变量,通过static关键字创建变量。如下:(注意:其定义在函数外部)

int sum;

在很多系统中这个变量占用4byte的内存。全局变量是静态的,所以它的整个程序中只有一次拷贝。我们也可以通过static将局部变量强制转化为全局变量。如下:

static int sum;

b.栈区

栈用来存储函数内部的变量(包括main()函数),它是一个LIFO(Last In Last Out—后进先出)的结构,在数据结构专栏中我们会详细介绍。每当一个函数声明一个新的变量它将被压入栈中。每当一个函数运行结束后,这个函数所有在栈中相关的变量都将被删除,而且它们所占用的内存也会被释放,这就产生了局部变量。栈内存在函数调用时分割为连续的帧。

但是栈区的大小通常都有一个限制,根据操作系统的不同而不同。如果一个程序尝试将过多的信息放入栈内时就会发生我们上文所说到的栈溢出。

c.堆区

堆是一个相对于栈的概念。堆是一个可以动态使用的大内存池。这一部分系统不能自动管理,你必须显示地申请(如使用malloc之类的函数)和释放内存(如free)。在变量使用结束时不释放内存或者释放内存失败会造成内存泄漏,该内存仍然被占用。与栈区不同的是堆区除了物理内存的大小限制之外通常没有其他大小限制。堆区内存的访问需要使用指针。

内存使用案例:

#include<stdio.h>
#include<stdlib.h>int x;int main()
{int y;char* arr;y = 1024;printf("stack memory:%d\n", y);arr = malloc(100 * sizeof(char));arr[0] = 'a';printf("heap memory:%c\n", arr[0]);free(arr);return 0;
}

 

2.递归与循环

C语言是一门面向对象编程语言,而对象包括数据和操作,而递归与循环是非常相似的操作,但它们都有适合自己操作的数据。递归与循环本质上都是代码的重复使用。

递归与循环的不同点:

(1)程序规模:递归的规模很小,且受到系统栈大小的限制;而循环规模很大,几乎不会受到限制。

(2)重复单位:递归的重复单位为函数;循环的重复单位为语句。

(3)问题解决方向:递归往往是自上而下,通过将问题的规模不断减小来解决;而循环可以是自上而下也可以自下而上,但通常是自下而上。

等等。

四、总结

要熟练地使用递归来解决问题需要我们进行更多的练习,把握递归的核心思想。

本文标签: C语言函数番外篇递归